@sideboard-ai/core 0.1.157 → 0.1.159

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 (38) hide show
  1. package/dist/{abletime-BEIDJG7W.js → abletime-HOQBDQC4.js} +2 -2
  2. package/dist/{abletime-GWZE6OYM.js → abletime-LK7GV556.js} +2 -2
  3. package/dist/{agents-LCDXN2B2.js → agents-5HY76BCR.js} +4 -4
  4. package/dist/{agents-C55LAEUR.js → agents-67ZBH5OE.js} +5 -5
  5. package/dist/{app-settings-VRQI3FIE.js → app-settings-BJ5GFAE5.js} +13 -17
  6. package/dist/{app-settings-H3INSR67.js → app-settings-YWJ4MVYM.js} +13 -17
  7. package/dist/{chunk-NVXVEGGM.js → chunk-5U4NZLBH.js} +4 -0
  8. package/dist/{chunk-DVJBO3M7.js → chunk-AGLPYXDT.js} +1 -1
  9. package/dist/{chunk-KQ4JOGUH.js → chunk-DWUOKF3W.js} +11 -5
  10. package/dist/{chunk-D7EGJ3EI.js → chunk-EQIJ5MHR.js} +119 -110
  11. package/dist/{chunk-3BWVOUQG.js → chunk-FLG4A5FJ.js} +119 -110
  12. package/dist/{chunk-2SDMENAL.js → chunk-JV4VNFMS.js} +1 -1
  13. package/dist/{chunk-LQMFCS54.js → chunk-KEDHHXKW.js} +3 -13
  14. package/dist/{chunk-CJQLPPEJ.js → chunk-MEZXWELT.js} +4 -14
  15. package/dist/{chunk-CVR4RJ6G.js → chunk-O7RWWPMR.js} +12 -13
  16. package/dist/{chunk-GWEEGE6L.js → chunk-OYOXLY6B.js} +404 -115
  17. package/dist/{chunk-4I4VKAPZ.js → chunk-SIMFKJIB.js} +13 -14
  18. package/dist/{chunk-SUCJAWV4.js → chunk-TGU3LSDG.js} +402 -113
  19. package/dist/{chunk-N27GVFZY.js → chunk-UBMCEZRE.js} +1 -1
  20. package/dist/{chunk-TIJ6QVX5.js → chunk-V4R6L2ZL.js} +15 -6
  21. package/dist/{coordinator-prompt-AM47SATF.js → coordinator-prompt-7NKY7QRK.js} +4 -4
  22. package/dist/{coordinator-prompt-26IJU2AV.js → coordinator-prompt-CNI6AK65.js} +3 -3
  23. package/dist/{global-workspace-QV7CC7SK.js → global-workspace-FKAJZHO5.js} +4 -4
  24. package/dist/{global-workspace-5BIW26YR.js → global-workspace-IJYCUO4M.js} +3 -3
  25. package/dist/index.cjs +1037 -648
  26. package/dist/index.d.cts +71 -57
  27. package/dist/index.d.ts +71 -57
  28. package/dist/index.js +339 -250
  29. package/dist/mcp/run-stdio.cjs +967 -575
  30. package/dist/mcp/run-stdio.js +283 -187
  31. package/dist/{orchestrator-BUH3LJLL.js → orchestrator-KFLS5BTP.js} +5 -5
  32. package/dist/{orchestrator-KQO4MXBI.js → orchestrator-NSVP7DJH.js} +7 -7
  33. package/dist/{plan-file-OZEBFSGQ.js → plan-file-I24VJVP2.js} +2 -2
  34. package/dist/{workspaces-NYSM2EPX.js → workspaces-D2M3AM2M.js} +3 -3
  35. package/dist/{workspaces-Q2VCHIAS.js → workspaces-OFSDJ7PZ.js} +4 -4
  36. package/dist/{worktree-PEWDAX5O.js → worktree-BQE5KLRY.js} +3 -3
  37. package/dist/{worktree-T4SWFSTT.js → worktree-EA6AD6D6.js} +2 -2
  38. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -578,27 +578,27 @@ var init_nested_electron_env = __esm({
578
578
  });
579
579
 
580
580
  // src/store/account-profile.ts
581
- function accountRoleLabel(role) {
582
- const known = ACCOUNT_ROLE_LABELS[role];
581
+ function legacyRoleLabel(role) {
582
+ const known = LEGACY_ROLE_LABELS[role];
583
583
  if (known) return known;
584
584
  return role.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
585
585
  }
586
- function normalizeAccountRole(value) {
586
+ function normalizeLegacyRole(value) {
587
587
  const key = (value ?? "").trim().toLowerCase().replace(/[_\s]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
588
588
  if (key === "both") return void 0;
589
589
  if (key.length < 2 || key.length > 40) return void 0;
590
590
  if (!/^[a-z][a-z0-9-]*$/.test(key)) return void 0;
591
591
  return key;
592
592
  }
593
- function normalizeAccountRoles(roles, legacyRole) {
593
+ function legacyRoleLabels(roles, legacyRole) {
594
594
  const raw = Array.isArray(roles) ? roles : [];
595
595
  const out = [];
596
596
  const seen = /* @__PURE__ */ new Set();
597
597
  const push = (value) => {
598
- const role = normalizeAccountRole(typeof value === "string" ? value : "");
599
- if (!role || seen.has(role) || out.length >= ACCOUNT_ROLE_MAX) return;
598
+ const role = normalizeLegacyRole(typeof value === "string" ? value : "");
599
+ if (!role || seen.has(role) || out.length >= 16) return;
600
600
  seen.add(role);
601
- out.push(role);
601
+ out.push(legacyRoleLabel(role));
602
602
  };
603
603
  for (const item of raw) push(item);
604
604
  if (out.length === 0) push(legacyRole);
@@ -608,117 +608,83 @@ function normalizeProfileNotes(value) {
608
608
  if (typeof value !== "string") return "";
609
609
  return value.replace(/\r\n/g, "\n").trim().slice(0, PROFILE_NOTES_MAX);
610
610
  }
611
- function reviewTeamHintsForRoles(roles) {
612
- const seen = /* @__PURE__ */ new Set();
613
- const out = [];
614
- for (const role of roles) {
615
- const hints = ROLE_TEAM_HINTS[role] ?? teamHintsForCustomRole(role);
616
- for (const hint of hints) {
617
- const key = hint.toLowerCase();
618
- if (seen.has(key)) continue;
619
- seen.add(key);
620
- out.push(hint);
621
- }
622
- }
623
- return out;
624
- }
625
- function teamHintsForCustomRole(role) {
626
- const slug = role.trim().toLowerCase();
627
- if (!slug) return [];
628
- if (slug.endsWith("-team") || slug.endsWith("-squad")) return [slug];
629
- return [`${slug}-team`, slug];
611
+ function foldLegacyRolesIntoNotes(roles, legacyRole, notes) {
612
+ const text7 = normalizeProfileNotes(notes);
613
+ const labels = legacyRoleLabels(roles, legacyRole);
614
+ if (!labels.length) return text7;
615
+ if (text7.toLowerCase().startsWith("roles:")) return text7;
616
+ const prefix = `Roles: ${labels.join(", ")}`;
617
+ return text7 ? `${prefix}. ${text7}` : prefix;
630
618
  }
631
619
  function resolveAccountProfile(input) {
632
620
  return resolveViewerProfile(input);
633
621
  }
634
622
  function resolveViewerProfile(account, project) {
635
- const accountRoles = normalizeAccountRoles(account?.roles, account?.role);
636
- const projectRoles = normalizeAccountRoles(project?.roles, project?.role);
637
- const rolesFromProject = projectRoles.length > 0;
638
- const roles = rolesFromProject ? projectRoles : accountRoles;
639
- const accountNotes = normalizeProfileNotes(account?.notes);
640
- const projectNotes = normalizeProfileNotes(project?.notes);
641
- const notes = [accountNotes, projectNotes].filter(Boolean).join("\n");
623
+ const accountNotes = foldLegacyRolesIntoNotes(
624
+ account?.roles,
625
+ account?.role,
626
+ account?.notes
627
+ );
628
+ const projectNotes = foldLegacyRolesIntoNotes(
629
+ project?.roles,
630
+ project?.role,
631
+ project?.notes
632
+ );
642
633
  return {
643
- roles,
644
- roleLabels: roles.map((role) => accountRoleLabel(role)),
645
- reviewTeamHints: reviewTeamHintsForRoles(roles),
646
- notes,
634
+ notes: [accountNotes, projectNotes].filter(Boolean).join("\n"),
647
635
  accountNotes,
648
- projectNotes,
649
- rolesFromProject
636
+ projectNotes
650
637
  };
651
638
  }
652
- function preferTeamsForRole(viewerTeams, hints) {
653
- const teams = viewerTeams.map((t) => t.trim()).filter(Boolean);
654
- const want = hints.map((t) => t.trim()).filter(Boolean);
655
- if (!want.length) return teams;
656
- if (!teams.length) return want;
657
- const hintKeys = want.map((t) => t.toLowerCase());
658
- const matched = teams.filter((team) => {
659
- const key = team.toLowerCase();
660
- return hintKeys.some((hint) => key === hint || key.includes(hint) || hint.includes(key));
661
- });
662
- return matched.length ? matched : teams;
663
- }
664
639
  function compactNotes(notes, max = 160) {
665
640
  const oneLine = notes.replace(/\s+/g, " ").trim();
666
641
  if (oneLine.length <= max) return oneLine;
667
642
  return `${oneLine.slice(0, max - 1).trimEnd()}\u2026`;
668
643
  }
669
644
  function formatAccountProfilePlaybookLine(profile) {
670
- if (profile.roleLabels.length === 0 && !profile.notes) return "";
671
- const parts = [];
672
- if (profile.roleLabels.length) {
673
- parts.push(
674
- `roles ${profile.roleLabels.join(", ")} (tickets to work on / PRs to review; prefer those teams)`
675
- );
676
- }
677
- if (profile.notes) {
678
- parts.push(`notes: ${compactNotes(profile.notes)}`);
679
- }
680
- return `- Viewer profile: ${parts.join(". ")}. A team review request is not a claim \u2014 only an individual reviewer is. When they ask to find work, use these notes with list_issues (tickets) and list_prs(queue=review) (reviews) and show the options. Only create_thread and start when they also asked to start.`;
645
+ if (!profile.accountNotes) return "";
646
+ return `- Account context (Settings \u2192 Agents): ${compactNotes(profile.accountNotes)}. When they ask to find work, use this with list_issues (tickets) and list_prs(queue=review) (reviews) and show the options. Only create_thread and start when they also asked to start. To change it, show the proposed text, ask_user (Save this context / Do not save), wait, then update_viewer_context(scope=account, confirmed=true).`;
681
647
  }
682
648
  function formatProjectProfilePlaybookLines(projects) {
683
649
  const lines = projects.map((project) => {
684
- const roles = (project.roleLabels ?? []).filter(Boolean);
685
650
  const notes = normalizeProfileNotes(project.notes);
686
- if (!roles.length && !notes) return "";
687
- const bits = [];
688
- if (roles.length) bits.push(`roles ${roles.join(", ")}`);
689
- if (notes) bits.push(compactNotes(notes, 120));
690
- return `- ${project.name}: ${bits.join(". ")}`;
651
+ if (!notes) return "";
652
+ return `- ${project.name}: ${compactNotes(notes, 120)}`;
691
653
  }).filter(Boolean);
692
654
  if (!lines.length) return "";
693
- return ["Project overrides (Settings \u2192 Projects; empty roles inherit account):", ...lines].join(
694
- "\n"
695
- );
655
+ return [
656
+ "Project context (Settings \u2192 Projects; adds to account context):",
657
+ ...lines
658
+ ].join("\n");
696
659
  }
697
660
  function formatWorkspaceProfileSuffix(profile) {
698
- const bits = [];
699
- if (profile.roles.length) bits.push(`roles:${profile.roles.join(",")}`);
700
- if (profile.projectNotes) bits.push(`notes:${compactNotes(profile.projectNotes, 80)}`);
701
- return bits.length ? ` ${bits.join(" ")}` : "";
661
+ if (!profile.projectNotes) return "";
662
+ return ` context:${compactNotes(profile.projectNotes, 80)}`;
663
+ }
664
+ function formatViewerContextDirective(profile) {
665
+ const account = profile.accountNotes || "(empty)";
666
+ const project = profile.projectNotes || "(empty \u2014 account context applies)";
667
+ return [
668
+ "Viewer context (Settings \u2192 Agents / Projects) \u2014 how to find this user\u2019s tickets and review PRs:",
669
+ `- Account: ${compactNotes(profile.accountNotes || account, 240)}`,
670
+ `- This project: ${compactNotes(profile.projectNotes || project, 240)}`,
671
+ "To change these, show the proposed text in chat, call ask_user (Save this context / Do not save), wait for the answer, then update_viewer_context with confirmed=true. Never write context without confirmation. get_viewer_context reads the current values."
672
+ ].join("\n");
673
+ }
674
+ function formatViewerContextReminder() {
675
+ return "Viewer context: get_viewer_context to read; update_viewer_context only after ask_user confirms (Save this context / Do not save).";
702
676
  }
703
- var ACCOUNT_ROLE_PRESETS, ACCOUNT_ROLES, ACCOUNT_ROLE_MAX, PROFILE_NOTES_MAX, ACCOUNT_ROLE_LABELS, ROLE_TEAM_HINTS;
677
+ var PROFILE_NOTES_MAX, LEGACY_ROLE_LABELS;
704
678
  var init_account_profile = __esm({
705
679
  "src/store/account-profile.ts"() {
706
680
  "use strict";
707
- ACCOUNT_ROLE_PRESETS = ["engineering", "design", "product"];
708
- ACCOUNT_ROLES = ACCOUNT_ROLE_PRESETS;
709
- ACCOUNT_ROLE_MAX = 16;
710
681
  PROFILE_NOTES_MAX = 2e3;
711
- ACCOUNT_ROLE_LABELS = {
682
+ LEGACY_ROLE_LABELS = {
712
683
  engineering: "Engineering",
713
684
  design: "Design",
714
685
  product: "Product",
715
686
  qa: "QA"
716
687
  };
717
- ROLE_TEAM_HINTS = {
718
- engineering: ["engineering-team", "engineering", "eng-team"],
719
- design: ["design-team", "design"],
720
- product: ["product-team", "product"]
721
- };
722
688
  }
723
689
  });
724
690
 
@@ -982,16 +948,11 @@ var init_issue_source_labels = __esm({
982
948
  // src/store/app-settings.ts
983
949
  var app_settings_exports = {};
984
950
  __export(app_settings_exports, {
985
- ACCOUNT_ROLES: () => ACCOUNT_ROLES,
986
- ACCOUNT_ROLE_LABELS: () => ACCOUNT_ROLE_LABELS,
987
- ACCOUNT_ROLE_MAX: () => ACCOUNT_ROLE_MAX,
988
- ACCOUNT_ROLE_PRESETS: () => ACCOUNT_ROLE_PRESETS,
989
951
  FOLLOW_UP_BEHAVIORS: () => FOLLOW_UP_BEHAVIORS,
990
952
  GITHUB_GIT_AUTH_MODES: () => GITHUB_GIT_AUTH_MODES,
991
953
  HARNESS_ENV_KEYS: () => HARNESS_ENV_KEYS,
992
954
  ISSUE_SOURCE_LABELS: () => ISSUE_SOURCE_LABELS,
993
955
  PROFILE_NOTES_MAX: () => PROFILE_NOTES_MAX,
994
- accountRoleLabel: () => accountRoleLabel,
995
956
  appSettingsPath: () => appSettingsPath,
996
957
  applyAppEnvironment: () => applyAppEnvironment,
997
958
  autoArchiveOnMergeEnabled: () => autoArchiveOnMergeEnabled,
@@ -1015,9 +976,12 @@ __export(app_settings_exports, {
1015
976
  emptyPublicIntegrations: () => emptyPublicIntegrations,
1016
977
  ensureSlackDeviceIdentity: () => ensureSlackDeviceIdentity,
1017
978
  findProjectProfileKey: () => findProjectProfileKey,
979
+ foldLegacyRolesIntoNotes: () => foldLegacyRolesIntoNotes,
1018
980
  followUpBehavior: () => followUpBehavior,
1019
981
  formatAccountProfilePlaybookLine: () => formatAccountProfilePlaybookLine,
1020
982
  formatProjectProfilePlaybookLines: () => formatProjectProfilePlaybookLines,
983
+ formatViewerContextDirective: () => formatViewerContextDirective,
984
+ formatViewerContextReminder: () => formatViewerContextReminder,
1021
985
  formatWorkspaceProfileSuffix: () => formatWorkspaceProfileSuffix,
1022
986
  getAbleTimeAccessToken: () => getAbleTimeAccessToken,
1023
987
  getAbleTimeHost: () => getAbleTimeHost,
@@ -1038,7 +1002,7 @@ __export(app_settings_exports, {
1038
1002
  maxConcurrentAgents: () => maxConcurrentAgents,
1039
1003
  orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
1040
1004
  orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
1041
- preferTeamsForRole: () => preferTeamsForRole,
1005
+ readViewerContext: () => readViewerContext,
1042
1006
  resolveAccountProfile: () => resolveAccountProfile,
1043
1007
  resolveAccountProfileFromSettings: () => resolveAccountProfileFromSettings,
1044
1008
  resolveAgentExecutable: () => resolveAgentExecutable,
@@ -1048,7 +1012,6 @@ __export(app_settings_exports, {
1048
1012
  resolveThreadDefaults: () => resolveThreadDefaults,
1049
1013
  resolveViewerProfile: () => resolveViewerProfile,
1050
1014
  resolveViewerProfileForRepo: () => resolveViewerProfileForRepo,
1051
- reviewTeamHintsForRoles: () => reviewTeamHintsForRoles,
1052
1015
  saveAbleTimeConnection: () => saveAbleTimeConnection,
1053
1016
  saveAppSettings: () => saveAppSettings,
1054
1017
  saveLinearOAuth: () => saveLinearOAuth,
@@ -1065,7 +1028,8 @@ __export(app_settings_exports, {
1065
1028
  updateDefaultsSettings: () => updateDefaultsSettings,
1066
1029
  updateIntegrationsSettings: () => updateIntegrationsSettings,
1067
1030
  updateOpencodeSettings: () => updateOpencodeSettings,
1068
- updateProjectProfileSettings: () => updateProjectProfileSettings
1031
+ updateProjectProfileSettings: () => updateProjectProfileSettings,
1032
+ writeViewerContext: () => writeViewerContext
1069
1033
  });
1070
1034
  function emptyPublicIntegrations() {
1071
1035
  return {
@@ -1323,9 +1287,7 @@ function normalizeDefaults(raw) {
1323
1287
  if (typeof source.fast === "boolean") {
1324
1288
  out.fast = source.fast;
1325
1289
  }
1326
- const roles = normalizeAccountRoles(source.roles, source.role);
1327
- if (roles.length) out.roles = roles;
1328
- const notes = normalizeProfileNotes(source.notes);
1290
+ const notes = foldLegacyRolesIntoNotes(source.roles, source.role, source.notes);
1329
1291
  if (notes) out.notes = notes;
1330
1292
  return out;
1331
1293
  }
@@ -1336,11 +1298,9 @@ function normalizeProjectProfile(raw) {
1336
1298
  if (!raw || typeof raw !== "object") return void 0;
1337
1299
  const source = raw;
1338
1300
  const out = {};
1339
- const roles = normalizeAccountRoles(source.roles, source.role);
1340
- if (roles.length) out.roles = roles;
1341
- const notes = normalizeProfileNotes(source.notes);
1301
+ const notes = foldLegacyRolesIntoNotes(source.roles, source.role, source.notes);
1342
1302
  if (notes) out.notes = notes;
1343
- return out.roles || out.notes ? out : void 0;
1303
+ return out.notes ? out : void 0;
1344
1304
  }
1345
1305
  function normalizeProjects(raw) {
1346
1306
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -1899,17 +1859,13 @@ function updateDefaultsSettings(patch) {
1899
1859
  defaults.fast = Boolean(patch.fast);
1900
1860
  }
1901
1861
  }
1902
- if ("roles" in patch) {
1903
- const roles = normalizeAccountRoles(patch.roles);
1904
- if (!roles.length) delete defaults.roles;
1905
- else defaults.roles = roles;
1906
- delete defaults.role;
1907
- }
1908
1862
  if ("notes" in patch) {
1909
1863
  const notes = normalizeProfileNotes(patch.notes);
1910
1864
  if (!notes) delete defaults.notes;
1911
1865
  else defaults.notes = notes;
1912
1866
  }
1867
+ delete defaults.roles;
1868
+ delete defaults.role;
1913
1869
  return saveAppSettings({ ...current, defaults });
1914
1870
  }
1915
1871
  function updateProjectProfileSettings(repoPath, patch) {
@@ -1919,17 +1875,14 @@ function updateProjectProfileSettings(repoPath, patch) {
1919
1875
  const projects = { ...current.projects };
1920
1876
  const existing = projects[key] ?? {};
1921
1877
  const next = { ...existing };
1922
- if ("roles" in patch) {
1923
- const roles = normalizeAccountRoles(patch.roles);
1924
- if (!roles.length) delete next.roles;
1925
- else next.roles = roles;
1926
- }
1927
1878
  if ("notes" in patch) {
1928
1879
  const notes = normalizeProfileNotes(patch.notes);
1929
1880
  if (!notes) delete next.notes;
1930
1881
  else next.notes = notes;
1931
1882
  }
1932
- if (!next.roles?.length && !next.notes) delete projects[key];
1883
+ delete next.roles;
1884
+ delete next.role;
1885
+ if (!next.notes) delete projects[key];
1933
1886
  else projects[key] = next;
1934
1887
  return saveAppSettings({ ...current, projects });
1935
1888
  }
@@ -1954,6 +1907,60 @@ function resolveViewerProfileForRepo(repoPath, settings = loadAppSettings()) {
1954
1907
  const project = key ? settings.projects[key] : void 0;
1955
1908
  return resolveViewerProfile(settings.defaults, project);
1956
1909
  }
1910
+ function readViewerContext(repoPath) {
1911
+ const settings = loadAppSettings();
1912
+ const key = findProjectProfileKey(settings.projects, repoPath);
1913
+ const resolved = resolveViewerProfile(
1914
+ settings.defaults,
1915
+ key ? settings.projects[key] : void 0
1916
+ );
1917
+ return {
1918
+ account: resolved.accountNotes,
1919
+ project: resolved.projectNotes,
1920
+ combined: resolved.notes,
1921
+ repoPath: key ?? (repoPath?.trim() ? profileRepoKey(repoPath) : null)
1922
+ };
1923
+ }
1924
+ function writeViewerContext(input) {
1925
+ const proposed = normalizeProfileNotes(input.context);
1926
+ if (input.scope === "project") {
1927
+ const settings = loadAppSettings();
1928
+ const key = findProjectProfileKey(settings.projects, input.repoPath) ?? profileRepoKey(input.repoPath ?? "");
1929
+ if (!key) {
1930
+ return {
1931
+ ok: false,
1932
+ error: "repoPath required",
1933
+ message: "Pass repoPath (or run from a registered worktree) when updating project context.",
1934
+ current: "",
1935
+ proposed
1936
+ };
1937
+ }
1938
+ const current2 = resolveViewerProfileForRepo(key, settings).projectNotes;
1939
+ if (!input.confirmed) {
1940
+ return {
1941
+ ok: false,
1942
+ error: "Confirmation required",
1943
+ message: CONFIRM_PROJECT,
1944
+ current: current2,
1945
+ proposed
1946
+ };
1947
+ }
1948
+ updateProjectProfileSettings(key, { notes: proposed || null });
1949
+ return { ok: true, scope: "project", context: proposed, repoPath: key };
1950
+ }
1951
+ const current = resolveAccountProfileFromSettings().accountNotes;
1952
+ if (!input.confirmed) {
1953
+ return {
1954
+ ok: false,
1955
+ error: "Confirmation required",
1956
+ message: CONFIRM_ACCOUNT,
1957
+ current,
1958
+ proposed
1959
+ };
1960
+ }
1961
+ updateDefaultsSettings({ notes: proposed || null });
1962
+ return { ok: true, scope: "account", context: proposed };
1963
+ }
1957
1964
  function resolveThreadDefaults(settings = loadAppSettings()) {
1958
1965
  return {
1959
1966
  agent: getDefaultAgent(settings),
@@ -2300,7 +2307,7 @@ function childEnvWithAppSettings(extra) {
2300
2307
  function harnessEnvKey(harness) {
2301
2308
  return HARNESS_ENV_KEYS[harness];
2302
2309
  }
2303
- var import_node_crypto2, import_node_fs7, import_node_os3, import_node_path8, HARNESS_ENV_KEYS, DEFAULT_AGENTS, GITHUB_GIT_AUTH_MODES, FOLLOW_UP_BEHAVIORS, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, GIT_AUTH_MODES, EMPTY_SETTINGS, DEFAULT_CLI_BIN;
2310
+ var import_node_crypto2, import_node_fs7, import_node_os3, import_node_path8, HARNESS_ENV_KEYS, DEFAULT_AGENTS, GITHUB_GIT_AUTH_MODES, FOLLOW_UP_BEHAVIORS, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, GIT_AUTH_MODES, EMPTY_SETTINGS, CONFIRM_ACCOUNT, CONFIRM_PROJECT, DEFAULT_CLI_BIN;
2304
2311
  var init_app_settings = __esm({
2305
2312
  "src/store/app-settings.ts"() {
2306
2313
  "use strict";
@@ -2351,6 +2358,8 @@ var init_app_settings = __esm({
2351
2358
  projects: {},
2352
2359
  advanced: {}
2353
2360
  };
2361
+ CONFIRM_ACCOUNT = "Show the user the proposed account context, call ask_user (Save this context / Do not save), wait for their answer, then retry with confirmed=true.";
2362
+ CONFIRM_PROJECT = "Show the user the proposed project context, call ask_user (Save this context / Do not save), wait for their answer, then retry with confirmed=true.";
2354
2363
  DEFAULT_CLI_BIN = {
2355
2364
  claude: "claude",
2356
2365
  codex: "codex",
@@ -4373,12 +4382,12 @@ var init_worktree_labels = __esm({
4373
4382
  });
4374
4383
 
4375
4384
  // src/git/gh-errors.ts
4376
- function isGhRateLimitError(text6) {
4377
- return /API rate limit (already )?exceeded/i.test(text6) || /rate limit exceeded/i.test(text6);
4385
+ function isGhRateLimitError(text7) {
4386
+ return /API rate limit (already )?exceeded/i.test(text7) || /rate limit exceeded/i.test(text7);
4378
4387
  }
4379
- function isGhPrBodyTooLongError(text6) {
4388
+ function isGhPrBodyTooLongError(text7) {
4380
4389
  return /body is too long|body.{0,20}too long|maximum is 65536|exceeds the maximum allowed size|request (entity |is )?too large|payload too large|413 Request Entity Too Large/i.test(
4381
- text6
4390
+ text7
4382
4391
  );
4383
4392
  }
4384
4393
  function clampGithubPrBody(body, max = GITHUB_PR_BODY_SAFE_CHARS) {
@@ -4398,21 +4407,21 @@ function formatRateLimitResetHint(resetEpochSec, nowMs = Date.now()) {
4398
4407
  const hours = Math.ceil(mins / 60);
4399
4408
  return `in about ${hours} hour${hours === 1 ? "" : "s"}`;
4400
4409
  }
4401
- function omitGhBodyArg(text6) {
4402
- return text6.replace(
4410
+ function omitGhBodyArg(text7) {
4411
+ return text7.replace(
4403
4412
  /--body(?:-file)?(\s+|=)(?:'[^']*'|"[^"]*"|\S+)/g,
4404
4413
  "--body <omitted>"
4405
4414
  );
4406
4415
  }
4407
- function capGhErrorText(text6, max = 400) {
4408
- const t = text6.trim();
4416
+ function capGhErrorText(text7, max = 400) {
4417
+ const t = text7.trim();
4409
4418
  if (t.length <= max) return t;
4410
4419
  return `${t.slice(0, max - 1).trimEnd()}\u2026`;
4411
4420
  }
4412
- function extractGhErrorDetail(text6) {
4413
- const trimmed = omitGhBodyArg(text6.trim());
4421
+ function extractGhErrorDetail(text7) {
4422
+ const trimmed = omitGhBodyArg(text7.trim());
4414
4423
  if (!trimmed) return "";
4415
- if (isGhPrBodyTooLongError(text6) || isGhPrBodyTooLongError(trimmed)) {
4424
+ if (isGhPrBodyTooLongError(text7) || isGhPrBodyTooLongError(trimmed)) {
4416
4425
  return "GraphQL: body is too long (GitHub PR body / request size limit).";
4417
4426
  }
4418
4427
  const graphql = trimmed.match(/\bGraphQL:\s*(.+)$/im);
@@ -4452,9 +4461,9 @@ function formatGhLandError(raw, opts) {
4452
4461
  }
4453
4462
  return detail || "Failed to create or update pull request";
4454
4463
  }
4455
- function isPrNotMergeableError(text6) {
4464
+ function isPrNotMergeableError(text7) {
4456
4465
  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(
4457
- text6
4466
+ text7
4458
4467
  );
4459
4468
  }
4460
4469
  function formatMergePrError(raw) {
@@ -4666,8 +4675,8 @@ var init_github_agent_auth = __esm({
4666
4675
  });
4667
4676
 
4668
4677
  // src/git/stale-lock.ts
4669
- function isIndexLockError(text6) {
4670
- return /Unable to create ['"][^'"]*index\.lock['"]: File exists/i.test(text6);
4678
+ function isIndexLockError(text7) {
4679
+ return /Unable to create ['"][^'"]*index\.lock['"]: File exists/i.test(text7);
4671
4680
  }
4672
4681
  function clearStaleIndexLock(gitDir, maxAgeMs = STALE_INDEX_LOCK_MS, now = Date.now()) {
4673
4682
  const lockPath = (0, import_node_path17.join)(gitDir, "index.lock");
@@ -6090,14 +6099,7 @@ async function listPrs(repoPath, opts) {
6090
6099
  const limit = resolved.limit ?? 200;
6091
6100
  const reviewerKey = (resolved.reviewer ?? "").trim().toLowerCase();
6092
6101
  const needsUnclaimed = reviewerKey === "unassigned" || reviewerKey === "none" || reviewerKey === "null";
6093
- let profile;
6094
- try {
6095
- profile = resolveViewerProfileForRepo(repoPath);
6096
- } catch {
6097
- profile = resolveViewerProfile();
6098
- }
6099
- const fetchedTeams = needsUnclaimed ? await listGithubViewerTeamSlugs(repoPath).catch(() => []) : [];
6100
- const viewerTeams = resolved.viewerTeams ?? (needsUnclaimed ? preferTeamsForRole(fetchedTeams, profile.reviewTeamHints) : void 0);
6102
+ const viewerTeams = resolved.viewerTeams ?? (needsUnclaimed ? await listGithubViewerTeamSlugs(repoPath).catch(() => []) : void 0);
6101
6103
  const fetchLimit = prListFetchLimit(limit, resolved.reviewer);
6102
6104
  const args = buildGhPrListArgs({
6103
6105
  slug,
@@ -7430,16 +7432,15 @@ function accountRolePlaybookLine() {
7430
7432
  return "";
7431
7433
  }
7432
7434
  }
7433
- function accountRoleReminderLine() {
7435
+ function accountContextReminderLine() {
7434
7436
  let profile;
7435
7437
  try {
7436
7438
  profile = resolveAccountProfileFromSettings();
7437
7439
  } catch {
7438
7440
  return "";
7439
7441
  }
7440
- if (profile.roleLabels.length === 0 && !profile.notes) return "";
7441
- const roles = profile.roleLabels.length ? profile.roleLabels.join(", ") : "no roles";
7442
- return `- Viewer profile: ${roles}. Use Settings notes with list_issues / list_prs when they ask to find work.`;
7442
+ if (!profile.accountNotes) return "";
7443
+ return "- Account context is set. Use get_viewer_context / Settings notes with list_issues / list_prs when they ask to find work. Update only after ask_user confirms.";
7443
7444
  }
7444
7445
  function projectProfilePlaybookBlock() {
7445
7446
  let settings;
@@ -7456,7 +7457,6 @@ function projectProfilePlaybookBlock() {
7456
7457
  const key = path2.replace(/\/+$/, "");
7457
7458
  return {
7458
7459
  name: names.get(key) ?? key.split("/").filter(Boolean).pop() ?? path2,
7459
- roleLabels: resolved.rolesFromProject ? resolved.roleLabels : [],
7460
7460
  notes: resolved.projectNotes
7461
7461
  };
7462
7462
  });
@@ -7470,7 +7470,7 @@ function coordinatorTurnReminder(opts) {
7470
7470
  `- YOUR orchestration thread id is ${opts.parentId} \u2014 pass parentThreadId="${opts.parentId}" on create_thread, or omit it.`,
7471
7471
  goal ? `- Goal / title: ${goal}` : null,
7472
7472
  accountDefaultsPlaybookLine(),
7473
- accountRoleReminderLine() || null,
7473
+ accountContextReminderLine() || null,
7474
7474
  "- Status: list_board (worktree Kanban: New \u2192 Draft \u2192 Review \u2192 Merged) or list_threads. Link chats as `[Title](sideboard://thread/<id>)`. Merge only if the user asked. If a child is stopped/error/broken, it did not finish \u2014 resume or tell the user."
7475
7475
  ].filter(Boolean).join("\n");
7476
7476
  }
@@ -7524,7 +7524,7 @@ function ensureGlobalCoordinatorCwd(opts) {
7524
7524
  "Never pass `agent=codex` when you yourself are Codex \u2014 nested Codex deadlocks on shared ~/.codex locks. Omit agent (Account default) or use cursor/claude.",
7525
7525
  "Typical flow (Home board): list_board \u2192 create_thread (sourceType=ticket|pr|branch) \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
7526
7526
  "Typical flow (branch / explicit source): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
7527
- "Typical flow (find work): list_workspaces \u2192 pick repo(s) matching viewer roles/notes \u2192 Sideboard list_issues / linear_* (tickets) or list_prs(queue=review) (reviews) \u2192 show the options. Never wait on Claude Linear MCP. Only create_thread + send_to_thread when they asked to start (e.g. \u201Cfind me work and start it\u201D) \u2014 then wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
7527
+ "Typical flow (find work): list_workspaces \u2192 pick repo(s) matching account / project context \u2192 Sideboard list_issues / linear_* (tickets) or list_prs(queue=review) (reviews) \u2192 show the options. Never wait on Claude Linear MCP. Only create_thread + send_to_thread when they asked to start (e.g. \u201Cfind me work and start it\u201D) \u2014 then wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
7528
7528
  'Typical flow (review inbox): list_workspaces \u2192 list_prs(queue=review, limit=N) \u2192 show those PRs (ticket ids in the title when present). create_thread sourceType=pr only when they asked to start / do the work. Do not list_issues for "tickets to review".',
7529
7529
  "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
7530
7530
  "Always ask worktree agents to commit, push, and open draft PRs (`ask_git` / `send_to_thread`). If the user gave a goal (Greptile 5/5, CI green), pass that goal through so they watch-fix-push until it lands \u2014 do not start that loop on a plain push, and do not tell the human to poll. Tell them to merge only when the user explicitly asked. The worktree agent runs git/gh; never merge from this orchestration cwd."
@@ -7578,10 +7578,10 @@ var init_coordinator_prompt = __esm({
7578
7578
  "Role: you oversee Sideboard worktree agents across registered repos. You do not live inside one of those worktrees.",
7579
7579
  "Sideboard MCP (fleet control \u2014 prefer these for status and orchestration):",
7580
7580
  "Discover:",
7581
- "- list_workspaces \u2014 registered repos (path + github slug + roles/notes when set). Use that profile to pick the right repo for tickets or reviews.",
7581
+ "- list_workspaces \u2014 registered repos (path + github slug + project context when set). Use account / project context to pick the right repo for tickets or reviews.",
7582
7582
  "- list_board \u2014 Home Kanban of worktrees (New / Draft / Review / Merged; one card per checkout). Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind, column, limit.",
7583
- '- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces. Review is PRs (the surface for assigned ticket work), not the tickets: "Get me N tickets to review" \u2192 list_prs(queue=review, limit=N) then create_thread sourceType=pr. That is open non-draft PRs labeled eng-review with no individual user reviewer yet. A team request (engineering-team) is not a claim \u2014 the viewer is on that team and can pick it up; claimed means an individual account is the reviewer. Bots ignored. Prefer teams that match Settings \u2192 Agents / Projects roles (check one or more of Engineering, Design, Product, or extras they added \u2014 never a combined both value; project roles override account for that repo). queue=mine is review-requested:@me; queue=approved|changes uses those labels. Also: state, label, reviewer=me|unassigned|login, query, limit default 40 max 250; raise limit or tighten when truncated. list_issues (query, assignee=me|unassigned|all|user, limit default 40 max 250) lists Linear, AbleTime, or GitHub tickets \u2014 do not use it for that review-inbox ask. When they ask for tickets to work on, use their notes (assignee=me or unassigned as the notes say) and prefer ones that match their roles.',
7584
- "- Find work: when they ask to find work, pick up tickets, or list reviews, use Settings \u2192 Agents (account) plus Settings \u2192 Projects (per-repo) roles and notes. Tickets \u2192 Sideboard list_issues / linear_* (Account Linear). Reviews \u2192 list_prs(queue=review). Do not call Claude Linear MCP or any other vendor Linear MCP \u2014 those HTTP connectors hang or flap on the first turn. If a vendor MCP is down or reconnecting, ignore it and keep going with Sideboard tools. Show the options \u2014 do not create_thread or start unless they also asked to start (e.g. \u201Cfind me work and start it\u201D). Do not start this unprompted.",
7583
+ '- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces. Review is PRs (the surface for assigned ticket work), not the tickets: "Get me N tickets to review" \u2192 list_prs(queue=review, limit=N) then create_thread sourceType=pr. That is open non-draft PRs labeled eng-review with no individual user reviewer yet. A team request (engineering-team) is not a claim \u2014 the viewer is on that team and can pick it up; claimed means an individual account is the reviewer. Bots ignored. Use Settings \u2192 Agents / Projects context (roles, teams, labels \u2014 freeform text; project adds to account) to prefer the right queues. queue=mine is review-requested:@me; queue=approved|changes uses those labels. Also: state, label, reviewer=me|unassigned|login, query, limit default 40 max 250; raise limit or tighten when truncated. list_issues (query, assignee=me|unassigned|all|user, limit default 40 max 250) lists Linear, AbleTime, or GitHub tickets \u2014 do not use it for that review-inbox ask. When they ask for tickets to work on, follow their context (assignee=me or unassigned as it says).',
7584
+ "- Find work: when they ask to find work, pick up tickets, or list reviews, use Settings \u2192 Agents (account) plus Settings \u2192 Projects (per-repo) context. Tickets \u2192 Sideboard list_issues / linear_* (Account Linear). Reviews \u2192 list_prs(queue=review). Do not call Claude Linear MCP or any other vendor Linear MCP \u2014 those HTTP connectors hang or flap on the first turn. If a vendor MCP is down or reconnecting, ignore it and keep going with Sideboard tools. Show the options \u2014 do not create_thread or start unless they also asked to start (e.g. \u201Cfind me work and start it\u201D). Do not start this unprompted. To change that context, show the proposed text, ask_user (Save this context / Do not save), wait, then update_viewer_context with confirmed=true. Never write it without confirmation.",
7585
7585
  "- linear_* (when Linear is connected) \u2014 list_teams for team key/states; get/create/update/comment with ENG-123. Worktree agents have the same Account tools \u2014 prefer they comment, update status, and create spin-offs (parent=) on their own ticket. Scope errors: reconnect Linear in Account settings. Prefer these over any Linear MCP the CLI may still list.",
7586
7586
  "- github_* \u2014 get/comment/update/create GitHub issues via Account `gh` (#123). Worktree agents have these too. Prefer over any vendor GitHub MCP. Pass parent= for spin-offs.",
7587
7587
  "- abletime_* (when AbleTime is connected) \u2014 orientation first; get/comment/update/create (parent= for spin-offs); ensure_task when work has no ticket (or create_thread from the default branch auto-creates one). Worktree agents have the same Account tools.",
@@ -7591,7 +7591,8 @@ var init_coordinator_prompt = __esm({
7591
7591
  "- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
7592
7592
  "- list_models \u2014 only when you need a specific model (rare); otherwise omit model so Account defaults apply",
7593
7593
  "- list_threads / get_thread \u2014 live thread list (parent id + last message preview). get_thread on this orchestration chat lists child worktree agents (status + lastText). Also includes usage / lastTurnUsage.",
7594
- "- ask_user \u2014 composer multiple-choice only when blocked on a concrete choice (approach fork, which API). Never for hellos, check-ins, or invented \u201Cwhat should we do?\u201D menus \u2014 reply in chat. Explain options first, description on every option, then wait.",
7594
+ "- ask_user \u2014 composer multiple-choice only when blocked on a concrete choice (approach fork, which API, Save this context / Do not save). Never for hellos, check-ins, or invented \u201Cwhat should we do?\u201D menus \u2014 reply in chat. Explain options first, description on every option, then wait.",
7595
+ "- get_viewer_context / update_viewer_context \u2014 read or replace Settings \u2192 Agents (account) and Settings \u2192 Projects (per-repo) context. You can update a project: pass scope=project and repoPath from list_workspaces (this cwd is not a project). update_viewer_context requires confirmed=true after ask_user. Never write context unprompted.",
7595
7596
  "- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for Slack / away-from-keyboard work, overnight schedules, or when the user will be away. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the machine awake. Closing this chat also releases it.",
7596
7597
  "- list_schedules / create_schedule / update_schedule / delete_schedule / run_schedule \u2014 local jobs that send a prompt to an orchestration chat (threadId or self) or start a new Global chat (omit threadId). One-shot `at`, interval `every` (15m/1h/6h/1d), or 5-field `cron`. Recurring jobs without threadId open a new chat each run. Jobs fire only while Sideboard.app is running; sleep skips until wake. Overnight/unattended runs: ask the user to enable Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or call set_caffeinate.",
7597
7598
  "Workspaces:",
@@ -8126,8 +8127,8 @@ ${input.permalink}` : "";
8126
8127
 
8127
8128
  ${body}${link}`;
8128
8129
  }
8129
- function isSlackExternalReplyPrompt(text6) {
8130
- return text6.startsWith("Slack reply from ") && text6.includes("not a command");
8130
+ function isSlackExternalReplyPrompt(text7) {
8131
+ return text7.startsWith("Slack reply from ") && text7.includes("not a command");
8131
8132
  }
8132
8133
  function pendingSlackExternalReplies(messages) {
8133
8134
  let i = messages.length - 1;
@@ -8168,9 +8169,9 @@ async function continueSourceThread(threadId, prompt) {
8168
8169
  function listSlackOutboundWatches() {
8169
8170
  return pruneWatches(readStore3());
8170
8171
  }
8171
- function formatOwnerSlackFyi(userName, text6) {
8172
+ function formatOwnerSlackFyi(userName, text7) {
8172
8173
  const who = userName.trim() || "Someone";
8173
- const body = text6.trim() || "(no text)";
8174
+ const body = text7.trim() || "(no text)";
8174
8175
  return `${who} replied in Slack:
8175
8176
  ${body}`;
8176
8177
  }
@@ -8185,7 +8186,7 @@ async function relayExternalReply(opts) {
8185
8186
  const thread = readThread(threadId);
8186
8187
  if (!thread || thread.status === "archived") return "ignored";
8187
8188
  try {
8188
- const text6 = formatSlackExternalReplyPrompt({
8189
+ const text7 = formatSlackExternalReplyPrompt({
8189
8190
  userName: opts.reply.userName,
8190
8191
  kind: opts.watch.kind,
8191
8192
  toLabel: opts.watch.toLabel,
@@ -8194,7 +8195,7 @@ async function relayExternalReply(opts) {
8194
8195
  });
8195
8196
  appendMessage(threadId, {
8196
8197
  role: "agent",
8197
- text: text6,
8198
+ text: text7,
8198
8199
  ts: (/* @__PURE__ */ new Date()).toISOString()
8199
8200
  });
8200
8201
  } catch {
@@ -8508,20 +8509,20 @@ function looksLikeMinifiedJsDump(line) {
8508
8509
  function looksLikeNestedElectronCrash(line) {
8509
8510
  return /HasCustomHostObject|ElectronInitializeICUandStartNode/i.test(line);
8510
8511
  }
8511
- function looksLikeNativeEventLoopCrash(text6) {
8512
- return /uv_run|uv__io_poll|SpinEventLoopInternal/i.test(text6);
8512
+ function looksLikeNativeEventLoopCrash(text7) {
8513
+ return /uv_run|uv__io_poll|SpinEventLoopInternal/i.test(text7);
8513
8514
  }
8514
- function looksLikeV8Oom(text6) {
8515
+ function looksLikeV8Oom(text7) {
8515
8516
  return /javascript heap|reached heap limit|OOMErrorHandler|FatalProcessOutOfMemory|Allocation failed - JavaScript heap/i.test(
8516
- text6
8517
+ text7
8517
8518
  );
8518
8519
  }
8519
- function looksLikeHomebrewLibuvCrash(text6) {
8520
- if (!looksLikeNativeEventLoopCrash(text6)) return false;
8521
- return /Cellar\/(?:libuv|node)|libuv\.\d+\.dylib/i.test(text6);
8520
+ function looksLikeHomebrewLibuvCrash(text7) {
8521
+ if (!looksLikeNativeEventLoopCrash(text7)) return false;
8522
+ return /Cellar\/(?:libuv|node)|libuv\.\d+\.dylib/i.test(text7);
8522
8523
  }
8523
- function clipStderr(text6, maxChars) {
8524
- const trimmed = text6.trim();
8524
+ function clipStderr(text7, maxChars) {
8525
+ const trimmed = text7.trim();
8525
8526
  if (trimmed.length <= maxChars) return trimmed;
8526
8527
  return trimmed.slice(0, maxChars);
8527
8528
  }
@@ -8555,16 +8556,16 @@ function summarizeTurnStderr(tail, maxChars = 500) {
8555
8556
  const joined = (useful.length ? useful : tail).slice(-6).join("\n").trim();
8556
8557
  return clipStderr(joined, maxChars);
8557
8558
  }
8558
- function looksLikeInvalidAgentSession(text6) {
8559
- const lower = text6.trim().toLowerCase();
8559
+ function looksLikeInvalidAgentSession(text7) {
8560
+ const lower = text7.trim().toLowerCase();
8560
8561
  if (!lower) return false;
8561
8562
  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) || /\brun\b.{0,80}\bnot found for agent\b/.test(lower);
8562
8563
  }
8563
- function looksLikeRetryableRunnerCrash(text6) {
8564
- if (looksLikeAgentFailureMessage(text6)) return false;
8565
- if (looksLikeInvalidAgentSession(text6)) return false;
8566
- if (looksLikeV8Oom(text6)) return false;
8567
- const lower = text6.trim().toLowerCase();
8564
+ function looksLikeRetryableRunnerCrash(text7) {
8565
+ if (looksLikeAgentFailureMessage(text7)) return false;
8566
+ if (looksLikeInvalidAgentSession(text7)) return false;
8567
+ if (looksLikeV8Oom(text7)) return false;
8568
+ const lower = text7.trim().toLowerCase();
8568
8569
  if (/cannot find (?:package|module)|err_module_not_found/.test(lower)) return false;
8569
8570
  if (!lower) return true;
8570
8571
  return /uv_run|spineventloopinternal|libuv|homebrew node \+ shared libuv|cursor runner crashed in node|hascustomhostobject|electroninitializeicuandstartnode|nested chromium|truncated crash dump|connection stalled|network request failed|cursor startup failed:.+\(retryable\)|sig(?:segv|abrt|ill)|segmentation fault|illegal instruction|fatal error/.test(
@@ -8576,12 +8577,12 @@ function shouldRetryFailedAgentTurn(detail, opts) {
8576
8577
  if (looksLikeV8Oom(detail) && opts.hasSession) return true;
8577
8578
  return looksLikeRetryableRunnerCrash(detail);
8578
8579
  }
8579
- function looksLikeAgentFailureMessage(text6) {
8580
- const lower = text6.trim().toLowerCase();
8580
+ function looksLikeAgentFailureMessage(text7) {
8581
+ const lower = text7.trim().toLowerCase();
8581
8582
  if (!lower) return false;
8582
8583
  return /you've hit your|hit your (session|weekly|opus) limit|usage limit/.test(lower) || /credit balance is too low|out of credits|insufficient.?quota|quota.?exceeded/.test(lower) || /invalid user api key|invalid api key|not logged in|not authenticated|unauthorized/.test(
8583
8584
  lower
8584
- ) || /\b429\b|too many requests|rate.?limit/.test(lower) || /prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower) || /\[resource_exhausted\]|resource_exhausted/.test(lower) || /findFilesWithRipgrep/.test(text6);
8585
+ ) || /\b429\b|too many requests|rate.?limit/.test(lower) || /prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower) || /\[resource_exhausted\]|resource_exhausted/.test(lower) || /findFilesWithRipgrep/.test(text7);
8585
8586
  }
8586
8587
  function fallbackTurnFailDetail(assistantText) {
8587
8588
  const t = assistantText.trim();
@@ -9640,10 +9641,10 @@ var init_brightsy = __esm({
9640
9641
  });
9641
9642
 
9642
9643
  // src/agents/claude-mcp.ts
9643
- function parseMcpList(text6) {
9644
+ function parseMcpList(text7) {
9644
9645
  const servers = [];
9645
9646
  const seen = /* @__PURE__ */ new Set();
9646
- for (const raw of text6.split("\n")) {
9647
+ for (const raw of text7.split("\n")) {
9647
9648
  const line = raw.trim();
9648
9649
  if (!line || /^Checking MCP/i.test(line)) continue;
9649
9650
  const m = line.match(/^(.+?):\s+\S+/);
@@ -9763,6 +9764,9 @@ function toolDescription(name, input) {
9763
9764
  if (/present_files$/i.test(name)) {
9764
9765
  return str2(input?.title) ? `Files ${str2(input?.title)}` : "Present files";
9765
9766
  }
9767
+ if (/wait_for_job$/i.test(name)) {
9768
+ return str2(input?.id) ? `Wait for ${str2(input?.id)}` : "Wait for job";
9769
+ }
9766
9770
  if (isSubagentToolName(name)) {
9767
9771
  const desc = str2(input?.description);
9768
9772
  const sub = asRecord(input?.subagentType);
@@ -9831,8 +9835,8 @@ function isSubagentToolName(name) {
9831
9835
  if (/^(task|agent|spawn_agent)$/i.test(n)) return true;
9832
9836
  return /connectedAgentRequest/i.test(n);
9833
9837
  }
9834
- function isInternalAgentStatusText(text6) {
9835
- const t = text6.trim();
9838
+ function isInternalAgentStatusText(text7) {
9839
+ const t = text7.trim();
9836
9840
  if (!t) return false;
9837
9841
  if (/waiting for gate to pass/i.test(t)) return true;
9838
9842
  if (/^agent is running\b/i.test(t) && t.length < 160) return true;
@@ -9846,9 +9850,9 @@ function lastTextPart(parts, type) {
9846
9850
  for (let i = parts.length - 1; i >= 0; i--) {
9847
9851
  const p = parts[i];
9848
9852
  if (p.type !== type) continue;
9849
- const text6 = p.text.trim();
9850
- if (!text6 || isInternalAgentStatusText(text6)) continue;
9851
- return text6;
9853
+ const text7 = p.text.trim();
9854
+ if (!text7 || isInternalAgentStatusText(text7)) continue;
9855
+ return text7;
9852
9856
  }
9853
9857
  return "";
9854
9858
  }
@@ -9937,7 +9941,7 @@ function liveActivitySummary(parts, opts) {
9937
9941
  );
9938
9942
  const runningPoll = [...tools].reverse().find((t) => t.status === "running" && isPollWrapperToolName(t.name));
9939
9943
  const thinking = lastTextPart(parts, "thinking");
9940
- const text6 = lastTextPart(parts, "text");
9944
+ const text7 = lastTextPart(parts, "text");
9941
9945
  if (runningSubs.length > 0) {
9942
9946
  const heads = runningSubs.map(toolLabel);
9943
9947
  const head = runningSubs.length === 1 ? heads[0] : `${runningSubs.length} subagents \xB7 ${heads.slice(0, 2).join(" \xB7 ")}`;
@@ -9947,7 +9951,7 @@ function liveActivitySummary(parts, opts) {
9947
9951
  if (runningTop) return toolLabel(runningTop);
9948
9952
  if (runningPoll) return toolLabel(runningPoll);
9949
9953
  if (thinking) return thinking.length > 96 ? `\u2026${thinking.slice(-96)}` : thinking;
9950
- if (text6) return "Writing reply\u2026";
9954
+ if (text7) return "Writing reply\u2026";
9951
9955
  const last = [...tools].reverse().find((t) => !isPollWrapperToolName(t.name)) ?? tools.at(-1);
9952
9956
  if (last) {
9953
9957
  const label = toolLabel(last);
@@ -9979,9 +9983,9 @@ function toolFilePath(input) {
9979
9983
  if (!input) return void 0;
9980
9984
  return str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
9981
9985
  }
9982
- function countLines(text6) {
9983
- if (!text6) return 0;
9984
- return text6.split("\n").length;
9986
+ function countLines(text7) {
9987
+ if (!text7) return 0;
9988
+ return text7.split("\n").length;
9985
9989
  }
9986
9990
  function diffFromInput(input) {
9987
9991
  if (!input) return {};
@@ -10206,9 +10210,9 @@ function partsToAssistantText(parts) {
10206
10210
  (p) => p.type === "text" && !messagePartParentId(p)
10207
10211
  ).map((p) => p.text).join("").trim();
10208
10212
  }
10209
- function stripBrightsyNdjsonNoise(text6) {
10210
- if (!text6 || !text6.includes('"type"')) return text6;
10211
- let out = text6;
10213
+ function stripBrightsyNdjsonNoise(text7) {
10214
+ if (!text7 || !text7.includes('"type"')) return text7;
10215
+ let out = text7;
10212
10216
  out = out.replace(
10213
10217
  /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
10214
10218
  ""
@@ -10250,7 +10254,10 @@ var init_profile = __esm({
10250
10254
  "ask_user",
10251
10255
  "present_plan",
10252
10256
  "present_schema",
10253
- "present_files"
10257
+ "present_files",
10258
+ "wait_for_job",
10259
+ "get_viewer_context",
10260
+ "update_viewer_context"
10254
10261
  ];
10255
10262
  WORKTREE_GITHUB_MCP_TOOLS = [
10256
10263
  "github_get_issue",
@@ -10563,8 +10570,8 @@ function isBrightsyConnected() {
10563
10570
  return false;
10564
10571
  }
10565
10572
  }
10566
- function promptMentionsBrightsy(text6) {
10567
- return BRIGHTSY_WORD.test(text6 ?? "");
10573
+ function promptMentionsBrightsy(text7) {
10574
+ return BRIGHTSY_WORD.test(text7 ?? "");
10568
10575
  }
10569
10576
  function isBrightsyMcpToolName(name) {
10570
10577
  const n = name.toLowerCase();
@@ -10842,7 +10849,10 @@ var init_injected_mcp = __esm({
10842
10849
  "mcp__sideboard__present_schema",
10843
10850
  "mcp__sideboard__present_files",
10844
10851
  "mcp__sideboard__ask_user",
10845
- "mcp__sideboard__present_plan"
10852
+ "mcp__sideboard__present_plan",
10853
+ "mcp__sideboard__wait_for_job",
10854
+ "mcp__sideboard__get_viewer_context",
10855
+ "mcp__sideboard__update_viewer_context"
10846
10856
  ];
10847
10857
  SIDEBOARD_GITHUB_MCP_ALLOWED_TOOLS = ["mcp__sideboard__github_*"];
10848
10858
  SIDEBOARD_LINEAR_MCP_ALLOWED_TOOLS = ["mcp__sideboard__linear_*"];
@@ -11179,9 +11189,9 @@ function compactMetadataFromClaude(obj) {
11179
11189
  return { trigger, postTokens };
11180
11190
  }
11181
11191
  function parseIssuesJson(raw) {
11182
- const text6 = raw.trim();
11183
- const candidates = [text6];
11184
- const match = text6.match(/\[[\s\S]*\]/);
11192
+ const text7 = raw.trim();
11193
+ const candidates = [text7];
11194
+ const match = text7.match(/\[[\s\S]*\]/);
11185
11195
  if (match) candidates.push(match[0]);
11186
11196
  for (const c of candidates) {
11187
11197
  try {
@@ -11449,8 +11459,8 @@ var init_claude = __esm({
11449
11459
  if (errorDetail) {
11450
11460
  events.push({ type: "stderr", data: errorDetail });
11451
11461
  } else {
11452
- const text6 = obj.result;
11453
- if (typeof text6 === "string" && text6) events.push({ type: "stdout", data: text6 });
11462
+ const text7 = obj.result;
11463
+ if (typeof text7 === "string" && text7) events.push({ type: "stdout", data: text7 });
11454
11464
  }
11455
11465
  const usage = usageFromClaude(obj.usage);
11456
11466
  if (usage) {
@@ -11591,9 +11601,9 @@ function asObject2(value) {
11591
11601
  function listMcpNamesFromJsonMap(raw, key) {
11592
11602
  return Object.keys(asObject2(asObject2(raw)[key])).filter((n) => n.trim());
11593
11603
  }
11594
- function listMcpNamesFromCodexToml(text6) {
11604
+ function listMcpNamesFromCodexToml(text7) {
11595
11605
  try {
11596
- const parsed = (0, import_smol_toml2.parse)(text6);
11606
+ const parsed = (0, import_smol_toml2.parse)(text7);
11597
11607
  return Object.keys(asObject2(parsed.mcp_servers)).filter((n) => n.trim());
11598
11608
  } catch {
11599
11609
  return [];
@@ -11724,8 +11734,8 @@ function codexConfigHasNetworkAccess() {
11724
11734
  ];
11725
11735
  for (const path2 of candidates) {
11726
11736
  if (!(0, import_node_fs32.existsSync)(path2)) continue;
11727
- const text6 = (0, import_node_fs32.readFileSync)(path2, "utf8");
11728
- if (/network_access\s*=\s*true/.test(text6)) return true;
11737
+ const text7 = (0, import_node_fs32.readFileSync)(path2, "utf8");
11738
+ if (/network_access\s*=\s*true/.test(text7)) return true;
11729
11739
  }
11730
11740
  return false;
11731
11741
  }
@@ -11738,8 +11748,8 @@ function unwrapCodexMcpResult(result) {
11738
11748
  const texts = [];
11739
11749
  for (const item of rec.content) {
11740
11750
  if (!item || typeof item !== "object") continue;
11741
- const text6 = item.text;
11742
- if (typeof text6 === "string") texts.push(text6);
11751
+ const text7 = item.text;
11752
+ if (typeof text7 === "string") texts.push(text7);
11743
11753
  }
11744
11754
  if (texts.length) return texts.join("\n");
11745
11755
  }
@@ -11981,8 +11991,8 @@ var init_codex = __esm({
11981
11991
  if (msg) nested.push(msg);
11982
11992
  }
11983
11993
  }
11984
- for (const text6 of nested) {
11985
- events.push({ type: "stdout", data: text6, parentId: item.id });
11994
+ for (const text7 of nested) {
11995
+ events.push({ type: "stdout", data: text7, parentId: item.id });
11986
11996
  }
11987
11997
  events.push({
11988
11998
  type: "tool_result",
@@ -12733,8 +12743,8 @@ var init_opencode = __esm({
12733
12743
  return { type: "session_id", data: sid };
12734
12744
  }
12735
12745
  if (obj.type === "text") {
12736
- const text6 = obj.part?.text ?? obj.text;
12737
- if (text6) return { type: "stdout", data: text6 };
12746
+ const text7 = obj.part?.text ?? obj.text;
12747
+ if (text7) return { type: "stdout", data: text7 };
12738
12748
  }
12739
12749
  if (obj.type === "tool_use") {
12740
12750
  const part = obj.part;
@@ -13013,8 +13023,8 @@ var init_list_models = __esm({
13013
13023
  });
13014
13024
 
13015
13025
  // src/agents/session-quota.ts
13016
- function isSessionQuotaLimit(text6) {
13017
- const lower = text6.trim().toLowerCase();
13026
+ function isSessionQuotaLimit(text7) {
13027
+ const lower = text7.trim().toLowerCase();
13018
13028
  if (!lower) return false;
13019
13029
  if (/credit balance is too low|out of credits|insufficient.?quota|billing/.test(lower)) {
13020
13030
  return false;
@@ -13022,10 +13032,10 @@ function isSessionQuotaLimit(text6) {
13022
13032
  if (/prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower)) {
13023
13033
  return false;
13024
13034
  }
13025
- return /you've hit your/.test(lower) || /hit your (session|weekly|opus) limit/.test(lower) || /usage limit/.test(lower) || /rate.?limit|too many requests|\b429\b/.test(lower) && /reset/i.test(text6);
13035
+ return /you've hit your/.test(lower) || /hit your (session|weekly|opus) limit/.test(lower) || /usage limit/.test(lower) || /rate.?limit|too many requests|\b429\b/.test(lower) && /reset/i.test(text7);
13026
13036
  }
13027
- function parseSessionQuotaResetAt(text6, now = /* @__PURE__ */ new Date()) {
13028
- const absolute = text6.match(
13037
+ function parseSessionQuotaResetAt(text7, now = /* @__PURE__ */ new Date()) {
13038
+ const absolute = text7.match(
13029
13039
  /resets\s+(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)(?:\s*\(([^)]+)\))?/i
13030
13040
  );
13031
13041
  if (absolute) {
@@ -13043,7 +13053,7 @@ function parseSessionQuotaResetAt(text6, now = /* @__PURE__ */ new Date()) {
13043
13053
  }
13044
13054
  return at;
13045
13055
  }
13046
- const relative = text6.match(
13056
+ const relative = text7.match(
13047
13057
  /resets\s+in\s+(\d+)\s*(minutes?|hours?|days?)/i
13048
13058
  );
13049
13059
  if (relative) {
@@ -13624,8 +13634,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
13624
13634
  onEvent({ type: "exit", data: exitCode });
13625
13635
  const finalized = finalizeParts(parts);
13626
13636
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
13627
- const text6 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
13628
- return { exitCode, sessionId, assistantText: text6, parts: finalized, usage };
13637
+ const text7 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
13638
+ return { exitCode, sessionId, assistantText: text7, parts: finalized, usage };
13629
13639
  });
13630
13640
  return {
13631
13641
  pid: child.pid,
@@ -13904,9 +13914,9 @@ async function tryClaudeSummary(transcript, opts) {
13904
13914
  { cwd: opts?.cwd, reject: false }
13905
13915
  );
13906
13916
  if (exitCode !== 0) return null;
13907
- const text6 = stdout.trim();
13908
- if (text6.length < 40) return null;
13909
- return text6;
13917
+ const text7 = stdout.trim();
13918
+ if (text7.length < 40) return null;
13919
+ return text7;
13910
13920
  } catch {
13911
13921
  return null;
13912
13922
  }
@@ -13955,9 +13965,9 @@ function extractiveSummary(transcript) {
13955
13965
  );
13956
13966
  return parts.join("\n");
13957
13967
  }
13958
- function clipSummary(text6) {
13959
- if (text6.length <= MAX_SUMMARY_CHARS) return text6;
13960
- return `${text6.slice(0, MAX_SUMMARY_CHARS)}
13968
+ function clipSummary(text7) {
13969
+ if (text7.length <= MAX_SUMMARY_CHARS) return text7;
13970
+ return `${text7.slice(0, MAX_SUMMARY_CHARS)}
13961
13971
 
13962
13972
  [\u2026summary truncated\u2026]`;
13963
13973
  }
@@ -14111,8 +14121,8 @@ function findLastBrightsyContextSummary(messages) {
14111
14121
  continue;
14112
14122
  }
14113
14123
  if (part.status === "error") continue;
14114
- const text6 = extractBrightsyContextSummary(part.result);
14115
- if (text6) return { index: i, text: text6 };
14124
+ const text7 = extractBrightsyContextSummary(part.result);
14125
+ if (text7) return { index: i, text: text7 };
14116
14126
  }
14117
14127
  }
14118
14128
  return null;
@@ -15321,6 +15331,242 @@ var init_child_halt = __esm({
15321
15331
  }
15322
15332
  });
15323
15333
 
15334
+ // src/mcp/wait-for-turn.ts
15335
+ function mcpWaitForTurnTimeoutMs(requested) {
15336
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
15337
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
15338
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
15339
+ }
15340
+ function mcpWaitStillRunningHint(status) {
15341
+ return status === "queued" ? MCP_WAIT_QUEUED_HINT : MCP_WAIT_STILL_RUNNING_HINT;
15342
+ }
15343
+ function mcpWaitFinishedHint(status) {
15344
+ if (status === "stopped") return MCP_WAIT_STOPPED_HINT;
15345
+ if (status === "broken") return MCP_WAIT_BROKEN_HINT;
15346
+ if (status === "error") return MCP_WAIT_ERROR_HINT;
15347
+ return void 0;
15348
+ }
15349
+ var MCP_WAIT_FOR_TURN_MAX_MS, MCP_WAIT_STILL_RUNNING_HINT, MCP_WAIT_QUEUED_HINT, MCP_WAIT_STOPPED_HINT, MCP_WAIT_BROKEN_HINT, MCP_WAIT_ERROR_HINT;
15350
+ var init_wait_for_turn = __esm({
15351
+ "src/mcp/wait-for-turn.ts"() {
15352
+ "use strict";
15353
+ MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
15354
+ MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send a check-in prompt or assume a hang while progress is updating.";
15355
+ MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send a check-in prompt, force_stop, or assume it failed to start.";
15356
+ MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
15357
+ MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
15358
+ MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
15359
+ }
15360
+ });
15361
+
15362
+ // src/mcp/wait-for-job.ts
15363
+ function mcpWaitForJobTimeoutMs(requested) {
15364
+ return mcpWaitForTurnTimeoutMs(requested);
15365
+ }
15366
+ function sanitizeDetachedJobId(id) {
15367
+ const s = id.trim();
15368
+ if (!JOB_ID_RE.test(s)) {
15369
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
15370
+ }
15371
+ return s;
15372
+ }
15373
+ function jobAlive(pid) {
15374
+ if (!Number.isInteger(pid) || pid <= 0) return false;
15375
+ try {
15376
+ process.kill(pid, 0);
15377
+ return true;
15378
+ } catch {
15379
+ return false;
15380
+ }
15381
+ }
15382
+ function readIntFile(file) {
15383
+ if (!(0, import_node_fs40.existsSync)(file)) return null;
15384
+ const n = Number.parseInt((0, import_node_fs40.readFileSync)(file, "utf8").trim(), 10);
15385
+ return Number.isInteger(n) ? n : null;
15386
+ }
15387
+ function jobDir(root, id, legacy = false) {
15388
+ return (0, import_node_path40.join)(root, legacy ? LEGACY_DETACHED_JOBS_DIR : DETACHED_JOBS_DIR, id);
15389
+ }
15390
+ function resolveJobDir(root, id) {
15391
+ const modern = jobDir(root, id, false);
15392
+ if ((0, import_node_fs40.existsSync)(modern)) return modern;
15393
+ const legacy = jobDir(root, id, true);
15394
+ if ((0, import_node_fs40.existsSync)(legacy)) return legacy;
15395
+ return modern;
15396
+ }
15397
+ function listJobIdsIn(root, rel) {
15398
+ const dir = (0, import_node_path40.join)(root, rel);
15399
+ if (!(0, import_node_fs40.existsSync)(dir)) return [];
15400
+ return (0, import_node_fs40.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && JOB_ID_RE.test(e.name)).map((e) => e.name);
15401
+ }
15402
+ function listRunningDetachedJobs(worktreePath) {
15403
+ const root = worktreePath.trim();
15404
+ if (!root) return [];
15405
+ const ids = /* @__PURE__ */ new Set([
15406
+ ...listJobIdsIn(root, DETACHED_JOBS_DIR),
15407
+ ...listJobIdsIn(root, LEGACY_DETACHED_JOBS_DIR)
15408
+ ]);
15409
+ const running = [];
15410
+ for (const id of ids) {
15411
+ const dir = resolveJobDir(root, id);
15412
+ const pid = readIntFile((0, import_node_path40.join)(dir, "pid"));
15413
+ if (pid != null && jobAlive(pid)) running.push(id);
15414
+ }
15415
+ return running.sort();
15416
+ }
15417
+ function looksLikeDeferredDonePromise(text7) {
15418
+ const t = (text7 ?? "").trim();
15419
+ if (!t) return false;
15420
+ return /\b(i['’]?ll|i will)\s+let you know\b/i.test(t) || /\blet you know when\b/i.test(t) || /\b(check back|ping me)\s+when\b/i.test(t) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(t);
15421
+ }
15422
+ function formatJobStillRunningContinuePrompt(jobIds) {
15423
+ const ids = jobIds.join(", ");
15424
+ return [
15425
+ `Detached job still running: ${ids}.`,
15426
+ "Do not end this turn. Loop wait_for_job (same id) and present_artifact type=log with content=delta until stillRunning is false.",
15427
+ "Then report the result. Do not tell the user you will let them know later."
15428
+ ].join(" ");
15429
+ }
15430
+ function formatDeferredDoneContinuePrompt() {
15431
+ return [
15432
+ "You ended the turn after promising to report later, but no detached job is running.",
15433
+ "If tests/pack/deploy still need to run: start once with detached-job.js, present_artifact type=log, then loop wait_for_job until stillRunning is false.",
15434
+ "Do not say you will let the user know later."
15435
+ ].join(" ");
15436
+ }
15437
+ function turnWatchedDetachedJob(parts) {
15438
+ return parts.some((p) => {
15439
+ if (p.type !== "tool") return false;
15440
+ if (/wait_for_job$/i.test(p.name ?? "")) return true;
15441
+ const blob = [p.name, p.detail, p.description, p.input ? JSON.stringify(p.input) : ""].filter(Boolean).join(" ");
15442
+ return /detached-job\.js\b/i.test(blob);
15443
+ });
15444
+ }
15445
+ function planJobContinue(opts) {
15446
+ if (opts.isOrchestrator) return { action: "none" };
15447
+ if (opts.agent === "brightsy") return { action: "none" };
15448
+ if (opts.queueLength > 0) return { action: "none" };
15449
+ if (opts.continueCount >= MAX_JOB_CONTINUES) return { action: "none" };
15450
+ const farewell = looksLikeDeferredDonePromise(opts.chatText);
15451
+ if (opts.runningJobIds.length > 0 && (farewell || opts.watchedJob)) {
15452
+ return {
15453
+ action: "wait",
15454
+ jobIds: opts.runningJobIds,
15455
+ prompt: formatJobStillRunningContinuePrompt(opts.runningJobIds)
15456
+ };
15457
+ }
15458
+ if (looksLikeDeferredDonePromise(opts.chatText) && !opts.alreadyNudged) {
15459
+ return { action: "nudge", prompt: formatDeferredDoneContinuePrompt() };
15460
+ }
15461
+ return { action: "none" };
15462
+ }
15463
+ function tailProgress(logFile, maxLines = 12) {
15464
+ if (!(0, import_node_fs40.existsSync)(logFile)) return "(no log yet)";
15465
+ const lines = (0, import_node_fs40.readFileSync)(logFile, "utf8").split("\n");
15466
+ return lines.slice(-maxLines).join("\n");
15467
+ }
15468
+ function readLogLines(file) {
15469
+ if (!(0, import_node_fs40.existsSync)(file)) return [];
15470
+ const lines = (0, import_node_fs40.readFileSync)(file, "utf8").split("\n");
15471
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
15472
+ return lines;
15473
+ }
15474
+ function takeDelta(logFile, cursorFile) {
15475
+ const lines = readLogLines(logFile);
15476
+ const cursor = readIntFile(cursorFile) ?? 0;
15477
+ const start = Math.min(Math.max(0, cursor), lines.length);
15478
+ return { delta: lines.slice(start).join("\n"), nextCursor: lines.length };
15479
+ }
15480
+ function snapshotJob(dir) {
15481
+ const pid = readIntFile((0, import_node_path40.join)(dir, "pid"));
15482
+ const running = pid != null && jobAlive(pid);
15483
+ return {
15484
+ pid,
15485
+ running,
15486
+ exitCode: readIntFile((0, import_node_path40.join)(dir, "exit")),
15487
+ log: (0, import_node_path40.join)(dir, "log"),
15488
+ cursor: (0, import_node_path40.join)(dir, "present.cursor"),
15489
+ progress: tailProgress((0, import_node_path40.join)(dir, "log"))
15490
+ };
15491
+ }
15492
+ function toResult(id, snap, extra) {
15493
+ const failed = extra?.failed === true || !snap.running && snap.exitCode != null && snap.exitCode !== 0;
15494
+ const ok = !snap.running && snap.exitCode === 0;
15495
+ const stillRunning = snap.running && !ok;
15496
+ const { delta, nextCursor } = takeDelta(snap.log, snap.cursor);
15497
+ try {
15498
+ (0, import_node_fs40.mkdirSync)((0, import_node_path40.dirname)(snap.cursor), { recursive: true });
15499
+ (0, import_node_fs40.writeFileSync)(snap.cursor, `${nextCursor}
15500
+ `);
15501
+ } catch {
15502
+ }
15503
+ const status = ok ? "ok" : failed && !stillRunning ? "failed" : stillRunning ? "running" : "idle";
15504
+ return {
15505
+ stillRunning,
15506
+ ok,
15507
+ failed: Boolean(failed && !stillRunning && !ok),
15508
+ status,
15509
+ id,
15510
+ pid: snap.pid,
15511
+ exitCode: snap.exitCode ?? void 0,
15512
+ delta,
15513
+ progress: extra?.progress ?? snap.progress,
15514
+ hint: stillRunning ? MCP_WAIT_JOB_STILL_RUNNING_HINT : void 0
15515
+ };
15516
+ }
15517
+ async function sleepMs(ms) {
15518
+ await new Promise((resolve) => setTimeout(resolve, ms));
15519
+ }
15520
+ async function waitForDetachedJob(cwd, id, opts) {
15521
+ const jobId = sanitizeDetachedJobId(id);
15522
+ const root = cwd.trim() || process.cwd();
15523
+ const dir = resolveJobDir(root, jobId);
15524
+ const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
15525
+ const sleep2 = opts?.sleep ?? sleepMs;
15526
+ if (!(0, import_node_fs40.existsSync)(dir)) {
15527
+ return {
15528
+ stillRunning: false,
15529
+ ok: false,
15530
+ failed: true,
15531
+ status: "failed",
15532
+ id: jobId,
15533
+ delta: "",
15534
+ progress: "No detached job. Start one first.",
15535
+ hint: "Start with detached-job.js start <id> -- <command>, then call wait_for_job again."
15536
+ };
15537
+ }
15538
+ const deadline = Date.now() + timeoutMs;
15539
+ let snap = snapshotJob(dir);
15540
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
15541
+ if (!snap.running && snap.pid == null && snap.progress === "(no log yet)") {
15542
+ return toResult(jobId, snap, {
15543
+ failed: true,
15544
+ progress: "No detached job. Start one first."
15545
+ });
15546
+ }
15547
+ while (Date.now() < deadline) {
15548
+ snap = snapshotJob(dir);
15549
+ if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
15550
+ if (!snap.running) return toResult(jobId, snap, { failed: true });
15551
+ await sleep2(Math.min(2e3, Math.max(50, deadline - Date.now())));
15552
+ }
15553
+ snap = snapshotJob(dir);
15554
+ return toResult(jobId, snap);
15555
+ }
15556
+ var import_node_fs40, import_node_path40, MAX_JOB_CONTINUES, MCP_WAIT_JOB_STILL_RUNNING_HINT, JOB_ID_RE;
15557
+ var init_wait_for_job = __esm({
15558
+ "src/mcp/wait-for-job.ts"() {
15559
+ "use strict";
15560
+ import_node_fs40 = require("fs");
15561
+ import_node_path40 = require("path");
15562
+ init_workspace_scratch();
15563
+ init_wait_for_turn();
15564
+ MAX_JOB_CONTINUES = 8;
15565
+ MCP_WAIT_JOB_STILL_RUNNING_HINT = "Job is still running. present_artifact type=log with the same artifact_id and content=delta (new lines only). Then call wait_for_job again. Do not end the turn or tell the user you will let them know later.";
15566
+ JOB_ID_RE = /^[a-zA-Z0-9._-]{1,64}$/;
15567
+ }
15568
+ });
15569
+
15324
15570
  // src/detect/detect.ts
15325
15571
  async function detectAgents() {
15326
15572
  ensureAgentPath();
@@ -15915,11 +16161,11 @@ function unwrapToolResult(result) {
15915
16161
  return typeof item.text === "string" ? item.text : "";
15916
16162
  }).filter(Boolean);
15917
16163
  if (texts.length === 1) {
15918
- const text6 = texts[0];
16164
+ const text7 = texts[0];
15919
16165
  try {
15920
- return JSON.parse(text6);
16166
+ return JSON.parse(text7);
15921
16167
  } catch {
15922
- return text6;
16168
+ return text7;
15923
16169
  }
15924
16170
  }
15925
16171
  if (texts.length > 1) return texts.join("\n");
@@ -16447,7 +16693,7 @@ function reuseLiveThread(input, repoPath, match) {
16447
16693
  }
16448
16694
  async function createThread(input, _onSetupLine) {
16449
16695
  const repoPath = await resolveRepoRoot(input.repoPath);
16450
- if (!(0, import_node_fs40.existsSync)(repoPath)) {
16696
+ if (!(0, import_node_fs41.existsSync)(repoPath)) {
16451
16697
  throw new Error(`Repo not found: ${repoPath}`);
16452
16698
  }
16453
16699
  const reused = reuseLiveThread(input, repoPath, {
@@ -16627,11 +16873,11 @@ async function listLinearIssues(agent, repoPath) {
16627
16873
  }
16628
16874
  return adapter.listLinearIssues(repoPath);
16629
16875
  }
16630
- var import_node_fs40;
16876
+ var import_node_fs41;
16631
16877
  var init_create = __esm({
16632
16878
  "src/threads/create.ts"() {
16633
16879
  "use strict";
16634
- import_node_fs40 = require("fs");
16880
+ import_node_fs41 = require("fs");
16635
16881
  init_detect();
16636
16882
  init_worktree();
16637
16883
  init_home_board();
@@ -16681,8 +16927,8 @@ function summarizeTurnLive(parts) {
16681
16927
  const interesting = [...tools].reverse().find((t) => !isPollWrapperToolName(t.name)) ?? lastTool;
16682
16928
  const lastToolLabel = interesting ? interesting.description || toolDescription(interesting.name, interesting.input) || interesting.name : void 0;
16683
16929
  const thinking = lastText(parts, "thinking");
16684
- const text6 = lastText(parts, "text");
16685
- const excerptRaw = thinking || text6;
16930
+ const text7 = lastText(parts, "text");
16931
+ const excerptRaw = thinking || text7;
16686
16932
  const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
16687
16933
  const summary = liveActivitySummary(parts);
16688
16934
  return {
@@ -16734,20 +16980,20 @@ function writeTurnLive(threadId, progress) {
16734
16980
  const path2 = threadLivePath(threadId);
16735
16981
  const tmp = `${path2}.${process.pid}.tmp`;
16736
16982
  try {
16737
- (0, import_node_fs41.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
16738
- (0, import_node_fs41.renameSync)(tmp, path2);
16983
+ (0, import_node_fs42.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
16984
+ (0, import_node_fs42.renameSync)(tmp, path2);
16739
16985
  } catch {
16740
16986
  try {
16741
- (0, import_node_fs41.unlinkSync)(tmp);
16987
+ (0, import_node_fs42.unlinkSync)(tmp);
16742
16988
  } catch {
16743
16989
  }
16744
16990
  }
16745
16991
  }
16746
16992
  function readTurnLive(threadId) {
16747
16993
  const path2 = threadLivePath(threadId);
16748
- if (!(0, import_node_fs41.existsSync)(path2)) return null;
16994
+ if (!(0, import_node_fs42.existsSync)(path2)) return null;
16749
16995
  try {
16750
- const raw = JSON.parse((0, import_node_fs41.readFileSync)(path2, "utf8"));
16996
+ const raw = JSON.parse((0, import_node_fs42.readFileSync)(path2, "utf8"));
16751
16997
  if (!raw || typeof raw.summary !== "string") return null;
16752
16998
  return raw;
16753
16999
  } catch {
@@ -16759,17 +17005,17 @@ function clearTurnLive(threadId) {
16759
17005
  if (buf?.timer) clearTimeout(buf.timer);
16760
17006
  buffers.delete(threadId);
16761
17007
  const path2 = threadLivePath(threadId);
16762
- if (!(0, import_node_fs41.existsSync)(path2)) return;
17008
+ if (!(0, import_node_fs42.existsSync)(path2)) return;
16763
17009
  try {
16764
- (0, import_node_fs41.unlinkSync)(path2);
17010
+ (0, import_node_fs42.unlinkSync)(path2);
16765
17011
  } catch {
16766
17012
  }
16767
17013
  }
16768
- var import_node_fs41, buffers, FLUSH_MS, MAX_PARTS;
17014
+ var import_node_fs42, buffers, FLUSH_MS, MAX_PARTS;
16769
17015
  var init_turn_live = __esm({
16770
17016
  "src/store/turn-live.ts"() {
16771
17017
  "use strict";
16772
- import_node_fs41 = require("fs");
17018
+ import_node_fs42 = require("fs");
16773
17019
  init_message_parts();
16774
17020
  init_paths();
16775
17021
  buffers = /* @__PURE__ */ new Map();
@@ -16921,8 +17167,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
16921
17167
  );
16922
17168
  const recent = from.messages.slice(-8).map((m) => {
16923
17169
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
16924
- const text6 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
16925
- return text6 ? `- ${role}: ${text6}` : null;
17170
+ const text7 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
17171
+ return text7 ? `- ${role}: ${text7}` : null;
16926
17172
  }).filter(Boolean);
16927
17173
  const body = [
16928
17174
  `# Orchestration handoff`,
@@ -16989,7 +17235,7 @@ var init_quota_failover = __esm({
16989
17235
  // src/threads/adopt.ts
16990
17236
  function thisModuleFile() {
16991
17237
  const cjsFile = typeof __filename !== "undefined" ? __filename : "";
16992
- return cjsFile || process.argv[1] || (0, import_node_path40.join)(process.cwd(), "package.json");
17238
+ return cjsFile || process.argv[1] || (0, import_node_path41.join)(process.cwd(), "package.json");
16993
17239
  }
16994
17240
  function openReadonlySqlite(file) {
16995
17241
  const req = (0, import_node_module4.createRequire)(thisModuleFile());
@@ -17007,25 +17253,25 @@ function mapAgentType(raw) {
17007
17253
  return null;
17008
17254
  }
17009
17255
  function resolveConductorCursorAgentId(workspacePath) {
17010
- if (!workspacePath || !(0, import_node_fs42.existsSync)(CURSOR_SDK_STORE)) return null;
17256
+ if (!workspacePath || !(0, import_node_fs43.existsSync)(CURSOR_SDK_STORE)) return null;
17011
17257
  const normalized = workspacePath.replace(/\/$/, "");
17012
17258
  let best = null;
17013
17259
  let hashes;
17014
17260
  try {
17015
- hashes = (0, import_node_fs42.readdirSync)(CURSOR_SDK_STORE);
17261
+ hashes = (0, import_node_fs43.readdirSync)(CURSOR_SDK_STORE);
17016
17262
  } catch {
17017
17263
  return null;
17018
17264
  }
17019
17265
  for (const hash of hashes) {
17020
- const agentsFile = (0, import_node_path40.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
17021
- if (!(0, import_node_fs42.existsSync)(agentsFile)) continue;
17022
- let text6;
17266
+ const agentsFile = (0, import_node_path41.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
17267
+ if (!(0, import_node_fs43.existsSync)(agentsFile)) continue;
17268
+ let text7;
17023
17269
  try {
17024
- text6 = (0, import_node_fs42.readFileSync)(agentsFile, "utf8");
17270
+ text7 = (0, import_node_fs43.readFileSync)(agentsFile, "utf8");
17025
17271
  } catch {
17026
17272
  continue;
17027
17273
  }
17028
- for (const line of text6.split("\n")) {
17274
+ for (const line of text7.split("\n")) {
17029
17275
  const trimmed = line.trim();
17030
17276
  if (!trimmed) continue;
17031
17277
  try {
@@ -17045,7 +17291,7 @@ function resolveConductorCursorAgentId(workspacePath) {
17045
17291
  return best?.agentId ?? null;
17046
17292
  }
17047
17293
  async function adoptThread(input) {
17048
- if (!(0, import_node_fs42.existsSync)(input.worktreePath)) {
17294
+ if (!(0, import_node_fs43.existsSync)(input.worktreePath)) {
17049
17295
  throw new Error(`Worktree not found: ${input.worktreePath}`);
17050
17296
  }
17051
17297
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -17074,18 +17320,18 @@ function conductorDbPath() {
17074
17320
  return CONDUCTOR_DB;
17075
17321
  }
17076
17322
  function listConductorWorkspaces() {
17077
- if (!(0, import_node_fs42.existsSync)(CONDUCTOR_DB)) {
17323
+ if (!(0, import_node_fs43.existsSync)(CONDUCTOR_DB)) {
17078
17324
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
17079
17325
  }
17080
- const tmp = (0, import_node_fs42.mkdtempSync)((0, import_node_path40.join)((0, import_node_os13.tmpdir)(), "sideboard-conductor-"));
17081
- const snapshot = (0, import_node_path40.join)(tmp, "conductor.db");
17326
+ const tmp = (0, import_node_fs43.mkdtempSync)((0, import_node_path41.join)((0, import_node_os13.tmpdir)(), "sideboard-conductor-"));
17327
+ const snapshot = (0, import_node_path41.join)(tmp, "conductor.db");
17082
17328
  try {
17083
- (0, import_node_fs42.copyFileSync)(CONDUCTOR_DB, snapshot);
17329
+ (0, import_node_fs43.copyFileSync)(CONDUCTOR_DB, snapshot);
17084
17330
  for (const suffix of ["-wal", "-shm"]) {
17085
17331
  const src = `${CONDUCTOR_DB}${suffix}`;
17086
- if ((0, import_node_fs42.existsSync)(src)) {
17332
+ if ((0, import_node_fs43.existsSync)(src)) {
17087
17333
  try {
17088
- (0, import_node_fs42.copyFileSync)(src, `${snapshot}${suffix}`);
17334
+ (0, import_node_fs43.copyFileSync)(src, `${snapshot}${suffix}`);
17089
17335
  } catch {
17090
17336
  }
17091
17337
  }
@@ -17161,22 +17407,22 @@ function listConductorWorkspaces() {
17161
17407
  db.close();
17162
17408
  }
17163
17409
  } finally {
17164
- (0, import_node_fs42.rmSync)(tmp, { recursive: true, force: true });
17410
+ (0, import_node_fs43.rmSync)(tmp, { recursive: true, force: true });
17165
17411
  }
17166
17412
  }
17167
17413
  function importConductorWorkspace(workspaceId) {
17168
- if (!(0, import_node_fs42.existsSync)(CONDUCTOR_DB)) {
17414
+ if (!(0, import_node_fs43.existsSync)(CONDUCTOR_DB)) {
17169
17415
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
17170
17416
  }
17171
- const tmp = (0, import_node_fs42.mkdtempSync)((0, import_node_path40.join)((0, import_node_os13.tmpdir)(), "sideboard-conductor-"));
17172
- const snapshot = (0, import_node_path40.join)(tmp, "conductor.db");
17417
+ const tmp = (0, import_node_fs43.mkdtempSync)((0, import_node_path41.join)((0, import_node_os13.tmpdir)(), "sideboard-conductor-"));
17418
+ const snapshot = (0, import_node_path41.join)(tmp, "conductor.db");
17173
17419
  try {
17174
- (0, import_node_fs42.copyFileSync)(CONDUCTOR_DB, snapshot);
17420
+ (0, import_node_fs43.copyFileSync)(CONDUCTOR_DB, snapshot);
17175
17421
  for (const suffix of ["-wal", "-shm"]) {
17176
17422
  const src = `${CONDUCTOR_DB}${suffix}`;
17177
- if ((0, import_node_fs42.existsSync)(src)) {
17423
+ if ((0, import_node_fs43.existsSync)(src)) {
17178
17424
  try {
17179
- (0, import_node_fs42.copyFileSync)(src, `${snapshot}${suffix}`);
17425
+ (0, import_node_fs43.copyFileSync)(src, `${snapshot}${suffix}`);
17180
17426
  } catch {
17181
17427
  }
17182
17428
  }
@@ -17194,7 +17440,7 @@ function importConductorWorkspace(workspaceId) {
17194
17440
  ).get(workspaceId);
17195
17441
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
17196
17442
  const worktreePath = String(row.workspacePath);
17197
- if (!(0, import_node_fs42.existsSync)(worktreePath)) {
17443
+ if (!(0, import_node_fs43.existsSync)(worktreePath)) {
17198
17444
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
17199
17445
  }
17200
17446
  let sessionId = null;
@@ -17257,31 +17503,31 @@ function importConductorWorkspace(workspaceId) {
17257
17503
  db.close();
17258
17504
  }
17259
17505
  } finally {
17260
- (0, import_node_fs42.rmSync)(tmp, { recursive: true, force: true });
17506
+ (0, import_node_fs43.rmSync)(tmp, { recursive: true, force: true });
17261
17507
  }
17262
17508
  }
17263
17509
  async function importConductorWorkspaceAsync(workspaceId) {
17264
17510
  return importConductorWorkspace(workspaceId);
17265
17511
  }
17266
- var import_node_child_process4, import_node_fs42, import_node_os13, import_node_path40, import_node_module4, CONDUCTOR_APP_SUPPORT, CONDUCTOR_DB, CURSOR_SDK_STORE;
17512
+ var import_node_child_process4, import_node_fs43, import_node_os13, import_node_path41, import_node_module4, CONDUCTOR_APP_SUPPORT, CONDUCTOR_DB, CURSOR_SDK_STORE;
17267
17513
  var init_adopt = __esm({
17268
17514
  "src/threads/adopt.ts"() {
17269
17515
  "use strict";
17270
17516
  import_node_child_process4 = require("child_process");
17271
- import_node_fs42 = require("fs");
17517
+ import_node_fs43 = require("fs");
17272
17518
  import_node_os13 = require("os");
17273
- import_node_path40 = require("path");
17519
+ import_node_path41 = require("path");
17274
17520
  import_node_module4 = require("module");
17275
17521
  init_worktree();
17276
17522
  init_thread_store();
17277
- CONDUCTOR_APP_SUPPORT = (0, import_node_path40.join)(
17523
+ CONDUCTOR_APP_SUPPORT = (0, import_node_path41.join)(
17278
17524
  process.env.HOME ?? "",
17279
17525
  "Library",
17280
17526
  "Application Support",
17281
17527
  "com.conductor.app"
17282
17528
  );
17283
- CONDUCTOR_DB = (0, import_node_path40.join)(CONDUCTOR_APP_SUPPORT, "conductor.db");
17284
- CURSOR_SDK_STORE = (0, import_node_path40.join)(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
17529
+ CONDUCTOR_DB = (0, import_node_path41.join)(CONDUCTOR_APP_SUPPORT, "conductor.db");
17530
+ CURSOR_SDK_STORE = (0, import_node_path41.join)(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
17285
17531
  }
17286
17532
  });
17287
17533
 
@@ -17348,7 +17594,7 @@ async function openStackLayer(input, _onSetupLine) {
17348
17594
  let createdWorktree = false;
17349
17595
  const trees = await listWorktrees(repoPath);
17350
17596
  const checkedOut = trees.find((w) => w.branch === branchName);
17351
- if (checkedOut?.path && (0, import_node_fs43.existsSync)(checkedOut.path)) {
17597
+ if (checkedOut?.path && (0, import_node_fs44.existsSync)(checkedOut.path)) {
17352
17598
  if (input.reuseExistingWorktree !== false) {
17353
17599
  worktreePath = checkedOut.path;
17354
17600
  } else {
@@ -17490,7 +17736,7 @@ async function initStackFromThread(input, onSetupLine) {
17490
17736
  async function createPrStack(input, onSetupLine) {
17491
17737
  await requireAgent(input.agent);
17492
17738
  const repoPath = await resolveRepoRoot(input.repoPath);
17493
- if (!(0, import_node_fs43.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
17739
+ if (!(0, import_node_fs44.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
17494
17740
  if (!input.branches.length) throw new Error("At least one branch name required");
17495
17741
  const status = await detectGhStack(repoPath);
17496
17742
  if (!status.available) throw new Error(status.reason);
@@ -17557,7 +17803,7 @@ async function createPrStack(input, onSetupLine) {
17557
17803
  }
17558
17804
  }
17559
17805
  const claimed = new Set(threads.map((t) => t.worktreePath));
17560
- if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs43.existsSync)(bootstrap.worktreePath)) {
17806
+ if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs44.existsSync)(bootstrap.worktreePath)) {
17561
17807
  try {
17562
17808
  await removeWorktree(repoPath, bootstrap.worktreePath, {
17563
17809
  deleteBranch: bootstrap.branchName
@@ -17577,11 +17823,11 @@ function stackAgentDefaultsFrom(input) {
17577
17823
  planMode: input.planMode
17578
17824
  };
17579
17825
  }
17580
- var import_node_fs43;
17826
+ var import_node_fs44;
17581
17827
  var init_stack_layers = __esm({
17582
17828
  "src/threads/stack-layers.ts"() {
17583
17829
  "use strict";
17584
- import_node_fs43 = require("fs");
17830
+ import_node_fs44 = require("fs");
17585
17831
  init_detect();
17586
17832
  init_run();
17587
17833
  init_stack();
@@ -17594,7 +17840,7 @@ var init_stack_layers = __esm({
17594
17840
 
17595
17841
  // src/diff/diff.ts
17596
17842
  async function inspectGitWorktree(worktreePath) {
17597
- if (!worktreePath || !(0, import_node_fs44.existsSync)(worktreePath)) return "missing_worktree";
17843
+ if (!worktreePath || !(0, import_node_fs45.existsSync)(worktreePath)) return "missing_worktree";
17598
17844
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
17599
17845
  reject: false
17600
17846
  });
@@ -17602,7 +17848,7 @@ async function inspectGitWorktree(worktreePath) {
17602
17848
  return "ok";
17603
17849
  }
17604
17850
  async function initializeGitRepository(worktreePath) {
17605
- if (!worktreePath || !(0, import_node_fs44.existsSync)(worktreePath)) {
17851
+ if (!worktreePath || !(0, import_node_fs45.existsSync)(worktreePath)) {
17606
17852
  throw new Error("Worktree not found");
17607
17853
  }
17608
17854
  const status = await inspectGitWorktree(worktreePath);
@@ -17736,11 +17982,11 @@ new file mode 100644
17736
17982
  };
17737
17983
  }
17738
17984
  async function untrackedPatch(worktreePath, path2, maxHunk) {
17739
- const abs = (0, import_node_path41.join)(worktreePath, path2);
17985
+ const abs = (0, import_node_path42.join)(worktreePath, path2);
17740
17986
  try {
17741
- const st = (0, import_node_fs44.statSync)(abs);
17987
+ const st = (0, import_node_fs45.statSync)(abs);
17742
17988
  if (st.isFile() && st.size > maxHunk) {
17743
- const buf = (0, import_node_fs44.readFileSync)(abs).subarray(0, maxHunk);
17989
+ const buf = (0, import_node_fs45.readFileSync)(abs).subarray(0, maxHunk);
17744
17990
  return syntheticAddPatch(path2, buf.toString("utf8"), maxHunk);
17745
17991
  }
17746
17992
  } catch {
@@ -17834,13 +18080,13 @@ function formatStat(files) {
17834
18080
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
17835
18081
  }
17836
18082
  function emptyScopeStats() {
17837
- const z7 = emptyStat();
18083
+ const z8 = emptyStat();
17838
18084
  return {
17839
- commits: z7,
17840
- uncommitted: z7,
17841
- staged: z7,
17842
- unstaged: z7,
17843
- last_turn: z7
18085
+ commits: z8,
18086
+ uncommitted: z8,
18087
+ staged: z8,
18088
+ unstaged: z8,
18089
+ last_turn: z8
17844
18090
  };
17845
18091
  }
17846
18092
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -18229,8 +18475,8 @@ function isImageRelativePath(relativePath) {
18229
18475
  function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
18230
18476
  assertSafeRelativePath(relativePath);
18231
18477
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
18232
- const abs = (0, import_node_path41.join)(worktreePath, relativePath);
18233
- const st = (0, import_node_fs44.statSync)(abs);
18478
+ const abs = (0, import_node_path42.join)(worktreePath, relativePath);
18479
+ const st = (0, import_node_fs45.statSync)(abs);
18234
18480
  if (!st.isFile()) {
18235
18481
  throw new Error(`Not a file: ${relativePath}`);
18236
18482
  }
@@ -18239,7 +18485,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
18239
18485
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
18240
18486
  );
18241
18487
  }
18242
- const buf = (0, import_node_fs44.readFileSync)(abs);
18488
+ const buf = (0, import_node_fs45.readFileSync)(abs);
18243
18489
  return {
18244
18490
  path: relativePath,
18245
18491
  contentBase64: buf.toString("base64"),
@@ -18249,12 +18495,12 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
18249
18495
  function readWorktreeFile(worktreePath, relativePath, opts) {
18250
18496
  assertSafeRelativePath(relativePath);
18251
18497
  const maxBytes = opts?.maxBytes ?? 2e5;
18252
- const abs = (0, import_node_path41.join)(worktreePath, relativePath);
18253
- const st = (0, import_node_fs44.statSync)(abs);
18498
+ const abs = (0, import_node_path42.join)(worktreePath, relativePath);
18499
+ const st = (0, import_node_fs45.statSync)(abs);
18254
18500
  if (!st.isFile()) {
18255
18501
  throw new Error(`Not a file: ${relativePath}`);
18256
18502
  }
18257
- const buf = (0, import_node_fs44.readFileSync)(abs);
18503
+ const buf = (0, import_node_fs45.readFileSync)(abs);
18258
18504
  if (isImageRelativePath(relativePath)) {
18259
18505
  const maxImageBytes = Math.max(maxBytes, 15e6);
18260
18506
  const truncated2 = buf.length > maxImageBytes;
@@ -18297,9 +18543,9 @@ function assertSafeRelativePath(relativePath) {
18297
18543
  }
18298
18544
  function writeWorktreeFile(worktreePath, relativePath, content) {
18299
18545
  assertSafeRelativePath(relativePath);
18300
- const abs = (0, import_node_path41.join)(worktreePath, relativePath);
18301
- (0, import_node_fs44.mkdirSync)((0, import_node_path41.dirname)(abs), { recursive: true });
18302
- (0, import_node_fs44.writeFileSync)(abs, content, "utf8");
18546
+ const abs = (0, import_node_path42.join)(worktreePath, relativePath);
18547
+ (0, import_node_fs45.mkdirSync)((0, import_node_path42.dirname)(abs), { recursive: true });
18548
+ (0, import_node_fs45.writeFileSync)(abs, content, "utf8");
18303
18549
  return { path: relativePath };
18304
18550
  }
18305
18551
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -18316,12 +18562,12 @@ async function getDiffSummary(worktreePath, repoPath, opts) {
18316
18562
  truncated: full.files.length > maxFiles
18317
18563
  };
18318
18564
  }
18319
- var import_node_fs44, import_node_path41, mergeBaseCache, MERGE_BASE_TTL_MS, SHA_RE, IMAGE_EXTENSIONS2, DEFAULT_UPLOAD_MAX_BYTES;
18565
+ var import_node_fs45, import_node_path42, mergeBaseCache, MERGE_BASE_TTL_MS, SHA_RE, IMAGE_EXTENSIONS2, DEFAULT_UPLOAD_MAX_BYTES;
18320
18566
  var init_diff = __esm({
18321
18567
  "src/diff/diff.ts"() {
18322
18568
  "use strict";
18323
- import_node_fs44 = require("fs");
18324
- import_node_path41 = require("path");
18569
+ import_node_fs45 = require("fs");
18570
+ import_node_path42 = require("path");
18325
18571
  init_run();
18326
18572
  init_worktree();
18327
18573
  mergeBaseCache = /* @__PURE__ */ new Map();
@@ -18470,7 +18716,7 @@ var init_long_running = __esm({
18470
18716
 
18471
18717
  A Sideboard or Cursor **worktree turn** SIGTERMs the agent shell (and its process group) when the user sends another message or the turn is interrupted. \`block_until_ms: 0\` is not enough \u2014 the child stays in that group.
18472
18718
 
18473
- Do **not** ask the human to check back. Detach, then **wait** in 45s slices (same idea as MCP \`wait_for_turn\`) until \`stillRunning\` is false.
18719
+ Do **not** ask the human to check back. Detach, then **wait** in 45s slices (MCP \`wait_for_job\`, same idea as \`wait_for_turn\`) until \`stillRunning\` is false. Do not end the turn with \u201CI\u2019ll let you know.\u201D
18474
18720
 
18475
18721
  ## Tool
18476
18722
 
@@ -18480,7 +18726,8 @@ Use the helper from the Sideboard playbook \u2014 an absolute \`node "\u2026" st
18480
18726
  # Start (exits in ~1s; job survives this turn)
18481
18727
  node <detached-job.js> start <id> -- <command> [args...]
18482
18728
 
18483
- # Wait \u2014 returns within ~45s even if the job is still going
18729
+ # Wait \u2014 prefer MCP wait_for_job (same 45s / stillRunning contract).
18730
+ # Shell fallback:
18484
18731
  node <detached-job.js> wait <id>
18485
18732
 
18486
18733
  # Block until the process exits (humans / a turn that will not be interrupted)
@@ -18493,7 +18740,7 @@ node <detached-job.js> status <id>
18493
18740
 
18494
18741
  Wait JSON:
18495
18742
 
18496
- - \`stillRunning: true\` \u2192 exit 2 \u2192 **call wait again**. Progress is in \`progress\` / \`phase\`. Do not start a second job. Do not ping the user.
18743
+ - \`stillRunning: true\` \u2192 **call wait_for_job again** (or shell wait). Progress is in \`progress\` / \`phase\`. Do not start a second job. Do not ping the user.
18497
18744
  - \`ok: true\` \u2192 exit 0 \u2192 continue the rest of the task.
18498
18745
  - \`failed: true\` \u2192 exit 1 \u2192 read \`progress\`, fix, start **once**.
18499
18746
 
@@ -18523,7 +18770,7 @@ node <detached-job.js> wait --pid-file FILE --log-file FILE [--ok-pattern TEXT]
18523
18770
 
18524
18771
  1. \`start\` once. If JSON says \`already-running\`, do not start again.
18525
18772
  2. Immediately \`present_artifact\` \`type=log\` (same \`artifact_id\`, \`status=running\`) \u2014 the human should see **working** in the side column, not a \u201Ccheck back later\u201D message.
18526
- 3. Loop \`wait\` (use \`--timeout-ms 15000\` for a livelier pane). After each slice, \`present_artifact\` the same id with \`content=delta\` only.
18773
+ 3. Loop \`wait_for_job\` (or shell \`wait\`). After each slice, \`present_artifact\` the same id with \`content=delta\` only.
18527
18774
  4. On \`ok\`, present once more (\`status=ok\`, last \`delta\`) and finish the task. On \`failed\`, fix from the log.
18528
18775
 
18529
18776
  Never tell the user \u201Csay status when it\u2019s done.\u201D You wait.
@@ -18589,7 +18836,7 @@ function parseFrontmatter(content) {
18589
18836
  }
18590
18837
  function readSkill(skillMd, source) {
18591
18838
  try {
18592
- const content = (0, import_node_fs45.readFileSync)(skillMd, "utf8");
18839
+ const content = (0, import_node_fs46.readFileSync)(skillMd, "utf8");
18593
18840
  const { name: fmName, description } = parseFrontmatter(content);
18594
18841
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
18595
18842
  const name = fmName || dirName;
@@ -18608,19 +18855,19 @@ function readSkill(skillMd, source) {
18608
18855
  }
18609
18856
  }
18610
18857
  function scanSkillsDir(dir, source, out) {
18611
- if (!(0, import_node_fs45.existsSync)(dir)) return;
18858
+ if (!(0, import_node_fs46.existsSync)(dir)) return;
18612
18859
  let entries;
18613
18860
  try {
18614
- entries = (0, import_node_fs45.readdirSync)(dir);
18861
+ entries = (0, import_node_fs46.readdirSync)(dir);
18615
18862
  } catch {
18616
18863
  return;
18617
18864
  }
18618
18865
  for (const entry of entries) {
18619
18866
  if (entry.startsWith(".")) continue;
18620
- const skillMd = (0, import_node_path42.join)(dir, entry, "SKILL.md");
18621
- if (!(0, import_node_fs45.existsSync)(skillMd)) continue;
18867
+ const skillMd = (0, import_node_path43.join)(dir, entry, "SKILL.md");
18868
+ if (!(0, import_node_fs46.existsSync)(skillMd)) continue;
18622
18869
  try {
18623
- if (!(0, import_node_fs45.statSync)(skillMd).isFile()) continue;
18870
+ if (!(0, import_node_fs46.statSync)(skillMd).isFile()) continue;
18624
18871
  } catch {
18625
18872
  continue;
18626
18873
  }
@@ -18629,24 +18876,24 @@ function scanSkillsDir(dir, source, out) {
18629
18876
  }
18630
18877
  }
18631
18878
  function scanClaudePluginSkills(pluginsRoot, out) {
18632
- if (!(0, import_node_fs45.existsSync)(pluginsRoot)) return;
18879
+ if (!(0, import_node_fs46.existsSync)(pluginsRoot)) return;
18633
18880
  const walk = (dir, depth, lookingForSkillsDir) => {
18634
18881
  if (depth > 7) return;
18635
18882
  let entries;
18636
18883
  try {
18637
- entries = (0, import_node_fs45.readdirSync)(dir);
18884
+ entries = (0, import_node_fs46.readdirSync)(dir);
18638
18885
  } catch {
18639
18886
  return;
18640
18887
  }
18641
18888
  if (lookingForSkillsDir && entries.includes("SKILL.md")) {
18642
- const skill = readSkill((0, import_node_path42.join)(dir, "SKILL.md"), "cli");
18889
+ const skill = readSkill((0, import_node_path43.join)(dir, "SKILL.md"), "cli");
18643
18890
  if (skill) out.push(skill);
18644
18891
  }
18645
18892
  for (const entry of entries) {
18646
18893
  if (entry === "node_modules" || entry === ".git") continue;
18647
- const full = (0, import_node_path42.join)(dir, entry);
18894
+ const full = (0, import_node_path43.join)(dir, entry);
18648
18895
  try {
18649
- if (!(0, import_node_fs45.statSync)(full).isDirectory()) continue;
18896
+ if (!(0, import_node_fs46.statSync)(full).isDirectory()) continue;
18650
18897
  } catch {
18651
18898
  continue;
18652
18899
  }
@@ -18664,17 +18911,17 @@ function discoverSkills(worktreePath) {
18664
18911
  const home = (0, import_node_os14.homedir)();
18665
18912
  const collected = [];
18666
18913
  for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
18667
- scanSkillsDir((0, import_node_path42.join)(worktreePath, rel), "workspace", collected);
18914
+ scanSkillsDir((0, import_node_path43.join)(worktreePath, rel), "workspace", collected);
18668
18915
  }
18669
18916
  for (const abs of [
18670
- (0, import_node_path42.join)(home, ".claude/skills"),
18671
- (0, import_node_path42.join)(home, ".cursor/skills"),
18672
- (0, import_node_path42.join)(home, ".sideboard/skills"),
18673
- (0, import_node_path42.join)(home, ".brightsy/skills")
18917
+ (0, import_node_path43.join)(home, ".claude/skills"),
18918
+ (0, import_node_path43.join)(home, ".cursor/skills"),
18919
+ (0, import_node_path43.join)(home, ".sideboard/skills"),
18920
+ (0, import_node_path43.join)(home, ".brightsy/skills")
18674
18921
  ]) {
18675
18922
  scanSkillsDir(abs, "user", collected);
18676
18923
  }
18677
- scanClaudePluginSkills((0, import_node_path42.join)(home, ".claude/plugins"), collected);
18924
+ scanClaudePluginSkills((0, import_node_path43.join)(home, ".claude/plugins"), collected);
18678
18925
  collected.push(...bundledSkills());
18679
18926
  const rank = {
18680
18927
  workspace: 0,
@@ -18700,7 +18947,7 @@ function readSkillBody(skillPath, maxChars = 12e3) {
18700
18947
  \u2026(truncated)` : body;
18701
18948
  }
18702
18949
  }
18703
- const raw = (0, import_node_fs45.readFileSync)(skillPath, "utf8");
18950
+ const raw = (0, import_node_fs46.readFileSync)(skillPath, "utf8");
18704
18951
  if (raw.startsWith("---")) {
18705
18952
  const end = raw.indexOf("\n---", 3);
18706
18953
  if (end >= 0) {
@@ -18714,13 +18961,13 @@ function readSkillBody(skillPath, maxChars = 12e3) {
18714
18961
 
18715
18962
  \u2026(truncated)` : raw;
18716
18963
  }
18717
- var import_node_fs45, import_node_os14, import_node_path42, BUNDLED_SKILL_PREFIX;
18964
+ var import_node_fs46, import_node_os14, import_node_path43, BUNDLED_SKILL_PREFIX;
18718
18965
  var init_discover = __esm({
18719
18966
  "src/skills/discover.ts"() {
18720
18967
  "use strict";
18721
- import_node_fs45 = require("fs");
18968
+ import_node_fs46 = require("fs");
18722
18969
  import_node_os14 = require("os");
18723
- import_node_path42 = require("path");
18970
+ import_node_path43 = require("path");
18724
18971
  init_long_running();
18725
18972
  BUNDLED_SKILL_PREFIX = "bundled:";
18726
18973
  }
@@ -18815,17 +19062,17 @@ var init_expand = __esm({
18815
19062
  function packagedDetachedJobPath() {
18816
19063
  const dir = packagedMcpDir();
18817
19064
  if (!dir) return null;
18818
- const script = (0, import_node_path43.join)(dir, "scripts", "detached-job.js");
18819
- return (0, import_node_fs46.existsSync)(script) ? script : null;
19065
+ const script = (0, import_node_path44.join)(dir, "scripts", "detached-job.js");
19066
+ return (0, import_node_fs47.existsSync)(script) ? script : null;
18820
19067
  }
18821
19068
  function resolveDetachedJobScript() {
18822
19069
  const packaged = packagedDetachedJobPath();
18823
19070
  if (packaged) return packaged;
18824
- let dir = (0, import_node_path43.dirname)((0, import_node_url3.fileURLToPath)(import_meta3.url));
19071
+ let dir = (0, import_node_path44.dirname)((0, import_node_url3.fileURLToPath)(import_meta3.url));
18825
19072
  for (let i = 0; i < 8; i++) {
18826
- const candidate = (0, import_node_path43.join)(dir, "scripts", "detached-job.js");
18827
- if ((0, import_node_fs46.existsSync)(candidate)) return candidate;
18828
- const parent = (0, import_node_path43.dirname)(dir);
19073
+ const candidate = (0, import_node_path44.join)(dir, "scripts", "detached-job.js");
19074
+ if ((0, import_node_fs47.existsSync)(candidate)) return candidate;
19075
+ const parent = (0, import_node_path44.dirname)(dir);
18829
19076
  if (parent === dir) break;
18830
19077
  dir = parent;
18831
19078
  }
@@ -18836,12 +19083,12 @@ function formatDetachedJobInvoke(scriptPath) {
18836
19083
  if (resolved) return `node ${JSON.stringify(resolved)}`;
18837
19084
  return "node scripts/detached-job.js";
18838
19085
  }
18839
- var import_node_fs46, import_node_path43, import_node_url3, import_meta3;
19086
+ var import_node_fs47, import_node_path44, import_node_url3, import_meta3;
18840
19087
  var init_detached_job_path = __esm({
18841
19088
  "src/skills/detached-job-path.ts"() {
18842
19089
  "use strict";
18843
- import_node_fs46 = require("fs");
18844
- import_node_path43 = require("path");
19090
+ import_node_fs47 = require("fs");
19091
+ import_node_path44 = require("path");
18845
19092
  import_node_url3 = require("url");
18846
19093
  init_packaged_runtime();
18847
19094
  import_meta3 = {};
@@ -19062,14 +19309,15 @@ function formatLongRunningDirective(opts) {
19062
19309
  `Helper (same tool as \`scripts/detached-job.js\` when that file exists in the worktree): \`${invoke}\``,
19063
19310
  `- Start once: \`${invoke} start <id> -- <command> [args...]\` (cwd = this worktree). If JSON says already-running, do not start again.`,
19064
19311
  "- Immediately `present_artifact` `type=log` with `artifact_id=<id>` and `status=running` \u2014 the side column is the live view.",
19065
- `- Loop \`${invoke} wait <id>\` (returns in ~45s). stillRunning \u2192 present the same id with \`content=delta\` only \u2192 wait again. Do not resend the full log or HTML.`,
19312
+ `- Loop Sideboard MCP \`wait_for_job\` with the same id (returns in ~45s). stillRunning \u2192 present the same id with \`content=delta\` only \u2192 wait_for_job again. Shell fallback: \`${invoke} wait <id>\`. Do not resend the full log or HTML.`,
19313
+ "- Do not end the turn with \u201CI\u2019ll let you know when it\u2019s done.\u201D Stay in the loop until stillRunning is false.",
19066
19314
  "- ok \u2192 finish the task. failed \u2192 read the log, fix, start once.",
19067
19315
  "State: `.context/.sideboard/detached-jobs/<id>/` (local scratch). Full guide: `/long-running` (always available)."
19068
19316
  ].join("\n");
19069
19317
  }
19070
19318
  function formatLongRunningReminder(opts) {
19071
19319
  const invoke = formatDetachedJobInvoke(opts?.scriptPath);
19072
- return `Long jobs: \`${invoke} start <id> -- <cmd>\`, loop wait, present_artifact type=log (delta). Do not ask the human to poll.`;
19320
+ return `Long jobs: \`${invoke} start <id> -- <cmd>\`, loop wait_for_job (or detached-job wait), present_artifact type=log (delta). Do not say you will let the user know later \u2014 stay in the turn.`;
19073
19321
  }
19074
19322
  function formatArtifactDirective() {
19075
19323
  return [
@@ -19106,11 +19354,11 @@ function loadAgentInstructions(worktreePath, agent) {
19106
19354
  const out = [];
19107
19355
  for (const rel of candidates) {
19108
19356
  if (seenPaths.has(rel)) continue;
19109
- const abs = (0, import_node_path44.join)(worktreePath, rel);
19110
- if (!(0, import_node_fs47.existsSync)(abs)) continue;
19357
+ const abs = (0, import_node_path45.join)(worktreePath, rel);
19358
+ if (!(0, import_node_fs48.existsSync)(abs)) continue;
19111
19359
  try {
19112
- if (!(0, import_node_fs47.statSync)(abs).isFile()) continue;
19113
- let content = (0, import_node_fs47.readFileSync)(abs, "utf8");
19360
+ if (!(0, import_node_fs48.statSync)(abs).isFile()) continue;
19361
+ let content = (0, import_node_fs48.readFileSync)(abs, "utf8");
19114
19362
  if (!content.trim()) continue;
19115
19363
  if (content.length > MAX_CHARS_PER_FILE) {
19116
19364
  content = `${content.slice(0, MAX_CHARS_PER_FILE)}
@@ -19150,12 +19398,12 @@ function withAgentInstructions(prompt, files) {
19150
19398
 
19151
19399
  ${prompt}`;
19152
19400
  }
19153
- var import_node_fs47, import_node_path44, GITHUB_TICKET_REF, KEYED_TICKET_REF, FILES_BY_AGENT, MAX_CHARS_PER_FILE;
19401
+ var import_node_fs48, import_node_path45, GITHUB_TICKET_REF, KEYED_TICKET_REF, FILES_BY_AGENT, MAX_CHARS_PER_FILE;
19154
19402
  var init_instructions = __esm({
19155
19403
  "src/agents/instructions.ts"() {
19156
19404
  "use strict";
19157
- import_node_fs47 = require("fs");
19158
- import_node_path44 = require("path");
19405
+ import_node_fs48 = require("fs");
19406
+ import_node_path45 = require("path");
19159
19407
  init_git_auth_mode();
19160
19408
  init_worktree_labels();
19161
19409
  init_detached_job_path();
@@ -19212,12 +19460,12 @@ function firstString2(record, keys) {
19212
19460
  return "";
19213
19461
  }
19214
19462
  async function readJson(res) {
19215
- const text6 = await res.text();
19216
- if (!text6.trim()) return null;
19463
+ const text7 = await res.text();
19464
+ if (!text7.trim()) return null;
19217
19465
  try {
19218
- return JSON.parse(text6);
19466
+ return JSON.parse(text7);
19219
19467
  } catch {
19220
- return text6;
19468
+ return text7;
19221
19469
  }
19222
19470
  }
19223
19471
  async function authorizedGet(url, token) {
@@ -19467,11 +19715,11 @@ function resolvePlanMarkdown(opts) {
19467
19715
  source: "exit_plan"
19468
19716
  };
19469
19717
  }
19470
- const text6 = opts.text?.trim();
19471
- if (text6 && text6.length >= 80) {
19718
+ const text7 = opts.text?.trim();
19719
+ if (text7 && text7.length >= 80) {
19472
19720
  return {
19473
19721
  title: "Plan",
19474
- content: text6,
19722
+ content: text7,
19475
19723
  path: PLAN_FILE_REL,
19476
19724
  source: "text"
19477
19725
  };
@@ -19503,40 +19751,40 @@ __export(plan_file_exports, {
19503
19751
  writePlanFile: () => writePlanFile
19504
19752
  });
19505
19753
  function ensureAttachmentsGitignore(worktreePath) {
19506
- const gitignoreAbs = (0, import_node_path45.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
19507
- if ((0, import_node_fs48.existsSync)(gitignoreAbs)) return;
19508
- (0, import_node_fs48.mkdirSync)((0, import_node_path45.dirname)(gitignoreAbs), { recursive: true });
19509
- (0, import_node_fs48.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
19754
+ const gitignoreAbs = (0, import_node_path46.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
19755
+ if ((0, import_node_fs49.existsSync)(gitignoreAbs)) return;
19756
+ (0, import_node_fs49.mkdirSync)((0, import_node_path46.dirname)(gitignoreAbs), { recursive: true });
19757
+ (0, import_node_fs49.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
19510
19758
  }
19511
19759
  function planFileAbs(worktreePath) {
19512
- return (0, import_node_path45.join)(worktreePath, PLAN_FILE_REL);
19760
+ return (0, import_node_path46.join)(worktreePath, PLAN_FILE_REL);
19513
19761
  }
19514
19762
  function readTextIfPresent2(abs) {
19515
- if (!(0, import_node_fs48.existsSync)(abs)) return null;
19763
+ if (!(0, import_node_fs49.existsSync)(abs)) return null;
19516
19764
  try {
19517
- const content = (0, import_node_fs48.readFileSync)(abs, "utf8");
19765
+ const content = (0, import_node_fs49.readFileSync)(abs, "utf8");
19518
19766
  return content.trim() ? content : null;
19519
19767
  } catch {
19520
19768
  return null;
19521
19769
  }
19522
19770
  }
19523
19771
  function readPlanFile(worktreePath) {
19524
- return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path45.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path45.join)(worktreePath, LEGACY_PLAN_FILE_REL));
19772
+ return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path46.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path46.join)(worktreePath, LEGACY_PLAN_FILE_REL));
19525
19773
  }
19526
19774
  function writePlanFile(worktreePath, content) {
19527
19775
  ensureAttachmentsGitignore(worktreePath);
19528
19776
  const abs = planFileAbs(worktreePath);
19529
- (0, import_node_fs48.mkdirSync)((0, import_node_path45.dirname)(abs), { recursive: true });
19777
+ (0, import_node_fs49.mkdirSync)((0, import_node_path46.dirname)(abs), { recursive: true });
19530
19778
  const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
19531
- (0, import_node_fs48.writeFileSync)(abs, body, "utf8");
19779
+ (0, import_node_fs49.writeFileSync)(abs, body, "utf8");
19532
19780
  return PLAN_FILE_REL;
19533
19781
  }
19534
- var import_node_fs48, import_node_path45;
19782
+ var import_node_fs49, import_node_path46;
19535
19783
  var init_plan_file = __esm({
19536
19784
  "src/plan/plan-file.ts"() {
19537
19785
  "use strict";
19538
- import_node_fs48 = require("fs");
19539
- import_node_path45 = require("path");
19786
+ import_node_fs49 = require("fs");
19787
+ import_node_path46 = require("path");
19540
19788
  init_workspace_scratch();
19541
19789
  init_plan_present();
19542
19790
  init_plan_present();
@@ -19584,13 +19832,13 @@ var init_sync_branch = __esm({
19584
19832
 
19585
19833
  // src/agents/cursor-store.ts
19586
19834
  function cursorSdkStoreDir(threadId) {
19587
- const root = (0, import_node_path46.join)(appDataDir(), CURSOR_SDK_STORE_DIR);
19835
+ const root = (0, import_node_path47.join)(appDataDir(), CURSOR_SDK_STORE_DIR);
19588
19836
  const id = sanitizeCursorStoreSegment(threadId);
19589
19837
  if (!id) return root;
19590
- return (0, import_node_path46.join)(root, "threads", id);
19838
+ return (0, import_node_path47.join)(root, "threads", id);
19591
19839
  }
19592
19840
  function cursorSdkRunsNdjsonPath(threadId) {
19593
- return (0, import_node_path46.join)(cursorSdkStoreDir(threadId), "runs.ndjson");
19841
+ return (0, import_node_path47.join)(cursorSdkStoreDir(threadId), "runs.ndjson");
19594
19842
  }
19595
19843
  function cursorSdkRunsNdjsonSearchPaths(threadId) {
19596
19844
  const scoped = cursorSdkRunsNdjsonPath(threadId);
@@ -19603,11 +19851,11 @@ function cursorSdkRunsNdjsonSearchPaths(threadId) {
19603
19851
  function sanitizeCursorStoreSegment(threadId) {
19604
19852
  return (threadId ?? "").trim().replace(/[^a-zA-Z0-9._-]/g, "_");
19605
19853
  }
19606
- var import_node_path46, CURSOR_SDK_STORE_DIR;
19854
+ var import_node_path47, CURSOR_SDK_STORE_DIR;
19607
19855
  var init_cursor_store = __esm({
19608
19856
  "src/agents/cursor-store.ts"() {
19609
19857
  "use strict";
19610
- import_node_path46 = require("path");
19858
+ import_node_path47 = require("path");
19611
19859
  init_paths();
19612
19860
  CURSOR_SDK_STORE_DIR = "cursor-sdk-store";
19613
19861
  }
@@ -19629,9 +19877,9 @@ function recoverFinishedCursorRun(opts) {
19629
19877
  return best;
19630
19878
  }
19631
19879
  function scanFinishedCursorRuns(runsPath, agentId, startedAfterMs) {
19632
- if (!(0, import_node_fs49.existsSync)(runsPath)) return null;
19880
+ if (!(0, import_node_fs50.existsSync)(runsPath)) return null;
19633
19881
  try {
19634
- const lines = (0, import_node_fs49.readFileSync)(runsPath, "utf8").split("\n");
19882
+ const lines = (0, import_node_fs50.readFileSync)(runsPath, "utf8").split("\n");
19635
19883
  let best = null;
19636
19884
  for (const line of lines) {
19637
19885
  const trimmed = line.trim();
@@ -19657,11 +19905,11 @@ function scanFinishedCursorRuns(runsPath, agentId, startedAfterMs) {
19657
19905
  return null;
19658
19906
  }
19659
19907
  }
19660
- var import_node_fs49;
19908
+ var import_node_fs50;
19661
19909
  var init_cursor_recover = __esm({
19662
19910
  "src/agents/cursor-recover.ts"() {
19663
19911
  "use strict";
19664
- import_node_fs49 = require("fs");
19912
+ import_node_fs50 = require("fs");
19665
19913
  init_cursor_store();
19666
19914
  }
19667
19915
  });
@@ -19793,13 +20041,13 @@ async function startOrchestration(opts) {
19793
20041
  }
19794
20042
  return updated;
19795
20043
  }
19796
- var import_node_events, import_node_fs50, LIVE_TURN_SPAWN_GRACE_MS, STALE_AGENT_PID_WAIT_MS, Orchestrator, singleton;
20044
+ var import_node_events, import_node_fs51, LIVE_TURN_SPAWN_GRACE_MS, STALE_AGENT_PID_WAIT_MS, Orchestrator, singleton;
19797
20045
  var init_orchestrator = __esm({
19798
20046
  "src/orchestrator/orchestrator.ts"() {
19799
20047
  "use strict";
19800
20048
  import_node_events = require("events");
19801
20049
  init_outbound_watch();
19802
- import_node_fs50 = require("fs");
20050
+ import_node_fs51 = require("fs");
19803
20051
  init_error_detail();
19804
20052
  init_run();
19805
20053
  init_stale_lock();
@@ -19820,6 +20068,7 @@ var init_orchestrator = __esm({
19820
20068
  init_thread_store();
19821
20069
  init_desktop_host();
19822
20070
  init_child_halt();
20071
+ init_wait_for_job();
19823
20072
  init_create();
19824
20073
  init_cowboy();
19825
20074
  init_orchestrator_capable();
@@ -19884,6 +20133,9 @@ var init_orchestrator = __esm({
19884
20133
  * finish or a user send so a later crash can recover again.
19885
20134
  */
19886
20135
  crashContinued = /* @__PURE__ */ new Set();
20136
+ /** Auto-continues after a worktree turn ended while a detached job still runs. */
20137
+ jobContinueCount = /* @__PURE__ */ new Map();
20138
+ jobContinueNudged = /* @__PURE__ */ new Set();
19887
20139
  maxConcurrent;
19888
20140
  runningCount = 0;
19889
20141
  constructor(opts) {
@@ -19967,7 +20219,7 @@ var init_orchestrator = __esm({
19967
20219
  }
19968
20220
  continue;
19969
20221
  }
19970
- if (!(0, import_node_fs50.existsSync)(thread.worktreePath)) {
20222
+ if (!(0, import_node_fs51.existsSync)(thread.worktreePath)) {
19971
20223
  setStatus(thread.id, "broken", "Worktree missing on disk");
19972
20224
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
19973
20225
  continue;
@@ -20182,6 +20434,37 @@ var init_orchestrator = __esm({
20182
20434
  this.emit({ type: "queue_changed", threadId, queue });
20183
20435
  this.haltDrain.delete(threadId);
20184
20436
  }
20437
+ /**
20438
+ * Worktree agent ended the turn after “I’ll let you know” (or left a
20439
+ * detached test/pack job running). Queue a continue so the chat does not
20440
+ * go idle with nobody watching the log.
20441
+ */
20442
+ maybeEnqueueJobContinue(threadId, chatText, parts = []) {
20443
+ if (this.haltDrain.has(threadId)) return;
20444
+ const thread = readThread(threadId);
20445
+ if (!thread || thread.status === "archived") return;
20446
+ const runningJobIds = listRunningDetachedJobs(thread.worktreePath ?? "");
20447
+ const decision = planJobContinue({
20448
+ runningJobIds,
20449
+ chatText,
20450
+ queueLength: thread.queue.length,
20451
+ continueCount: this.jobContinueCount.get(threadId) ?? 0,
20452
+ alreadyNudged: this.jobContinueNudged.has(threadId),
20453
+ isOrchestrator: isOrchestratorThread(thread),
20454
+ agent: thread.agent,
20455
+ watchedJob: turnWatchedDetachedJob(parts)
20456
+ });
20457
+ if (decision.action === "none") {
20458
+ if (runningJobIds.length === 0) this.jobContinueCount.delete(threadId);
20459
+ return;
20460
+ }
20461
+ if (decision.action === "nudge") this.jobContinueNudged.add(threadId);
20462
+ else this.jobContinueCount.set(threadId, (this.jobContinueCount.get(threadId) ?? 0) + 1);
20463
+ const queue = [decision.prompt, ...thread.queue];
20464
+ updateThread(threadId, { queue });
20465
+ this.emit({ type: "queue_changed", threadId, queue });
20466
+ this.haltDrain.delete(threadId);
20467
+ }
20185
20468
  getThreads(includeArchived = false) {
20186
20469
  return listThreads({ includeArchived });
20187
20470
  }
@@ -20281,6 +20564,8 @@ var init_orchestrator = __esm({
20281
20564
  }
20282
20565
  const queue = [...current.queue, prompt];
20283
20566
  this.crashContinued.delete(thread.id);
20567
+ this.jobContinueCount.delete(thread.id);
20568
+ this.jobContinueNudged.delete(thread.id);
20284
20569
  this.haltDrain.delete(thread.id);
20285
20570
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id) || current.status === "running";
20286
20571
  shouldSteer = followUp === "steer" && (inFlight || current.queue.length > 0);
@@ -20313,11 +20598,11 @@ var init_orchestrator = __esm({
20313
20598
  return results;
20314
20599
  }
20315
20600
  /** Edit the text of a not-yet-started queued message. */
20316
- async editQueuedMessage(threadRef, index, text6) {
20601
+ async editQueuedMessage(threadRef, index, text7) {
20317
20602
  const thread = this.requireThread(threadRef);
20318
20603
  return withThreadLock(thread.id, async () => {
20319
20604
  const current = this.requireThread(thread.id);
20320
- const trimmed = text6.trim();
20605
+ const trimmed = text7.trim();
20321
20606
  if (!trimmed || index < 0 || index >= current.queue.length) {
20322
20607
  return current;
20323
20608
  }
@@ -20503,6 +20788,7 @@ var init_orchestrator = __esm({
20503
20788
  ticketId: issueTicket?.id,
20504
20789
  ticketProvider: issueTicket?.provider
20505
20790
  }) : null;
20791
+ const viewerContextReminder = thread.agent !== "brightsy" && !isOrchestratorThread(thread) ? formatViewerContextReminder() : null;
20506
20792
  const slackReplyContext = formatSlackRepliesForTurn(
20507
20793
  pendingSlackExternalReplies(thread.messages)
20508
20794
  );
@@ -20512,6 +20798,7 @@ var init_orchestrator = __esm({
20512
20798
  worktreeReminder,
20513
20799
  optionalServicesReminder,
20514
20800
  issueToolsReminder,
20801
+ viewerContextReminder,
20515
20802
  artifactReminder,
20516
20803
  longRunningReminder,
20517
20804
  slackReplyContext,
@@ -20552,6 +20839,18 @@ var init_orchestrator = __esm({
20552
20839
  ticketId: freshIssueTicket?.id,
20553
20840
  ticketProvider: freshIssueTicket?.provider
20554
20841
  });
20842
+ let viewerContextDirective = null;
20843
+ if (!isBrightsy && !isOrchestration) {
20844
+ try {
20845
+ viewerContextDirective = formatViewerContextDirective(
20846
+ resolveViewerProfileForRepo(fresh.repoPath || fresh.worktreePath)
20847
+ );
20848
+ } catch {
20849
+ viewerContextDirective = formatViewerContextDirective(
20850
+ resolveViewerProfileForRepo()
20851
+ );
20852
+ }
20853
+ }
20555
20854
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
20556
20855
  const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled2() ? formatRenameBranchDirective(fresh, {
20557
20856
  customPrompt: settings?.prompts?.renameBranch
@@ -20583,6 +20882,7 @@ var init_orchestrator = __esm({
20583
20882
  worktreeDirective,
20584
20883
  optionalServicesDirective,
20585
20884
  issueToolsDirective,
20885
+ viewerContextDirective,
20586
20886
  artifactDirective,
20587
20887
  longRunningDirective,
20588
20888
  renameBranchDirective,
@@ -20693,6 +20993,7 @@ var init_orchestrator = __esm({
20693
20993
  worktreeDirective,
20694
20994
  optionalServicesDirective,
20695
20995
  issueToolsDirective,
20996
+ viewerContextDirective,
20696
20997
  artifactDirective,
20697
20998
  longRunningDirective,
20698
20999
  renameBranchDirective,
@@ -20795,6 +21096,7 @@ var init_orchestrator = __esm({
20795
21096
  this.emit({ type: "turn_finished", threadId, exitCode });
20796
21097
  if (exitCode === 0) {
20797
21098
  this.crashContinued.delete(threadId);
21099
+ this.maybeEnqueueJobContinue(threadId, chatText, parts);
20798
21100
  } else {
20799
21101
  const blob = [chatText, detail].filter(Boolean).join("\n");
20800
21102
  void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
@@ -21226,13 +21528,13 @@ var init_orchestrator = __esm({
21226
21528
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
21227
21529
  const lastError = thread.lastError ?? null;
21228
21530
  const rawText = (lastAgent?.text ?? "").trim();
21229
- const text6 = (rawText && !isInternalAgentStatusText(rawText) ? rawText : "") || (thread.status === "error" || thread.status === "stopped" || thread.status === "broken" ? lastError ?? "" : "");
21531
+ const text7 = (rawText && !isInternalAgentStatusText(rawText) ? rawText : "") || (thread.status === "error" || thread.status === "stopped" || thread.status === "broken" ? lastError ?? "" : "");
21230
21532
  const stillRunning = this.threadLooksLive(thread);
21231
21533
  const live = stillRunning ? readTurnLive(thread.id) : null;
21232
21534
  const liveSummary = live?.summary && !isInternalAgentStatusText(live.summary) ? live.summary : null;
21233
21535
  const queuedHint = stillRunning && thread.status === "queued" && !liveSummary ? "Queued \u2014 waiting for a concurrency slot" : null;
21234
21536
  return {
21235
- text: text6,
21537
+ text: text7,
21236
21538
  status: thread.status,
21237
21539
  sessionId: thread.sessionId,
21238
21540
  lastError,
@@ -21795,7 +22097,7 @@ var init_orchestrator = __esm({
21795
22097
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
21796
22098
  return restored2;
21797
22099
  }
21798
- if (!(0, import_node_fs50.existsSync)(thread.worktreePath)) {
22100
+ if (!(0, import_node_fs51.existsSync)(thread.worktreePath)) {
21799
22101
  if (isCowboyThread(thread) || isPrimaryCheckoutThread(thread)) {
21800
22102
  throw new Error(
21801
22103
  `Cowboy checkout missing: ${thread.worktreePath}. Re-add the project folder, then restore.`
@@ -21999,10 +22301,6 @@ var init_schedule_runner = __esm({
21999
22301
  var index_exports = {};
22000
22302
  __export(index_exports, {
22001
22303
  ABLETIME_MCP_PATH: () => ABLETIME_MCP_PATH,
22002
- ACCOUNT_ROLES: () => ACCOUNT_ROLES,
22003
- ACCOUNT_ROLE_LABELS: () => ACCOUNT_ROLE_LABELS,
22004
- ACCOUNT_ROLE_MAX: () => ACCOUNT_ROLE_MAX,
22005
- ACCOUNT_ROLE_PRESETS: () => ACCOUNT_ROLE_PRESETS,
22006
22304
  AGENT_GIT_ACTIONS: () => AGENT_GIT_ACTIONS,
22007
22305
  AGENT_RUNNER_MAX_OLD_SPACE_MB: () => AGENT_RUNNER_MAX_OLD_SPACE_MB,
22008
22306
  ATTACHMENTS_DIR: () => ATTACHMENTS_DIR,
@@ -22089,7 +22387,6 @@ __export(index_exports, {
22089
22387
  WORKTREE_MCP_TOOLS: () => WORKTREE_MCP_TOOLS,
22090
22388
  abletimeMcpRequest: () => abletimeMcpRequest,
22091
22389
  abletimeMcpUrl: () => abletimeMcpUrl,
22092
- accountRoleLabel: () => accountRoleLabel,
22093
22390
  ackSlackInboundSeen: () => ackSlackInboundSeen,
22094
22391
  addBoardPin: () => addBoardPin,
22095
22392
  addPrStackLayer: () => addPrStackLayer,
@@ -22270,6 +22567,7 @@ __export(index_exports, {
22270
22567
  fireSchedule: () => fireSchedule,
22271
22568
  flattenTurnInput: () => flattenTurnInput,
22272
22569
  flipLinearRelationType: () => flipLinearRelationType,
22570
+ foldLegacyRolesIntoNotes: () => foldLegacyRolesIntoNotes,
22273
22571
  followUpBehavior: () => followUpBehavior,
22274
22572
  forkChatTab: () => forkChatTab,
22275
22573
  forkMessageSlice: () => forkMessageSlice,
@@ -22310,6 +22608,8 @@ __export(index_exports, {
22310
22608
  formatSlackWorkingText: () => formatSlackWorkingText,
22311
22609
  formatTranscriptMarkdown: () => formatTranscriptMarkdown,
22312
22610
  formatUiReminder: () => formatUiReminder,
22611
+ formatViewerContextDirective: () => formatViewerContextDirective,
22612
+ formatViewerContextReminder: () => formatViewerContextReminder,
22313
22613
  formatWorkspaceInventory: () => formatWorkspaceInventory,
22314
22614
  formatWorkspaceProfileSuffix: () => formatWorkspaceProfileSuffix,
22315
22615
  formatWorktreeDirective: () => formatWorktreeDirective,
@@ -22547,7 +22847,6 @@ __export(index_exports, {
22547
22847
  planQuestionsSignature: () => planQuestionsSignature,
22548
22848
  pollSlackOutboundWatches: () => pollSlackOutboundWatches,
22549
22849
  posixShellSingleQuote: () => posixShellSingleQuote,
22550
- preferTeamsForRole: () => preferTeamsForRole,
22551
22850
  preferredCursorCostCents: () => preferredCursorCostCents,
22552
22851
  prepareTerminalCommand: () => prepareTerminalCommand,
22553
22852
  previewLand: () => previewLand,
@@ -22559,6 +22858,7 @@ __export(index_exports, {
22559
22858
  readSetupLog: () => readSetupLog,
22560
22859
  readSkillBody: () => readSkillBody,
22561
22860
  readThread: () => readThread,
22861
+ readViewerContext: () => readViewerContext,
22562
22862
  readWorktreeFile: () => readWorktreeFile,
22563
22863
  readWorktreeFileForUpload: () => readWorktreeFileForUpload,
22564
22864
  readWorktreeInclude: () => readWorktreeInclude,
@@ -22615,7 +22915,6 @@ __export(index_exports, {
22615
22915
  resolveViewerProfile: () => resolveViewerProfile,
22616
22916
  resolveViewerProfileForRepo: () => resolveViewerProfileForRepo,
22617
22917
  resolveWorktreeStartPoint: () => resolveWorktreeStartPoint,
22618
- reviewTeamHintsForRoles: () => reviewTeamHintsForRoles,
22619
22918
  rewriteAbleTimeError: () => rewriteAbleTimeError,
22620
22919
  rewriteLinearError: () => rewriteLinearError,
22621
22920
  run: () => run,
@@ -22749,6 +23048,7 @@ __export(index_exports, {
22749
23048
  writeInjectedMcpConfig: () => writeInjectedMcpConfig,
22750
23049
  writePlanFile: () => writePlanFile,
22751
23050
  writeThread: () => writeThread,
23051
+ writeViewerContext: () => writeViewerContext,
22752
23052
  writeWorktreeFile: () => writeWorktreeFile
22753
23053
  });
22754
23054
  module.exports = __toCommonJS(index_exports);
@@ -22797,9 +23097,9 @@ async function getGitHubStatus() {
22797
23097
  };
22798
23098
  }
22799
23099
  const status = await run("gh", ["auth", "status"], { reject: false });
22800
- const text6 = `${status.stdout}
23100
+ const text7 = `${status.stdout}
22801
23101
  ${status.stderr}`;
22802
- const loginMatch = text6.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text6.match(/Logged in to ([^\s]+) as (\S+)/i);
23102
+ const loginMatch = text7.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text7.match(/Logged in to ([^\s]+) as (\S+)/i);
22803
23103
  if (loginMatch) {
22804
23104
  return {
22805
23105
  connected: true,
@@ -24165,11 +24465,11 @@ function fenceLanguage(path2, language) {
24165
24465
  }
24166
24466
  function buildCodeRefAttachment(input) {
24167
24467
  const path2 = input.path.trim();
24168
- const text6 = input.text.replace(/\n$/, "");
24468
+ const text7 = input.text.replace(/\n$/, "");
24169
24469
  if (!path2) {
24170
24470
  throw new Error("code reference requires a file path");
24171
24471
  }
24172
- if (!text6.trim()) {
24472
+ if (!text7.trim()) {
24173
24473
  throw new Error("code reference requires selected text");
24174
24474
  }
24175
24475
  if (input.startLine < 1 || input.endLine < 1 || input.endLine < input.startLine) {
@@ -24182,7 +24482,7 @@ function buildCodeRefAttachment(input) {
24182
24482
  `Referenced code from \`${path2}\` (${range}).`,
24183
24483
  "",
24184
24484
  fence,
24185
- text6,
24485
+ text7,
24186
24486
  "```"
24187
24487
  ].join("\n");
24188
24488
  return {
@@ -24239,16 +24539,16 @@ var PASTE_ATTACH_MIN_CHARS = 1200;
24239
24539
  var PASTE_ATTACH_MIN_LINES = 15;
24240
24540
  var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
24241
24541
  var PASTED_NAME_ALT_RE = /^pasted-(\d+)\.txt$/i;
24242
- function pastedTextStats(text6) {
24243
- const chars = text6.length;
24542
+ function pastedTextStats(text7) {
24543
+ const chars = text7.length;
24244
24544
  if (chars === 0) return { chars: 0, lines: 0 };
24245
- const lines = text6.split(/\r\n|\r|\n/).length;
24545
+ const lines = text7.split(/\r\n|\r|\n/).length;
24246
24546
  return { chars, lines };
24247
24547
  }
24248
- function shouldAttachPastedText(text6) {
24249
- const trimmed = text6.trim();
24548
+ function shouldAttachPastedText(text7) {
24549
+ const trimmed = text7.trim();
24250
24550
  if (!trimmed) return false;
24251
- const { chars, lines } = pastedTextStats(text6);
24551
+ const { chars, lines } = pastedTextStats(text7);
24252
24552
  return chars >= PASTE_ATTACH_MIN_CHARS || lines >= PASTE_ATTACH_MIN_LINES;
24253
24553
  }
24254
24554
  function nextPastedTextName(existing) {
@@ -24259,13 +24559,13 @@ function nextPastedTextName(existing) {
24259
24559
  }
24260
24560
  return `Pasted text #${max + 1}.txt`;
24261
24561
  }
24262
- function buildPastedTextAttachment(text6, opts) {
24562
+ function buildPastedTextAttachment(text7, opts) {
24263
24563
  return {
24264
24564
  id: opts?.id ?? (0, import_node_crypto11.randomUUID)(),
24265
24565
  name: opts?.name ?? "Pasted text #1.txt",
24266
24566
  kind: "file",
24267
24567
  path: opts?.path,
24268
- content: text6
24568
+ content: text7
24269
24569
  };
24270
24570
  }
24271
24571
 
@@ -24365,8 +24665,8 @@ function latestPendingPlanQuestions(input) {
24365
24665
  return null;
24366
24666
  }
24367
24667
  var PLAN_QUESTION_ANSWERS_PREFIX = "Answers to your questions:";
24368
- function isPlanQuestionAnswersMessage(text6) {
24369
- return text6.startsWith(PLAN_QUESTION_ANSWERS_PREFIX);
24668
+ function isPlanQuestionAnswersMessage(text7) {
24669
+ return text7.startsWith(PLAN_QUESTION_ANSWERS_PREFIX);
24370
24670
  }
24371
24671
  function formatPlanQuestionAnswers(questions, answers) {
24372
24672
  const lines = [PLAN_QUESTION_ANSWERS_PREFIX, ""];
@@ -24413,9 +24713,9 @@ init_orphan_cleanup();
24413
24713
  // src/mcp/server.ts
24414
24714
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
24415
24715
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
24416
- var import_zod6 = require("zod");
24716
+ var import_zod7 = require("zod");
24417
24717
  var import_node_crypto13 = require("crypto");
24418
- var import_node_path49 = require("path");
24718
+ var import_node_path50 = require("path");
24419
24719
  init_orchestrator();
24420
24720
  init_worktree();
24421
24721
  init_global_workspace();
@@ -24432,30 +24732,8 @@ function mcpArchiveBlockedReason(thread) {
24432
24732
 
24433
24733
  // src/mcp/server.ts
24434
24734
  init_profile();
24435
-
24436
- // src/mcp/wait-for-turn.ts
24437
- var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
24438
- function mcpWaitForTurnTimeoutMs(requested) {
24439
- const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
24440
- if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
24441
- return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
24442
- }
24443
- var MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send a check-in prompt or assume a hang while progress is updating.";
24444
- var MCP_WAIT_QUEUED_HINT = "Child is queued waiting for a concurrency slot \u2014 it has not started yet. Call wait_for_turn again. Do not send a check-in prompt, force_stop, or assume it failed to start.";
24445
- function mcpWaitStillRunningHint(status) {
24446
- return status === "queued" ? MCP_WAIT_QUEUED_HINT : MCP_WAIT_STILL_RUNNING_HINT;
24447
- }
24448
- var MCP_WAIT_STOPPED_HINT = "Child was stopped before the turn finished. Do not treat this as success. send_to_thread to resume, or tell the user.";
24449
- var MCP_WAIT_BROKEN_HINT = "Child worktree is broken (missing on disk). Tell the user \u2014 do not treat this as success.";
24450
- var MCP_WAIT_ERROR_HINT = "Child turn failed. lastError/text is the failure \u2014 switch agent, tell the user, or retry. Do not treat empty text as success.";
24451
- function mcpWaitFinishedHint(status) {
24452
- if (status === "stopped") return MCP_WAIT_STOPPED_HINT;
24453
- if (status === "broken") return MCP_WAIT_BROKEN_HINT;
24454
- if (status === "error") return MCP_WAIT_ERROR_HINT;
24455
- return void 0;
24456
- }
24457
-
24458
- // src/mcp/server.ts
24735
+ init_wait_for_turn();
24736
+ init_wait_for_job();
24459
24737
  init_message_parts();
24460
24738
  init_turn_live();
24461
24739
 
@@ -24464,9 +24742,9 @@ init_message_parts();
24464
24742
  function lastMessagePreview(messages, max = 160) {
24465
24743
  if (!messages?.length) return null;
24466
24744
  for (let i = messages.length - 1; i >= 0; i--) {
24467
- const text6 = messages[i]?.text?.trim();
24468
- if (!text6 || isInternalAgentStatusText(text6)) continue;
24469
- const flat = text6.replace(/\s+/g, " ");
24745
+ const text7 = messages[i]?.text?.trim();
24746
+ if (!text7 || isInternalAgentStatusText(text7)) continue;
24747
+ const flat = text7.replace(/\s+/g, " ");
24470
24748
  return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
24471
24749
  }
24472
24750
  return null;
@@ -24700,8 +24978,8 @@ function labelGithubUrl(url) {
24700
24978
  }
24701
24979
  return { kind: "other", label: "GitHub", url: raw };
24702
24980
  }
24703
- function appendGithubLink(text6, githubUrl) {
24704
- const body = text6.trimEnd();
24981
+ function appendGithubLink(text7, githubUrl) {
24982
+ const body = text7.trimEnd();
24705
24983
  if (!githubUrl?.trim()) return body;
24706
24984
  const labeled = labelGithubUrl(githubUrl);
24707
24985
  if (!labeled) {
@@ -25397,6 +25675,104 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
25397
25675
  if (isAbleTimeConnected(settings)) registerAbleTimeTools(server);
25398
25676
  }
25399
25677
 
25678
+ // src/mcp/viewer-context-tools.ts
25679
+ var import_zod5 = require("zod");
25680
+ init_worktree();
25681
+ init_app_settings();
25682
+ init_workspaces();
25683
+ function text5(payload, isError = false) {
25684
+ return mcpJson(payload, isError);
25685
+ }
25686
+ function matchRegisteredWorkspace(repoPath) {
25687
+ const hint = (repoPath ?? "").trim().replace(/\/+$/, "");
25688
+ if (!hint) return null;
25689
+ const match = listWorkspaces().find((w) => {
25690
+ const stored = w.path.replace(/\/+$/, "");
25691
+ return stored === hint || hint.startsWith(`${stored}/`) || stored.startsWith(`${hint}/`);
25692
+ });
25693
+ return match?.path.replace(/\/+$/, "") ?? null;
25694
+ }
25695
+ async function resolveProjectRepoPath(repoPath) {
25696
+ const hint = repoPath?.trim() || process.cwd();
25697
+ let root = hint;
25698
+ try {
25699
+ root = await resolveRepoRoot(hint);
25700
+ } catch {
25701
+ root = hint.replace(/\/+$/, "");
25702
+ }
25703
+ return matchRegisteredWorkspace(root) ?? matchRegisteredWorkspace(hint);
25704
+ }
25705
+ var PROJECT_PATH_HINT = "Pass repoPath from list_workspaces. Orchestration cwd is synthetic and empty \u2014 do not omit repoPath for project context.";
25706
+ function registerViewerContextTools(server) {
25707
+ server.tool(
25708
+ "get_viewer_context",
25709
+ "Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_workspaces for project context. Worktree turns may omit repoPath (uses cwd).",
25710
+ {
25711
+ repoPath: import_zod5.z.string().optional().describe(
25712
+ "Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
25713
+ )
25714
+ },
25715
+ async ({ repoPath }) => {
25716
+ try {
25717
+ const path2 = await resolveProjectRepoPath(repoPath);
25718
+ return text5(readViewerContext(path2 ?? void 0));
25719
+ } catch (err) {
25720
+ return text5(
25721
+ { error: err instanceof Error ? err.message : String(err) },
25722
+ true
25723
+ );
25724
+ }
25725
+ }
25726
+ );
25727
+ server.tool(
25728
+ "update_viewer_context",
25729
+ "Replace account or project user context (Settings \u2192 Agents / Projects). Never call until the user confirmed. Show the proposed text, ask_user (Save this context / Do not save), wait, then pass confirmed=true. Empty context clears that field.",
25730
+ {
25731
+ scope: import_zod5.z.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
25732
+ context: import_zod5.z.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
25733
+ confirmed: import_zod5.z.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
25734
+ repoPath: import_zod5.z.string().optional().describe(
25735
+ "Required for scope=project from orchestration (path from list_workspaces). Worktree turns may omit it (cwd)."
25736
+ )
25737
+ },
25738
+ async ({ scope, context, confirmed, repoPath }) => {
25739
+ try {
25740
+ if (scope === "project") {
25741
+ const path2 = await resolveProjectRepoPath(repoPath);
25742
+ if (!path2) {
25743
+ return text5(
25744
+ {
25745
+ ok: false,
25746
+ error: "repoPath required",
25747
+ message: PROJECT_PATH_HINT
25748
+ },
25749
+ true
25750
+ );
25751
+ }
25752
+ const result2 = writeViewerContext({
25753
+ scope,
25754
+ context,
25755
+ repoPath: path2,
25756
+ confirmed: confirmed === true
25757
+ });
25758
+ return text5(result2, !result2.ok);
25759
+ }
25760
+ const result = writeViewerContext({
25761
+ scope,
25762
+ context,
25763
+ confirmed: confirmed === true
25764
+ });
25765
+ return text5(result, !result.ok);
25766
+ } catch (err) {
25767
+ return text5(
25768
+ { error: err instanceof Error ? err.message : String(err) },
25769
+ true
25770
+ );
25771
+ }
25772
+ }
25773
+ );
25774
+ }
25775
+
25400
25776
  // src/mcp/pr-list.ts
25401
25777
  function compactPrRow(pr) {
25402
25778
  return {
@@ -25436,17 +25812,17 @@ init_list_prs();
25436
25812
  init_app_settings();
25437
25813
 
25438
25814
  // src/mcp/schedule-tools.ts
25439
- var import_zod5 = require("zod");
25815
+ var import_zod6 = require("zod");
25440
25816
  init_schedules();
25441
25817
  init_schedule_runner();
25442
- function text5(payload, isError = false) {
25818
+ function text6(payload, isError = false) {
25443
25819
  return {
25444
25820
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
25445
25821
  ...isError ? { isError: true } : {}
25446
25822
  };
25447
25823
  }
25448
25824
  function fail5(err) {
25449
- return text5(
25825
+ return text6(
25450
25826
  { error: err instanceof Error ? err.message : String(err) },
25451
25827
  true
25452
25828
  );
@@ -25471,7 +25847,7 @@ function registerScheduleTools(server) {
25471
25847
  {},
25472
25848
  async () => {
25473
25849
  try {
25474
- return text5({ schedules: listSchedules() });
25850
+ return text6({ schedules: listSchedules() });
25475
25851
  } catch (err) {
25476
25852
  return fail5(err);
25477
25853
  }
@@ -25481,17 +25857,17 @@ function registerScheduleTools(server) {
25481
25857
  "create_schedule",
25482
25858
  "Create a local schedule that, when due, sends a prompt to an orchestration chat (threadId) or starts a new Global orchestration chat (omit threadId). Pass threadId=self to continue this coordinator. Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without threadId open a new chat each run. Overnight/unattended runs need Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or set_caffeinate. Sideboard.app must be running for the job to fire.",
25483
25859
  {
25484
- prompt: import_zod5.z.string().describe("User message / goal queued when the schedule fires"),
25485
- name: import_zod5.z.string().optional(),
25486
- at: import_zod5.z.string().optional().describe("ISO datetime for a one-shot"),
25487
- every: import_zod5.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
25488
- cron: import_zod5.z.string().optional().describe("5-field cron expression"),
25489
- tz: import_zod5.z.string().optional().describe("IANA timezone for cron (default: system)"),
25490
- threadId: import_zod5.z.string().optional().describe(
25860
+ prompt: import_zod6.z.string().describe("User message / goal queued when the schedule fires"),
25861
+ name: import_zod6.z.string().optional(),
25862
+ at: import_zod6.z.string().optional().describe("ISO datetime for a one-shot"),
25863
+ every: import_zod6.z.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
25864
+ cron: import_zod6.z.string().optional().describe("5-field cron expression"),
25865
+ tz: import_zod6.z.string().optional().describe("IANA timezone for cron (default: system)"),
25866
+ threadId: import_zod6.z.string().optional().describe(
25491
25867
  'Existing orchestration chat id, or "self" for this coordinator. Omit to create a new Global chat on fire.'
25492
25868
  ),
25493
- agent: import_zod5.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
25494
- model: import_zod5.z.string().optional()
25869
+ agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
25870
+ model: import_zod6.z.string().optional()
25495
25871
  },
25496
25872
  async (args) => {
25497
25873
  try {
@@ -25511,7 +25887,7 @@ function registerScheduleTools(server) {
25511
25887
  model: args.model,
25512
25888
  createdBy: "mcp"
25513
25889
  });
25514
- return text5({
25890
+ return text6({
25515
25891
  schedule,
25516
25892
  hint: "Fires while Sideboard.app is running. Recurring jobs without threadId create a new orchestration chat each run. Overnight runs need Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled, or set_caffeinate."
25517
25893
  });
@@ -25524,17 +25900,17 @@ function registerScheduleTools(server) {
25524
25900
  "update_schedule",
25525
25901
  "Update a local schedule (prompt, cadence, target thread, enabled). Pass id from list_schedules.",
25526
25902
  {
25527
- id: import_zod5.z.string(),
25528
- prompt: import_zod5.z.string().optional(),
25529
- name: import_zod5.z.string().optional(),
25530
- at: import_zod5.z.string().optional(),
25531
- every: import_zod5.z.string().optional(),
25532
- cron: import_zod5.z.string().optional(),
25533
- tz: import_zod5.z.string().optional(),
25534
- threadId: import_zod5.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
25535
- agent: import_zod5.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
25536
- model: import_zod5.z.string().optional(),
25537
- enabled: import_zod5.z.boolean().optional()
25903
+ id: import_zod6.z.string(),
25904
+ prompt: import_zod6.z.string().optional(),
25905
+ name: import_zod6.z.string().optional(),
25906
+ at: import_zod6.z.string().optional(),
25907
+ every: import_zod6.z.string().optional(),
25908
+ cron: import_zod6.z.string().optional(),
25909
+ tz: import_zod6.z.string().optional(),
25910
+ threadId: import_zod6.z.string().optional().describe('Existing orchestration chat, "self", or empty string to create a new chat each run'),
25911
+ agent: import_zod6.z.enum(["claude", "cursor", "codex", "opencode"]).optional(),
25912
+ model: import_zod6.z.string().optional(),
25913
+ enabled: import_zod6.z.boolean().optional()
25538
25914
  },
25539
25915
  async (args) => {
25540
25916
  try {
@@ -25563,7 +25939,7 @@ function registerScheduleTools(server) {
25563
25939
  enabled: args.enabled,
25564
25940
  model: args.model
25565
25941
  });
25566
- return text5({ schedule });
25942
+ return text6({ schedule });
25567
25943
  } catch (err) {
25568
25944
  return fail5(err);
25569
25945
  }
@@ -25572,11 +25948,11 @@ function registerScheduleTools(server) {
25572
25948
  server.tool(
25573
25949
  "delete_schedule",
25574
25950
  "Delete a local schedule. Pass id from list_schedules.",
25575
- { id: import_zod5.z.string() },
25951
+ { id: import_zod6.z.string() },
25576
25952
  async ({ id }) => {
25577
25953
  try {
25578
25954
  deleteSchedule(id);
25579
- return text5({ ok: true, id });
25955
+ return text6({ ok: true, id });
25580
25956
  } catch (err) {
25581
25957
  return fail5(err);
25582
25958
  }
@@ -25585,14 +25961,14 @@ function registerScheduleTools(server) {
25585
25961
  server.tool(
25586
25962
  "run_schedule",
25587
25963
  "Fire a schedule now (does not wait for nextRunAt). Queues the prompt or starts a new Global chat. If Sideboard.app is running, the desktop drains the turn.",
25588
- { id: import_zod5.z.string() },
25964
+ { id: import_zod6.z.string() },
25589
25965
  async ({ id }) => {
25590
25966
  try {
25591
25967
  if (!getSchedule(id)) {
25592
25968
  return fail5(new Error(`Schedule not found: ${id}`));
25593
25969
  }
25594
25970
  const schedule = await fireSchedule(id);
25595
- return text5({ schedule });
25971
+ return text6({ schedule });
25596
25972
  } catch (err) {
25597
25973
  return fail5(err);
25598
25974
  }
@@ -25606,27 +25982,27 @@ init_gh_errors();
25606
25982
  init_git_auth_mode();
25607
25983
 
25608
25984
  // src/board/load-home-board.ts
25609
- var import_node_fs52 = require("fs");
25610
- var import_node_path48 = require("path");
25985
+ var import_node_fs53 = require("fs");
25986
+ var import_node_path49 = require("path");
25611
25987
  init_worktree();
25612
25988
  init_paths();
25613
25989
 
25614
25990
  // src/board/board-pins.ts
25615
25991
  var import_node_crypto12 = require("crypto");
25616
- var import_node_fs51 = require("fs");
25617
- var import_node_path47 = require("path");
25992
+ var import_node_fs52 = require("fs");
25993
+ var import_node_path48 = require("path");
25618
25994
  init_paths();
25619
25995
  init_home_board();
25620
25996
  var FILE = "home-board-pins.json";
25621
25997
  var VERSION = 1;
25622
25998
  function pinsFile() {
25623
- return (0, import_node_path47.join)(appDataDir(), FILE);
25999
+ return (0, import_node_path48.join)(appDataDir(), FILE);
25624
26000
  }
25625
26001
  function readDisk() {
25626
26002
  const path2 = pinsFile();
25627
- if (!(0, import_node_fs51.existsSync)(path2)) return [];
26003
+ if (!(0, import_node_fs52.existsSync)(path2)) return [];
25628
26004
  try {
25629
- const raw = JSON.parse((0, import_node_fs51.readFileSync)(path2, "utf8"));
26005
+ const raw = JSON.parse((0, import_node_fs52.readFileSync)(path2, "utf8"));
25630
26006
  if (raw?.version !== VERSION || !Array.isArray(raw.items)) return [];
25631
26007
  return raw.items.filter((item) => item?.id && item.kind && item.ref);
25632
26008
  } catch {
@@ -25634,8 +26010,8 @@ function readDisk() {
25634
26010
  }
25635
26011
  }
25636
26012
  function writeDisk(items) {
25637
- (0, import_node_fs51.mkdirSync)(appDataDir(), { recursive: true });
25638
- (0, import_node_fs51.writeFileSync)(pinsFile(), JSON.stringify({ version: VERSION, items }, null, 2), "utf8");
26013
+ (0, import_node_fs52.mkdirSync)(appDataDir(), { recursive: true });
26014
+ (0, import_node_fs52.writeFileSync)(pinsFile(), JSON.stringify({ version: VERSION, items }, null, 2), "utf8");
25639
26015
  }
25640
26016
  function listBoardPins() {
25641
26017
  return readDisk();
@@ -25687,7 +26063,7 @@ function replaceBoardPins(items) {
25687
26063
  }
25688
26064
  function clearBoardPins() {
25689
26065
  try {
25690
- (0, import_node_fs51.unlinkSync)(pinsFile());
26066
+ (0, import_node_fs52.unlinkSync)(pinsFile());
25691
26067
  } catch {
25692
26068
  }
25693
26069
  }
@@ -25704,7 +26080,7 @@ function homeBoardWorkspaceKey(workspaces) {
25704
26080
  return workspaces.map((w) => w.path).filter(Boolean).sort().join("\n");
25705
26081
  }
25706
26082
  function cacheFile() {
25707
- return (0, import_node_path48.join)(appDataDir(), "home-board-cache.json");
26083
+ return (0, import_node_path49.join)(appDataDir(), "home-board-cache.json");
25708
26084
  }
25709
26085
  function emptyInputs() {
25710
26086
  return {
@@ -25730,9 +26106,9 @@ function shouldCacheHomeBoardInputs(inputs) {
25730
26106
  }
25731
26107
  function readDiskCache() {
25732
26108
  const path2 = cacheFile();
25733
- if (!(0, import_node_fs52.existsSync)(path2)) return null;
26109
+ if (!(0, import_node_fs53.existsSync)(path2)) return null;
25734
26110
  try {
25735
- const raw = JSON.parse((0, import_node_fs52.readFileSync)(path2, "utf8"));
26111
+ const raw = JSON.parse((0, import_node_fs53.readFileSync)(path2, "utf8"));
25736
26112
  if (raw?.version !== CACHE_VERSION || typeof raw.fetchedAt !== "number") {
25737
26113
  return null;
25738
26114
  }
@@ -25746,14 +26122,14 @@ function readDiskCache() {
25746
26122
  }
25747
26123
  function writeDiskCache(entry) {
25748
26124
  try {
25749
- (0, import_node_fs52.mkdirSync)(appDataDir(), { recursive: true });
25750
- (0, import_node_fs52.writeFileSync)(cacheFile(), JSON.stringify(entry), "utf8");
26125
+ (0, import_node_fs53.mkdirSync)(appDataDir(), { recursive: true });
26126
+ (0, import_node_fs53.writeFileSync)(cacheFile(), JSON.stringify(entry), "utf8");
25751
26127
  } catch {
25752
26128
  }
25753
26129
  }
25754
26130
  function deleteDiskCache() {
25755
26131
  try {
25756
- (0, import_node_fs52.unlinkSync)(cacheFile());
26132
+ (0, import_node_fs53.unlinkSync)(cacheFile());
25757
26133
  } catch {
25758
26134
  }
25759
26135
  }
@@ -26023,13 +26399,14 @@ async function startMcpServer() {
26023
26399
  version: "0.1.0"
26024
26400
  });
26025
26401
  const worktreeProfile = sideboardMcpProfile() === "worktree";
26402
+ registerViewerContextTools(server);
26026
26403
  if (worktreeProfile) {
26027
26404
  registerConnectedIssueVendorTools(server);
26028
26405
  }
26029
26406
  if (!worktreeProfile) {
26030
26407
  server.tool(
26031
26408
  "list_workspaces",
26032
- "List registered Sideboard workspaces (repos). Each line is name, path, github:owner/repo when resolvable, and viewer roles/notes when set \u2014 use path as repoPath for list_board/list_branches/list_prs/list_issues/create_thread. Roles and notes (Settings \u2192 Agents / Projects) say which tickets and review PRs belong to the user.",
26409
+ "List registered Sideboard workspaces (repos). Each line is name, path, github:owner/repo when resolvable, and project context when set \u2014 use path as repoPath for list_board/list_branches/list_prs/list_issues/create_thread. Account / project context (Settings \u2192 Agents / Projects) says which tickets and review PRs belong to the user.",
26033
26410
  {},
26034
26411
  async () => {
26035
26412
  const workspaces = orch.listWorkspaces();
@@ -26060,7 +26437,7 @@ async function startMcpServer() {
26060
26437
  async () => {
26061
26438
  const threads = orch.getThreads(true);
26062
26439
  const lines = threads.map((t) => {
26063
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path49.basename)(t.repoPath) || t.repoPath;
26440
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path50.basename)(t.repoPath) || t.repoPath;
26064
26441
  const live = orch.threadLooksLive(t) ? readTurnLive(t.id) : null;
26065
26442
  const parent = t.parentThreadId ? ` parent:${t.parentThreadId.slice(0, 8)}` : "";
26066
26443
  const preview = lastMessagePreview(t.messages, 80);
@@ -26078,13 +26455,13 @@ async function startMcpServer() {
26078
26455
  "list_board",
26079
26456
  "Home Kanban of worktrees (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chat tabs nest as inner cards. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), column, limit (default 40). create_thread adds a worktree (and a Home card), or returns the live one if that ticket/PR/named branch is already checked out.",
26080
26457
  {
26081
- query: import_zod6.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
26082
- repoPath: import_zod6.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
26083
- kind: import_zod6.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
26084
- column: import_zod6.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
26458
+ query: import_zod7.z.string().optional().describe("Case-insensitive token search across title, id, labels, repo"),
26459
+ repoPath: import_zod7.z.string().optional().describe("Limit to one workspace path from list_workspaces"),
26460
+ kind: import_zod7.z.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
26461
+ column: import_zod7.z.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
26085
26462
  "Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
26086
26463
  ),
26087
- limit: import_zod6.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
26464
+ limit: import_zod7.z.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
26088
26465
  },
26089
26466
  async ({ query, repoPath, kind, column, limit }) => {
26090
26467
  const workspaces = orch.listWorkspaces();
@@ -26122,7 +26499,7 @@ async function startMcpServer() {
26122
26499
  server.tool(
26123
26500
  "get_thread",
26124
26501
  "Get a compact thread summary by id/ref. Includes last message preview, parentThreadId, and child worktree threads (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (thread billed token + costUsd totals when providers reported cost) and lastTurnUsage.",
26125
- { ref: import_zod6.z.string() },
26502
+ { ref: import_zod7.z.string() },
26126
26503
  async ({ ref }) => {
26127
26504
  const t = orch.getThread(ref);
26128
26505
  if (!t) {
@@ -26164,17 +26541,17 @@ async function startMcpServer() {
26164
26541
  "present_artifact",
26165
26542
  "Show a document or live log in Sideboard\u2019s side column. For html/svg/markdown/react, pass the FULL document and do not also fence that same body in chat. For type=log, pass only NEW lines (same artifact_id appends). Prefer type=log for long-running job output \u2014 do not resend HTML. type=react is a single default-export component (JSX/TSX); only react/react-dom imports.",
26166
26543
  {
26167
- title: import_zod6.z.string().describe("Short title shown in the artifact pane header"),
26168
- type: import_zod6.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
26544
+ title: import_zod7.z.string().describe("Short title shown in the artifact pane header"),
26545
+ type: import_zod7.z.enum(["html", "svg", "markdown", "react", "log"]).describe(
26169
26546
  "html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
26170
26547
  ),
26171
- content: import_zod6.z.string().describe(
26548
+ content: import_zod7.z.string().describe(
26172
26549
  "html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
26173
26550
  ),
26174
- artifact_id: import_zod6.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
26175
- status: import_zod6.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
26176
- phase: import_zod6.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
26177
- mode: import_zod6.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
26551
+ artifact_id: import_zod7.z.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
26552
+ status: import_zod7.z.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
26553
+ phase: import_zod7.z.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
26554
+ mode: import_zod7.z.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
26178
26555
  },
26179
26556
  async ({ title, type, artifact_id, status, phase, mode }) => {
26180
26557
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26195,15 +26572,15 @@ async function startMcpServer() {
26195
26572
  "ask_user",
26196
26573
  "Ask the user a clarifying multiple-choice question in Sideboard\u2019s composer. Call only when work is blocked on choosing among a few concrete options (approach fork, which API, auth vs cookies). Do not call for greetings, check-ins, \u201Chello\u201D, open-ended how-can-I-help, or to invent a menu of possible next tasks \u2014 reply in chat instead. If one option is the obvious default, proceed without asking. Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. After calling, stop and wait for answers. Not for \u201Cis the plan ready?\u201D.",
26197
26574
  {
26198
- questions: import_zod6.z.array(
26199
- import_zod6.z.object({
26200
- question: import_zod6.z.string().describe("Full question text ending with ?"),
26201
- header: import_zod6.z.string().max(24).optional().describe("Short label shown above the question"),
26202
- multiSelect: import_zod6.z.boolean().optional().describe("Allow selecting multiple options"),
26203
- options: import_zod6.z.array(
26204
- import_zod6.z.object({
26205
- label: import_zod6.z.string(),
26206
- description: import_zod6.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
26575
+ questions: import_zod7.z.array(
26576
+ import_zod7.z.object({
26577
+ question: import_zod7.z.string().describe("Full question text ending with ?"),
26578
+ header: import_zod7.z.string().max(24).optional().describe("Short label shown above the question"),
26579
+ multiSelect: import_zod7.z.boolean().optional().describe("Allow selecting multiple options"),
26580
+ options: import_zod7.z.array(
26581
+ import_zod7.z.object({
26582
+ label: import_zod7.z.string(),
26583
+ description: import_zod7.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
26207
26584
  })
26208
26585
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
26209
26586
  })
@@ -26222,9 +26599,9 @@ async function startMcpServer() {
26222
26599
  "present_plan",
26223
26600
  "Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
26224
26601
  {
26225
- title: import_zod6.z.string().optional().describe("Short plan title (defaults to Plan)"),
26226
- content: import_zod6.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
26227
- thread_id: import_zod6.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
26602
+ title: import_zod7.z.string().optional().describe("Short plan title (defaults to Plan)"),
26603
+ content: import_zod7.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
26604
+ thread_id: import_zod7.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
26228
26605
  },
26229
26606
  async ({ title, content, thread_id }) => {
26230
26607
  const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
@@ -26247,15 +26624,15 @@ async function startMcpServer() {
26247
26624
  "present_schema",
26248
26625
  "Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
26249
26626
  {
26250
- title: import_zod6.z.string().describe("Pane title"),
26251
- mode: import_zod6.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
26252
- datasource: import_zod6.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
26253
- resource_id: import_zod6.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
26254
- record_id: import_zod6.z.string().optional().describe("Record id when opening form mode"),
26255
- resource: import_zod6.z.record(import_zod6.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
26256
- record: import_zod6.z.record(import_zod6.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
26257
- records: import_zod6.z.array(import_zod6.z.record(import_zod6.z.unknown())).optional().describe("Inline records for table mode"),
26258
- pane_id: import_zod6.z.string().optional().describe("Stable pane id across updates")
26627
+ title: import_zod7.z.string().describe("Pane title"),
26628
+ mode: import_zod7.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
26629
+ datasource: import_zod7.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
26630
+ resource_id: import_zod7.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
26631
+ record_id: import_zod7.z.string().optional().describe("Record id when opening form mode"),
26632
+ resource: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
26633
+ record: import_zod7.z.record(import_zod7.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
26634
+ records: import_zod7.z.array(import_zod7.z.record(import_zod7.z.unknown())).optional().describe("Inline records for table mode"),
26635
+ pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
26259
26636
  },
26260
26637
  async (args) => {
26261
26638
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26276,10 +26653,10 @@ async function startMcpServer() {
26276
26653
  "present_files",
26277
26654
  "Open Sideboard\u2019s Files column (CMS-style file manager: browse, upload, pick). Use datasource=brightsy when the user is logged into Brightsy account storage; datasource=memory for a session-local demo store. Prefer this over claiming a file manager UI is unavailable. Pair with present_schema when editing records that need media.",
26278
26655
  {
26279
- title: import_zod6.z.string().optional().describe("Pane title (default: Files)"),
26280
- datasource: import_zod6.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
26281
- path: import_zod6.z.string().optional().describe("Initial folder path (e.g. public)"),
26282
- pane_id: import_zod6.z.string().optional().describe("Stable pane id across updates")
26656
+ title: import_zod7.z.string().optional().describe("Pane title (default: Files)"),
26657
+ datasource: import_zod7.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
26658
+ path: import_zod7.z.string().optional().describe("Initial folder path (e.g. public)"),
26659
+ pane_id: import_zod7.z.string().optional().describe("Stable pane id across updates")
26283
26660
  },
26284
26661
  async (args) => {
26285
26662
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -26294,6 +26671,20 @@ async function startMcpServer() {
26294
26671
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
26295
26672
  }
26296
26673
  );
26674
+ server.tool(
26675
+ "wait_for_job",
26676
+ "Wait on a detached long job (tests, pack, deploy) started with detached-job.js. MCP clients kill tools around 60s, so this returns within 45s. stillRunning is the source of truth \u2014 if true, present_artifact type=log with content=delta (same artifact_id) and call wait_for_job again. Do not end the turn or tell the user you will let them know later. If false, ok/failed is the result.",
26677
+ {
26678
+ id: import_zod7.z.string().describe("Detached job id (same kebab-case id passed to detached-job.js start)"),
26679
+ timeoutMs: import_zod7.z.number().optional()
26680
+ },
26681
+ async ({ id, timeoutMs }) => {
26682
+ const result = await waitForDetachedJob(process.cwd(), id, {
26683
+ timeoutMs: mcpWaitForJobTimeoutMs(timeoutMs)
26684
+ });
26685
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
26686
+ }
26687
+ );
26297
26688
  if (!worktreeProfile) {
26298
26689
  registerSlackTools(server);
26299
26690
  registerConnectedIssueVendorTools(server);
@@ -26306,7 +26697,7 @@ async function startMcpServer() {
26306
26697
  "set_caffeinate",
26307
26698
  "Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. Closing or archiving this orchestration chat also releases the hold. macOS only.",
26308
26699
  {
26309
- enabled: import_zod6.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
26700
+ enabled: import_zod7.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
26310
26701
  },
26311
26702
  async ({ enabled }) => {
26312
26703
  const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
@@ -26344,18 +26735,18 @@ async function startMcpServer() {
26344
26735
  "create_thread",
26345
26736
  `Create a worktree thread (chat) from branch, pr, or ticket. A ticket, PR, or named branch may have only one live worktree \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. Creating from the default branch still opens a new isolated worktree. Pass repoPath from list_workspaces. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Then use send_to_thread to chat.`,
26346
26737
  {
26347
- sourceType: import_zod6.z.enum(["branch", "pr", "ticket"]),
26348
- sourceRef: import_zod6.z.string(),
26349
- agent: import_zod6.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
26350
- model: import_zod6.z.string().nullable().optional().describe(
26738
+ sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]),
26739
+ sourceRef: import_zod7.z.string(),
26740
+ agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
26741
+ model: import_zod7.z.string().nullable().optional().describe(
26351
26742
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
26352
26743
  ),
26353
- repoPath: import_zod6.z.string(),
26354
- title: import_zod6.z.string().optional(),
26355
- cowboy: import_zod6.z.boolean().optional().describe(
26744
+ repoPath: import_zod7.z.string(),
26745
+ title: import_zod7.z.string().optional(),
26746
+ cowboy: import_zod7.z.boolean().optional().describe(
26356
26747
  "If true, work in the project folder on the default branch (no thread/* worktree). Requires Settings \u2192 Advanced \u2192 Cowboy mode. Land is commit+push to that branch. Archive does not delete the folder."
26357
26748
  ),
26358
- parentThreadId: import_zod6.z.string().optional().describe(
26749
+ parentThreadId: import_zod7.z.string().optional().describe(
26359
26750
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
26360
26751
  )
26361
26752
  },
@@ -26371,10 +26762,10 @@ async function startMcpServer() {
26371
26762
  "start_board_card",
26372
26763
  "Same as create_thread for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second worktree when one already matches \u2014 returns that thread (alreadyStarted). Then send_to_thread.",
26373
26764
  {
26374
- kind: import_zod6.z.enum(["ticket", "pr", "branch"]),
26375
- ref: import_zod6.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
26376
- repoPath: import_zod6.z.string().describe("Workspace path from list_workspaces / list_board"),
26377
- title: import_zod6.z.string().optional()
26765
+ kind: import_zod7.z.enum(["ticket", "pr", "branch"]),
26766
+ ref: import_zod7.z.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
26767
+ repoPath: import_zod7.z.string().describe("Workspace path from list_workspaces / list_board"),
26768
+ title: import_zod7.z.string().optional()
26378
26769
  },
26379
26770
  async ({ kind, ref, repoPath, title }) => {
26380
26771
  const root = await resolveRepoRoot(repoPath);
@@ -26478,9 +26869,9 @@ async function startMcpServer() {
26478
26869
  "send_to_thread",
26479
26870
  'Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not force_stop to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that stops the child mid-thought. Call wait_for_turn again instead.',
26480
26871
  {
26481
- ref: import_zod6.z.string(),
26482
- prompt: import_zod6.z.string(),
26483
- force_stop: import_zod6.z.boolean().optional()
26872
+ ref: import_zod7.z.string(),
26873
+ prompt: import_zod7.z.string(),
26874
+ force_stop: import_zod7.z.boolean().optional()
26484
26875
  },
26485
26876
  async ({ ref, prompt, force_stop }) => {
26486
26877
  if (force_stop) {
@@ -26509,8 +26900,8 @@ async function startMcpServer() {
26509
26900
  "wait_for_turn",
26510
26901
  "Wait until the thread finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. stillRunning is the source of truth \u2014 if false, the child is not working (do not say it is waiting for a gate). If stillRunning is true, progress is tools/thinking (or \u201Cqueued, waiting for a concurrency slot\u201D if it has not started). Call wait_for_turn again. Do not send a check-in prompt, force_stop, or assume a hang. On status error, lastError/text is the failure. On status stopped or broken, the child did not finish \u2014 resume with send_to_thread or tell the user; do not treat that as success. When finished, usage is the last agent turn\u2019s tokens + costUsd (when the provider reported cost).",
26511
26902
  {
26512
- ref: import_zod6.z.string(),
26513
- timeoutMs: import_zod6.z.number().optional()
26903
+ ref: import_zod7.z.string(),
26904
+ timeoutMs: import_zod7.z.number().optional()
26514
26905
  },
26515
26906
  async ({ ref, timeoutMs }) => {
26516
26907
  const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
@@ -26540,7 +26931,7 @@ async function startMcpServer() {
26540
26931
  server.tool(
26541
26932
  "get_turn_result",
26542
26933
  "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript. Includes usage for the last agent turn (tokens + costUsd when reported).",
26543
- { ref: import_zod6.z.string() },
26934
+ { ref: import_zod7.z.string() },
26544
26935
  async ({ ref }) => {
26545
26936
  const result = orch.getTurnResult(ref);
26546
26937
  return {
@@ -26561,8 +26952,8 @@ async function startMcpServer() {
26561
26952
  "stop_thread",
26562
26953
  "Force-stop a thread: kill any in-flight agent turn AND clear queued prompts so drainQueue cannot continue. Does not archive the worktree. Optional force defaults to true.",
26563
26954
  {
26564
- ref: import_zod6.z.string(),
26565
- force: import_zod6.z.boolean().optional()
26955
+ ref: import_zod7.z.string(),
26956
+ force: import_zod7.z.boolean().optional()
26566
26957
  },
26567
26958
  async ({ ref, force }) => {
26568
26959
  const t = orch.getThread(ref);
@@ -26592,7 +26983,7 @@ async function startMcpServer() {
26592
26983
  server.tool(
26593
26984
  "archive_thread",
26594
26985
  "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, and open PRs by asking the worktree agent (ask_git). Merge only when the user explicitly asked.",
26595
- { ref: import_zod6.z.string() },
26986
+ { ref: import_zod7.z.string() },
26596
26987
  async ({ ref }) => {
26597
26988
  const t = orch.getThread(ref);
26598
26989
  if (!t) {
@@ -26625,7 +27016,7 @@ async function startMcpServer() {
26625
27016
  server.tool(
26626
27017
  "restore_thread",
26627
27018
  "Restore an archived thread (recreates worktree from branch when needed)",
26628
- { ref: import_zod6.z.string() },
27019
+ { ref: import_zod7.z.string() },
26629
27020
  async ({ ref }) => {
26630
27021
  try {
26631
27022
  const restored = await orch.restore(ref);
@@ -26651,8 +27042,8 @@ async function startMcpServer() {
26651
27042
  "get_diff",
26652
27043
  "Compact diff summary (capped hunks, paginated)",
26653
27044
  {
26654
- ref: import_zod6.z.string(),
26655
- maxFiles: import_zod6.z.number().optional()
27045
+ ref: import_zod7.z.string(),
27046
+ maxFiles: import_zod7.z.number().optional()
26656
27047
  },
26657
27048
  async ({ ref, maxFiles }) => {
26658
27049
  const summary = await orch.diffSummary(ref);
@@ -26672,7 +27063,7 @@ async function startMcpServer() {
26672
27063
  server.tool(
26673
27064
  "get_pr_checks",
26674
27065
  "Snapshot of GitHub PR checks for a worktree thread (`gh pr checks` plus merge/review gates). null = no PR. If the user gave a goal (Greptile 5/5, CI green), the worktree agent watches with `gh pr checks --watch` via /long-running \u2014 this tool is a snapshot, not a waiter. Coordinators: do not run gh from the orchestration cwd.",
26675
- { ref: import_zod6.z.string().describe("Worktree thread id/ref") },
27066
+ { ref: import_zod7.z.string().describe("Worktree thread id/ref") },
26676
27067
  async ({ ref }) => {
26677
27068
  try {
26678
27069
  const checks = await orch.getPrChecks(ref);
@@ -26688,7 +27079,7 @@ async function startMcpServer() {
26688
27079
  server.tool(
26689
27080
  "request_review",
26690
27081
  'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .claude/skills/review/SKILL.md when present (else copies .sideboard/review.md into .context/review.md, or seeds that file from the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
26691
- { ref: import_zod6.z.string().describe("Worktree thread id/ref to review") },
27082
+ { ref: import_zod7.z.string().describe("Worktree thread id/ref to review") },
26692
27083
  async ({ ref }) => {
26693
27084
  try {
26694
27085
  const tab = await orch.requestReview(ref);
@@ -26717,8 +27108,8 @@ async function startMcpServer() {
26717
27108
  "ask_git",
26718
27109
  "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` even when origin is SSH / Settings \u2192 Git is SSH). When dirty, queues the worktree agent to commit; then wait_for_turn (loop while stillRunning). Do not start a checks loop on a plain push \u2014 only if the user gave a goal (Greptile 5/5, CI green). Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd. If this tool errors that the GraphQL/PR body is too long, the branch is already pushed \u2014 have the worktree agent retry `gh pr create --body-file` with a short description (GitHub limit 65,536 characters). Do not invent SSH/auth failures from that error.",
26719
27110
  {
26720
- ref: import_zod6.z.string().describe("Worktree thread id/ref"),
26721
- action: import_zod6.z.enum(AGENT_GIT_ACTIONS).describe(
27111
+ ref: import_zod7.z.string().describe("Worktree thread id/ref"),
27112
+ action: import_zod7.z.enum(AGENT_GIT_ACTIONS).describe(
26722
27113
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
26723
27114
  )
26724
27115
  },
@@ -26763,7 +27154,7 @@ async function startMcpServer() {
26763
27154
  }
26764
27155
  }
26765
27156
  );
26766
- const agentEnum = import_zod6.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
27157
+ const agentEnum = import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
26767
27158
  server.tool(
26768
27159
  "list_models",
26769
27160
  "List models for an agent. Prefer Auto: do not call this unless you have a reason to pin a specific model (user request, cost/latency, capability). Omit agent to list all.",
@@ -26786,11 +27177,11 @@ async function startMcpServer() {
26786
27177
  "fork_worktree",
26787
27178
  "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id.",
26788
27179
  {
26789
- ref: import_zod6.z.string().describe("Worktree thread id/ref to fork"),
26790
- through_index: import_zod6.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27180
+ ref: import_zod7.z.string().describe("Worktree thread id/ref to fork"),
27181
+ through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
26791
27182
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
26792
- model: import_zod6.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
26793
- title: import_zod6.z.string().optional()
27183
+ model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27184
+ title: import_zod7.z.string().optional()
26794
27185
  },
26795
27186
  async ({ ref, through_index, agent, model, title }) => {
26796
27187
  try {
@@ -26831,11 +27222,11 @@ async function startMcpServer() {
26831
27222
  "fork_chat",
26832
27223
  "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
26833
27224
  {
26834
- ref: import_zod6.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
26835
- through_index: import_zod6.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
27225
+ ref: import_zod7.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
27226
+ through_index: import_zod7.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
26836
27227
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
26837
- model: import_zod6.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
26838
- title: import_zod6.z.string().optional()
27228
+ model: import_zod7.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
27229
+ title: import_zod7.z.string().optional()
26839
27230
  },
26840
27231
  async ({ ref, through_index, agent, model, title }) => {
26841
27232
  try {
@@ -26881,8 +27272,8 @@ async function startMcpServer() {
26881
27272
  "run_dev_script",
26882
27273
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
26883
27274
  {
26884
- ref: import_zod6.z.string(),
26885
- name: import_zod6.z.string().optional()
27275
+ ref: import_zod7.z.string(),
27276
+ name: import_zod7.z.string().optional()
26886
27277
  },
26887
27278
  async ({ ref, name }) => {
26888
27279
  const result = await orch.startDev(ref, name);
@@ -26904,7 +27295,7 @@ async function startMcpServer() {
26904
27295
  server.tool(
26905
27296
  "list_run_scripts",
26906
27297
  "List named run scripts available for a thread",
26907
- { ref: import_zod6.z.string() },
27298
+ { ref: import_zod7.z.string() },
26908
27299
  async ({ ref }) => {
26909
27300
  const scripts = orch.listThreadRunScripts(ref);
26910
27301
  const active = orch.getActiveRuns(ref);
@@ -26922,8 +27313,8 @@ async function startMcpServer() {
26922
27313
  "stop_dev_script",
26923
27314
  "Stop a running script for a thread (all scripts if name omitted)",
26924
27315
  {
26925
- ref: import_zod6.z.string(),
26926
- name: import_zod6.z.string().optional()
27316
+ ref: import_zod7.z.string(),
27317
+ name: import_zod7.z.string().optional()
26927
27318
  },
26928
27319
  async ({ ref, name }) => {
26929
27320
  orch.stopDev(ref, name);
@@ -26933,7 +27324,7 @@ async function startMcpServer() {
26933
27324
  server.tool(
26934
27325
  "run_setup",
26935
27326
  "Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
26936
- { ref: import_zod6.z.string() },
27327
+ { ref: import_zod7.z.string() },
26937
27328
  async ({ ref }) => {
26938
27329
  const result = await orch.runSetup(ref);
26939
27330
  return {
@@ -26944,7 +27335,7 @@ async function startMcpServer() {
26944
27335
  server.tool(
26945
27336
  "add_workspace",
26946
27337
  "Register a git repo as a Sideboard workspace",
26947
- { repoPath: import_zod6.z.string() },
27338
+ { repoPath: import_zod7.z.string() },
26948
27339
  async ({ repoPath }) => {
26949
27340
  const ws = await orch.addWorkspace(repoPath);
26950
27341
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -26953,7 +27344,7 @@ async function startMcpServer() {
26953
27344
  server.tool(
26954
27345
  "remove_workspace",
26955
27346
  "Unregister a Sideboard workspace (does not archive threads)",
26956
- { repoPath: import_zod6.z.string() },
27347
+ { repoPath: import_zod7.z.string() },
26957
27348
  async ({ repoPath }) => {
26958
27349
  orch.removeWorkspace(repoPath);
26959
27350
  return { content: [{ type: "text", text: "ok" }] };
@@ -26963,12 +27354,12 @@ async function startMcpServer() {
26963
27354
  "fanout",
26964
27355
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
26965
27356
  {
26966
- prompt: import_zod6.z.string(),
26967
- agents: import_zod6.z.array(import_zod6.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
26968
- repoPath: import_zod6.z.string(),
26969
- sourceType: import_zod6.z.enum(["branch", "pr", "ticket"]).optional(),
26970
- sourceRef: import_zod6.z.string().optional(),
26971
- title: import_zod6.z.string().optional()
27357
+ prompt: import_zod7.z.string(),
27358
+ agents: import_zod7.z.array(import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
27359
+ repoPath: import_zod7.z.string(),
27360
+ sourceType: import_zod7.z.enum(["branch", "pr", "ticket"]).optional(),
27361
+ sourceRef: import_zod7.z.string().optional(),
27362
+ title: import_zod7.z.string().optional()
26972
27363
  },
26973
27364
  async (args) => {
26974
27365
  const threads = await orch.bestOfN(args);
@@ -26995,8 +27386,8 @@ async function startMcpServer() {
26995
27386
  "list_branches",
26996
27387
  "List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
26997
27388
  {
26998
- repoPath: import_zod6.z.string(),
26999
- unmergedOnly: import_zod6.z.boolean().optional()
27389
+ repoPath: import_zod7.z.string(),
27390
+ unmergedOnly: import_zod7.z.boolean().optional()
27000
27391
  },
27001
27392
  async ({ repoPath, unmergedOnly }) => {
27002
27393
  const root = await resolveRepoRoot(repoPath);
@@ -27017,19 +27408,19 @@ async function startMcpServer() {
27017
27408
  "list_prs",
27018
27409
  'List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_workspaces. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled eng-review with no individual user reviewer. Prefer teams that match Settings \u2192 Agents / Projects roles for this repo. A team like engineering-team is not a claim (you are on that team). Also: state (open|closed|merged|all|review), label, reviewer (me|unassigned|login), query, limit (default 40, max 250). Then create_thread with sourceType=pr.',
27019
27410
  {
27020
- repoPath: import_zod6.z.string(),
27021
- query: import_zod6.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
27022
- queue: import_zod6.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
27411
+ repoPath: import_zod7.z.string(),
27412
+ query: import_zod7.z.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
27413
+ queue: import_zod7.z.enum(["review", "mine", "approved", "changes"]).optional().describe(
27023
27414
  'review = unclaimed eng-review inbox (use for "get me N tickets to review"). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes.'
27024
27415
  ),
27025
- state: import_zod6.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
27026
- label: import_zod6.z.string().optional().describe(
27416
+ state: import_zod7.z.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
27417
+ label: import_zod7.z.string().optional().describe(
27027
27418
  "GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
27028
27419
  ),
27029
- reviewer: import_zod6.z.string().optional().describe(
27420
+ reviewer: import_zod7.z.string().optional().describe(
27030
27421
  "me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
27031
27422
  ),
27032
- limit: import_zod6.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27423
+ limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27033
27424
  },
27034
27425
  async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
27035
27426
  const root = await resolveRepoRoot(repoPath);
@@ -27061,7 +27452,7 @@ async function startMcpServer() {
27061
27452
  server.tool(
27062
27453
  "get_pr_stack",
27063
27454
  "Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before ask_git merge on stacked PRs (and only merge when the user explicitly asked).",
27064
- { ref: import_zod6.z.string() },
27455
+ { ref: import_zod7.z.string() },
27065
27456
  async ({ ref }) => {
27066
27457
  const stack = await orch.getPrStack(ref);
27067
27458
  return {
@@ -27073,8 +27464,8 @@ async function startMcpServer() {
27073
27464
  "open_pr_stack_layers",
27074
27465
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
27075
27466
  {
27076
- ref: import_zod6.z.string(),
27077
- layer: import_zod6.z.number().int().positive().optional()
27467
+ ref: import_zod7.z.string(),
27468
+ layer: import_zod7.z.number().int().positive().optional()
27078
27469
  },
27079
27470
  async ({ ref, layer }) => {
27080
27471
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -27108,9 +27499,9 @@ async function startMcpServer() {
27108
27499
  "add_stack_layer",
27109
27500
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
27110
27501
  {
27111
- ref: import_zod6.z.string(),
27112
- branchName: import_zod6.z.string(),
27113
- title: import_zod6.z.string().optional()
27502
+ ref: import_zod7.z.string(),
27503
+ branchName: import_zod7.z.string(),
27504
+ title: import_zod7.z.string().optional()
27114
27505
  },
27115
27506
  async ({ ref, branchName, title }) => {
27116
27507
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -27139,11 +27530,11 @@ async function startMcpServer() {
27139
27530
  "create_pr_stack",
27140
27531
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
27141
27532
  {
27142
- repoPath: import_zod6.z.string(),
27143
- branches: import_zod6.z.array(import_zod6.z.string()).min(1),
27144
- agent: import_zod6.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
27145
- base: import_zod6.z.string().optional(),
27146
- title: import_zod6.z.string().optional()
27533
+ repoPath: import_zod7.z.string(),
27534
+ branches: import_zod7.z.array(import_zod7.z.string()).min(1),
27535
+ agent: import_zod7.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
27536
+ base: import_zod7.z.string().optional(),
27537
+ title: import_zod7.z.string().optional()
27147
27538
  },
27148
27539
  async (args) => {
27149
27540
  const result = await orch.createPrStack({
@@ -27182,10 +27573,10 @@ async function startMcpServer() {
27182
27573
  "list_issues",
27183
27574
  "List or search issues (Linear, AbleTime, or GitHub; falls back to GitHub). Use Settings \u2192 Agents / Projects notes and roles to pick tickets relevant to the viewer (query + assignee=me or unassigned as the notes say). Default 40; pass query and/or limit (max 250) when truncated. assignee: me (Linear default), unassigned, all, or a user id. Then create_thread with sourceType=ticket.",
27184
27575
  {
27185
- repoPath: import_zod6.z.string(),
27186
- query: import_zod6.z.string().optional().describe("Search title, identifier, or description"),
27187
- assignee: import_zod6.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
27188
- limit: import_zod6.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27576
+ repoPath: import_zod7.z.string(),
27577
+ query: import_zod7.z.string().optional().describe("Search title, identifier, or description"),
27578
+ assignee: import_zod7.z.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
27579
+ limit: import_zod7.z.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
27189
27580
  },
27190
27581
  async ({ repoPath, query, assignee, limit }) => {
27191
27582
  const root = await resolveRepoRoot(repoPath);
@@ -27325,8 +27716,8 @@ var BrightsySideboardApi = class {
27325
27716
  };
27326
27717
  function taskMessageText(task) {
27327
27718
  const parts = task.message?.parts ?? [];
27328
- const text6 = parts.find((p) => p.kind === "text")?.text ?? "";
27329
- return text6.trim();
27719
+ const text7 = parts.find((p) => p.kind === "text")?.text ?? "";
27720
+ return text7.trim();
27330
27721
  }
27331
27722
 
27332
27723
  // src/brightsy/cloud-connect.ts
@@ -27828,11 +28219,11 @@ var import_ws = require("ws");
27828
28219
  init_api();
27829
28220
  var BACKOFF_START_MS = 1e3;
27830
28221
  var BACKOFF_MAX_MS = 3e4;
27831
- function stripSlackMentions(text6) {
27832
- return text6.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
28222
+ function stripSlackMentions(text7) {
28223
+ return text7.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
27833
28224
  }
27834
- function isSlackStopCommand(text6) {
27835
- const t = stripSlackMentions(text6).toLowerCase();
28225
+ function isSlackStopCommand(text7) {
28226
+ const t = stripSlackMentions(text7).toLowerCase();
27836
28227
  return t === "stop" || t === "sideboard_force_stop";
27837
28228
  }
27838
28229
  function parseSlackSocketFrame(raw) {
@@ -27854,8 +28245,8 @@ function inboundFromSocketFrame(frame) {
27854
28245
  const ts = event.ts?.trim();
27855
28246
  if (!channelId || !ts) return null;
27856
28247
  const rawText = event.text?.trim() ?? "";
27857
- const text6 = stripSlackMentions(rawText);
27858
- if (!text6) return null;
28248
+ const text7 = stripSlackMentions(rawText);
28249
+ if (!text7) return null;
27859
28250
  const teamId = (frame.payload?.team_id || event.team || "").trim();
27860
28251
  if (!teamId) return null;
27861
28252
  const isDm = event.type === "message" && (event.channel_type === "im" || event.channel_type === "mpim" || !event.channel_type && channelId.startsWith("D"));
@@ -27867,7 +28258,7 @@ function inboundFromSocketFrame(frame) {
27867
28258
  ts,
27868
28259
  threadTs: event.thread_ts?.trim() || void 0,
27869
28260
  userId: event.user?.trim(),
27870
- text: text6,
28261
+ text: text7,
27871
28262
  kind: isMention ? "mention" : "dm"
27872
28263
  };
27873
28264
  }
@@ -28411,12 +28802,12 @@ function formatSlackInboundPrompt(msg) {
28411
28802
 
28412
28803
  ${msg.text}`;
28413
28804
  }
28414
- function isSlackInboundUserPrompt(text6) {
28415
- return text6.startsWith("Slack DM\n") || text6.startsWith("Slack @mention\n");
28805
+ function isSlackInboundUserPrompt(text7) {
28806
+ return text7.startsWith("Slack DM\n") || text7.startsWith("Slack @mention\n");
28416
28807
  }
28417
- function formatSlackSignedReply(deviceLabel, text6) {
28808
+ function formatSlackSignedReply(deviceLabel, text7) {
28418
28809
  const label = deviceLabel.trim();
28419
- const body = text6.trim();
28810
+ const body = text7.trim();
28420
28811
  if (!body) return body;
28421
28812
  if (!label) return body;
28422
28813
  const head = `${label}:`;
@@ -28425,8 +28816,8 @@ function formatSlackSignedReply(deviceLabel, text6) {
28425
28816
  }
28426
28817
  return `${label}: ${body}`;
28427
28818
  }
28428
- function signForThisMac(text6) {
28429
- return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text6);
28819
+ function signForThisMac(text7) {
28820
+ return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text7);
28430
28821
  }
28431
28822
  function slackReplyThreadTs(msg) {
28432
28823
  if (msg.threadTs && msg.threadTs !== msg.ts) return msg.threadTs;
@@ -28487,9 +28878,9 @@ function replyStub(target) {
28487
28878
  kind: "dm"
28488
28879
  };
28489
28880
  }
28490
- async function postSlackText(target, text6, opts) {
28881
+ async function postSlackText(target, text7, opts) {
28491
28882
  if (opts.postReply) {
28492
- const result = await opts.postReply(replyStub(target), text6);
28883
+ const result = await opts.postReply(replyStub(target), text7);
28493
28884
  const ts2 = result && typeof result === "object" && typeof result.ts === "string" ? result.ts.trim() : "";
28494
28885
  return ts2 ? { ts: ts2 } : null;
28495
28886
  }
@@ -28499,7 +28890,7 @@ async function postSlackText(target, text6, opts) {
28499
28890
  "chat.postMessage",
28500
28891
  {
28501
28892
  channel: target.channelId,
28502
- text: text6,
28893
+ text: text7,
28503
28894
  thread_ts: target.threadTs
28504
28895
  },
28505
28896
  opts.fetchImpl
@@ -28507,9 +28898,9 @@ async function postSlackText(target, text6, opts) {
28507
28898
  const ts = json.ts?.trim();
28508
28899
  return ts ? { ts } : null;
28509
28900
  }
28510
- async function updateSlackText(target, ts, text6, opts) {
28901
+ async function updateSlackText(target, ts, text7, opts) {
28511
28902
  if (opts.updateReply) {
28512
- await opts.updateReply(replyStub(target), ts, text6);
28903
+ await opts.updateReply(replyStub(target), ts, text7);
28513
28904
  return;
28514
28905
  }
28515
28906
  const token = writeTokenForTeam(target.teamId);
@@ -28519,7 +28910,7 @@ async function updateSlackText(target, ts, text6, opts) {
28519
28910
  {
28520
28911
  channel: target.channelId,
28521
28912
  ts,
28522
- text: text6
28913
+ text: text7
28523
28914
  },
28524
28915
  opts.fetchImpl
28525
28916
  );
@@ -28547,8 +28938,8 @@ function replyTargetOf(msg) {
28547
28938
  threadTs: slackReplyThreadTs(msg)
28548
28939
  };
28549
28940
  }
28550
- async function postSlackReply(msg, text6, opts) {
28551
- await postSlackText(replyTargetOf(msg), signForThisMac(text6), opts);
28941
+ async function postSlackReply(msg, text7, opts) {
28942
+ await postSlackText(replyTargetOf(msg), signForThisMac(text7), opts);
28552
28943
  }
28553
28944
  function startSlackTurnProgress(msg, threadId, opts) {
28554
28945
  const log = opts.onLog ?? (() => void 0);
@@ -28631,10 +29022,10 @@ function startSlackTurnProgress(msg, threadId, opts) {
28631
29022
  }
28632
29023
  });
28633
29024
  },
28634
- finishWith: (text6) => {
29025
+ finishWith: (text7) => {
28635
29026
  stop();
28636
29027
  return run2(async () => {
28637
- const signed = signForThisMac(text6.trim());
29028
+ const signed = signForThisMac(text7.trim());
28638
29029
  if (!signed) {
28639
29030
  if (!postedTs) return;
28640
29031
  const ts = postedTs;
@@ -28662,11 +29053,11 @@ function startSlackTurnProgress(msg, threadId, opts) {
28662
29053
  };
28663
29054
  }
28664
29055
  var lastRelayed = /* @__PURE__ */ new Map();
28665
- function markRelayed(threadId, text6) {
28666
- lastRelayed.set(threadId, `${threadId}:${text6}`);
29056
+ function markRelayed(threadId, text7) {
29057
+ lastRelayed.set(threadId, `${threadId}:${text7}`);
28667
29058
  }
28668
- function alreadyRelayed(threadId, text6) {
28669
- return lastRelayed.get(threadId) === `${threadId}:${text6}`;
29059
+ function alreadyRelayed(threadId, text7) {
29060
+ return lastRelayed.get(threadId) === `${threadId}:${text7}`;
28670
29061
  }
28671
29062
  async function relayCoordinatorReplyToSlack(threadId, opts) {
28672
29063
  const target = getSlackReplyTarget(threadId);
@@ -28675,14 +29066,14 @@ async function relayCoordinatorReplyToSlack(threadId, opts) {
28675
29066
  const lastUser = thread ? [...thread.messages].reverse().find((m) => m.role === "user") : void 0;
28676
29067
  if (lastUser && isSlackInboundUserPrompt(lastUser.text)) return;
28677
29068
  const result = getOrchestrator().getTurnResult(threadId);
28678
- const text6 = result.text.trim();
28679
- if (!text6) return;
28680
- if (alreadyRelayed(threadId, text6)) return;
28681
- markRelayed(threadId, text6);
29069
+ const text7 = result.text.trim();
29070
+ if (!text7) return;
29071
+ if (alreadyRelayed(threadId, text7)) return;
29072
+ markRelayed(threadId, text7);
28682
29073
  const log = opts.onLog ?? (() => void 0);
28683
29074
  try {
28684
- await postSlackText(target, signForThisMac(text6), opts);
28685
- log(`replied ${target.threadTs ?? target.channelId} (${text6.length} chars)`);
29075
+ await postSlackText(target, signForThisMac(text7), opts);
29076
+ log(`replied ${target.threadTs ?? target.channelId} (${text7.length} chars)`);
28686
29077
  } catch (err) {
28687
29078
  lastRelayed.delete(threadId);
28688
29079
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -28931,10 +29322,10 @@ function resolveSlackListenMode(opts) {
28931
29322
 
28932
29323
  // src/slack/relay-hub.ts
28933
29324
  init_api();
28934
- function parseSlackDeviceDestination(text6) {
28935
- const m = text6.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
28936
- if (!m) return { label: null, rest: text6 };
28937
- return { label: m[1], rest: text6.slice(m[0].length) };
29325
+ function parseSlackDeviceDestination(text7) {
29326
+ const m = text7.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
29327
+ if (!m) return { label: null, rest: text7 };
29328
+ return { label: m[1], rest: text7.slice(m[0].length) };
28938
29329
  }
28939
29330
  var SlackRelayHub = class {
28940
29331
  sessions = /* @__PURE__ */ new Map();
@@ -29156,9 +29547,9 @@ var import_node_http3 = require("http");
29156
29547
  var import_ws3 = require("ws");
29157
29548
 
29158
29549
  // src/slack/relay-static.ts
29159
- var import_node_fs53 = require("fs");
29550
+ var import_node_fs54 = require("fs");
29160
29551
  var import_promises = require("fs/promises");
29161
- var import_node_path50 = __toESM(require("path"), 1);
29552
+ var import_node_path51 = __toESM(require("path"), 1);
29162
29553
  var TYPES = {
29163
29554
  ".css": "text/css; charset=utf-8",
29164
29555
  ".html": "text/html; charset=utf-8",
@@ -29189,9 +29580,9 @@ function resolveStaticPath(root, requestUrl) {
29189
29580
  return null;
29190
29581
  }
29191
29582
  if (!pathname.startsWith("/") || pathname.includes("\0")) return null;
29192
- const rootResolved = import_node_path50.default.resolve(root);
29193
- const candidate = import_node_path50.default.resolve(rootResolved, `.${pathname}`);
29194
- if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path50.default.sep)) {
29583
+ const rootResolved = import_node_path51.default.resolve(root);
29584
+ const candidate = import_node_path51.default.resolve(rootResolved, `.${pathname}`);
29585
+ if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path51.default.sep)) {
29195
29586
  return null;
29196
29587
  }
29197
29588
  return candidate;
@@ -29205,7 +29596,7 @@ async function fileSize(file) {
29205
29596
  }
29206
29597
  }
29207
29598
  function sendFile(req, res, file, size) {
29208
- const ext = import_node_path50.default.extname(file).toLowerCase();
29599
+ const ext = import_node_path51.default.extname(file).toLowerCase();
29209
29600
  res.writeHead(200, {
29210
29601
  "Content-Type": TYPES[ext] ?? "application/octet-stream",
29211
29602
  "Content-Length": size,
@@ -29215,7 +29606,7 @@ function sendFile(req, res, file, size) {
29215
29606
  res.end();
29216
29607
  return true;
29217
29608
  }
29218
- (0, import_node_fs53.createReadStream)(file).pipe(res);
29609
+ (0, import_node_fs54.createReadStream)(file).pipe(res);
29219
29610
  return true;
29220
29611
  }
29221
29612
  async function tryServeStatic(req, res, root) {
@@ -29224,7 +29615,7 @@ async function tryServeStatic(req, res, root) {
29224
29615
  if (!candidate) return false;
29225
29616
  const direct = await fileSize(candidate);
29226
29617
  if (direct != null) return sendFile(req, res, candidate, direct);
29227
- const asIndex = import_node_path50.default.join(candidate, "index.html");
29618
+ const asIndex = import_node_path51.default.join(candidate, "index.html");
29228
29619
  const indexSize = await fileSize(asIndex);
29229
29620
  if (indexSize != null) return sendFile(req, res, asIndex, indexSize);
29230
29621
  return false;
@@ -29460,10 +29851,6 @@ init_outbound_watch();
29460
29851
  // Annotate the CommonJS export names for ESM import in node:
29461
29852
  0 && (module.exports = {
29462
29853
  ABLETIME_MCP_PATH,
29463
- ACCOUNT_ROLES,
29464
- ACCOUNT_ROLE_LABELS,
29465
- ACCOUNT_ROLE_MAX,
29466
- ACCOUNT_ROLE_PRESETS,
29467
29854
  AGENT_GIT_ACTIONS,
29468
29855
  AGENT_RUNNER_MAX_OLD_SPACE_MB,
29469
29856
  ATTACHMENTS_DIR,
@@ -29550,7 +29937,6 @@ init_outbound_watch();
29550
29937
  WORKTREE_MCP_TOOLS,
29551
29938
  abletimeMcpRequest,
29552
29939
  abletimeMcpUrl,
29553
- accountRoleLabel,
29554
29940
  ackSlackInboundSeen,
29555
29941
  addBoardPin,
29556
29942
  addPrStackLayer,
@@ -29731,6 +30117,7 @@ init_outbound_watch();
29731
30117
  fireSchedule,
29732
30118
  flattenTurnInput,
29733
30119
  flipLinearRelationType,
30120
+ foldLegacyRolesIntoNotes,
29734
30121
  followUpBehavior,
29735
30122
  forkChatTab,
29736
30123
  forkMessageSlice,
@@ -29771,6 +30158,8 @@ init_outbound_watch();
29771
30158
  formatSlackWorkingText,
29772
30159
  formatTranscriptMarkdown,
29773
30160
  formatUiReminder,
30161
+ formatViewerContextDirective,
30162
+ formatViewerContextReminder,
29774
30163
  formatWorkspaceInventory,
29775
30164
  formatWorkspaceProfileSuffix,
29776
30165
  formatWorktreeDirective,
@@ -30008,7 +30397,6 @@ init_outbound_watch();
30008
30397
  planQuestionsSignature,
30009
30398
  pollSlackOutboundWatches,
30010
30399
  posixShellSingleQuote,
30011
- preferTeamsForRole,
30012
30400
  preferredCursorCostCents,
30013
30401
  prepareTerminalCommand,
30014
30402
  previewLand,
@@ -30020,6 +30408,7 @@ init_outbound_watch();
30020
30408
  readSetupLog,
30021
30409
  readSkillBody,
30022
30410
  readThread,
30411
+ readViewerContext,
30023
30412
  readWorktreeFile,
30024
30413
  readWorktreeFileForUpload,
30025
30414
  readWorktreeInclude,
@@ -30076,7 +30465,6 @@ init_outbound_watch();
30076
30465
  resolveViewerProfile,
30077
30466
  resolveViewerProfileForRepo,
30078
30467
  resolveWorktreeStartPoint,
30079
- reviewTeamHintsForRoles,
30080
30468
  rewriteAbleTimeError,
30081
30469
  rewriteLinearError,
30082
30470
  run,
@@ -30210,5 +30598,6 @@ init_outbound_watch();
30210
30598
  writeInjectedMcpConfig,
30211
30599
  writePlanFile,
30212
30600
  writeThread,
30601
+ writeViewerContext,
30213
30602
  writeWorktreeFile
30214
30603
  });