@sideboard-ai/core 0.1.168 → 0.1.170

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{abletime-BMS53RIZ.js → abletime-ESVTHTFH.js} +2 -2
  2. package/dist/{abletime-AGF54TNA.js → abletime-IC2V7CNB.js} +2 -2
  3. package/dist/{agents-TSO4MB3X.js → agents-GOBVOXFG.js} +9 -9
  4. package/dist/{agents-KWJXZAHB.js → agents-ZCIMZHXZ.js} +9 -9
  5. package/dist/{app-settings-BRLZQA7N.js → app-settings-LF3TS7XS.js} +5 -1
  6. package/dist/{app-settings-JJQDWPEK.js → app-settings-VMOTQORT.js} +5 -1
  7. package/dist/{chunk-74TWOSQP.js → chunk-3C4B52TS.js} +78 -42
  8. package/dist/{chunk-CVZ4KXVP.js → chunk-7IUO67CW.js} +122 -2
  9. package/dist/{chunk-HSGSQA42.js → chunk-DHPZW2EA.js} +8 -34
  10. package/dist/{chunk-KJFFQAMA.js → chunk-DKLGWRWC.js} +1 -1
  11. package/dist/{chunk-NYMFNUT5.js → chunk-GQHVICNT.js} +194 -147
  12. package/dist/{chunk-ILXKJ4ZD.js → chunk-NB7TEKCF.js} +1 -1
  13. package/dist/{chunk-RS756MKF.js → chunk-P2VCMIZU.js} +8 -34
  14. package/dist/{chunk-UIR4VDYA.js → chunk-SAZIRJR4.js} +30 -10
  15. package/dist/{chunk-UVLPL2FJ.js → chunk-TEI72FYD.js} +198 -216
  16. package/dist/{chunk-3A54S244.js → chunk-UDEWGTP6.js} +29 -10
  17. package/dist/{chunk-TW5A2HFY.js → chunk-Y7DJA3UW.js} +78 -42
  18. package/dist/{chunk-LTXS26K5.js → chunk-YMF5WCIA.js} +122 -2
  19. package/dist/{coordinator-prompt-75N74HAO.js → coordinator-prompt-JTDCEU74.js} +3 -3
  20. package/dist/{coordinator-prompt-Y372QRFE.js → coordinator-prompt-RRUU2OYO.js} +3 -3
  21. package/dist/{global-workspace-JNNFVSP7.js → global-workspace-4SI3CRRT.js} +3 -3
  22. package/dist/{global-workspace-TPE4LKVM.js → global-workspace-ZF6EJAM3.js} +3 -3
  23. package/dist/index.cjs +421 -278
  24. package/dist/index.d.cts +135 -37
  25. package/dist/index.d.ts +135 -37
  26. package/dist/index.js +33 -19
  27. package/dist/mcp/run-stdio.cjs +401 -207
  28. package/dist/mcp/run-stdio.js +8 -8
  29. package/dist/{orchestrator-XG3QCMOS.js → orchestrator-LE6L6MJC.js} +5 -5
  30. package/dist/{orchestrator-IIME6KXQ.js → orchestrator-UZSFTDKA.js} +5 -5
  31. package/dist/{workspaces-2IGQNFPL.js → workspaces-A7C3WA54.js} +3 -3
  32. package/dist/{workspaces-BWZSDSGG.js → workspaces-NOC2MWQV.js} +3 -3
  33. package/dist/{worktree-7X7XNSYL.js → worktree-GXO2VDQF.js} +12 -4
  34. package/dist/{worktree-5BFEK6PF.js → worktree-PODE23PD.js} +12 -4
  35. package/package.json +1 -1
@@ -2382,8 +2382,10 @@ function lookupSoccerTeam(titleOrSlug) {
2382
2382
  }
2383
2383
  function teamNameFromSlug(slug) {
2384
2384
  const normalized = slug.toLowerCase().replace(/^thread\//, "");
2385
- const suffixMatch = normalized.match(/^(.+)-(\d+)$/);
2386
- const base = suffixMatch?.[1] ?? normalized;
2385
+ const withoutTicket = normalized.replace(/^[a-z]{2,8}-\d{1,6}-/, "").replace(/^\d{1,8}-/, "");
2386
+ const lookup = withoutTicket || normalized;
2387
+ const suffixMatch = lookup.match(/^(.+)-(\d+)$/);
2388
+ const base = suffixMatch?.[1] ?? lookup;
2387
2389
  const suffix = suffixMatch?.[2];
2388
2390
  const team = FAMOUS_SOCCER_TEAM_SEEDS.find((t) => t.slug === base);
2389
2391
  if (team) return suffix ? `${team.name} ${suffix}` : team.name;
@@ -2804,6 +2806,25 @@ function worktreeNameFromPath(worktreePath) {
2804
2806
  const parts = normalized.split("/");
2805
2807
  return parts[parts.length - 1] || worktreePath;
2806
2808
  }
2809
+ function ticketSlugForBranch(sourceRef) {
2810
+ const raw = sourceRef.trim();
2811
+ if (!raw) return null;
2812
+ const linear = raw.match(/\b([A-Za-z]{2,8}-\d{1,6})\b/);
2813
+ if (linear) return linear[1].toLowerCase();
2814
+ const hashed = raw.match(/^#(\d{1,8})$/);
2815
+ if (hashed) return hashed[1];
2816
+ const digits = raw.match(/^(\d{1,8})$/);
2817
+ if (digits) return digits[1];
2818
+ const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
2819
+ return slug || null;
2820
+ }
2821
+ function worktreeSlugForTicket(ticketRef, teamSlug) {
2822
+ const ticket = ticketSlugForBranch(ticketRef);
2823
+ const team = teamSlug.trim().replace(/^thread\//, "");
2824
+ if (!ticket) return team;
2825
+ if (!team) return ticket;
2826
+ return `${ticket}-${team}`;
2827
+ }
2807
2828
  function isPlaceholderBranch(branchName, worktreePath) {
2808
2829
  const branch = branchName.trim();
2809
2830
  if (!branch || branch === "HEAD") return true;
@@ -3008,6 +3029,36 @@ var init_worktree_exclude = __esm({
3008
3029
  }
3009
3030
  });
3010
3031
 
3032
+ // src/agents/orchestrator-capable.ts
3033
+ function isOrchestratorCapableAgent(agent) {
3034
+ return Boolean(
3035
+ agent && ORCHESTRATOR_AGENT_KINDS.includes(agent)
3036
+ );
3037
+ }
3038
+ function assertOrchestratorCapableAgent(agent, context = "orchestration") {
3039
+ if (!isOrchestratorCapableAgent(agent)) {
3040
+ throw new Error(
3041
+ `${agent} cannot run ${context} \u2014 it does not support Sideboard MCP. Use Claude, Cursor, Codex, or OpenCode.`
3042
+ );
3043
+ }
3044
+ return agent;
3045
+ }
3046
+ function coerceOrchestratorAgent(agent, fallback = "claude") {
3047
+ return isOrchestratorCapableAgent(agent) ? agent : fallback;
3048
+ }
3049
+ var ORCHESTRATOR_AGENT_KINDS;
3050
+ var init_orchestrator_capable = __esm({
3051
+ "src/agents/orchestrator-capable.ts"() {
3052
+ "use strict";
3053
+ ORCHESTRATOR_AGENT_KINDS = [
3054
+ "claude",
3055
+ "codex",
3056
+ "opencode",
3057
+ "cursor"
3058
+ ];
3059
+ }
3060
+ });
3061
+
3011
3062
  // src/store/account-profile.ts
3012
3063
  function legacyRoleLabel(role) {
3013
3064
  const known = LEGACY_ROLE_LABELS[role];
@@ -3273,7 +3324,9 @@ __export(app_settings_exports, {
3273
3324
  resolveAgentExecutable: () => resolveAgentExecutable,
3274
3325
  resolveClaudeExecutable: () => resolveClaudeExecutable,
3275
3326
  resolveEffectiveIssueSource: () => resolveEffectiveIssueSource,
3327
+ resolveNewOrchestratorOptions: () => resolveNewOrchestratorOptions,
3276
3328
  resolveNewThreadOptions: () => resolveNewThreadOptions,
3329
+ resolveOrchestratorDefaults: () => resolveOrchestratorDefaults,
3277
3330
  resolveThreadDefaults: () => resolveThreadDefaults,
3278
3331
  resolveViewerProfile: () => resolveViewerProfile,
3279
3332
  resolveViewerProfileForRepo: () => resolveViewerProfileForRepo,
@@ -3535,6 +3588,25 @@ function normalizeIntegrations(raw) {
3535
3588
  }
3536
3589
  return out;
3537
3590
  }
3591
+ function normalizeOrchestratorDefaults(raw) {
3592
+ if (!raw || typeof raw !== "object") return void 0;
3593
+ const source = raw;
3594
+ const out = {};
3595
+ if (typeof source.agent === "string" && isOrchestratorCapableAgent(source.agent)) {
3596
+ out.agent = source.agent;
3597
+ }
3598
+ if (typeof source.model === "string") {
3599
+ const model = source.model.trim();
3600
+ if (model) out.model = model;
3601
+ }
3602
+ if (normalizeThinkingEffort(source.effort)) {
3603
+ out.effort = normalizeThinkingEffort(source.effort);
3604
+ }
3605
+ if (typeof source.fast === "boolean") {
3606
+ out.fast = source.fast;
3607
+ }
3608
+ return Object.keys(out).length > 0 ? out : void 0;
3609
+ }
3538
3610
  function normalizeDefaults(raw) {
3539
3611
  if (!raw || typeof raw !== "object") return {};
3540
3612
  const source = raw;
@@ -3554,6 +3626,8 @@ function normalizeDefaults(raw) {
3554
3626
  }
3555
3627
  const notes = foldLegacyRolesIntoNotes(source.roles, source.role, source.notes);
3556
3628
  if (notes) out.notes = notes;
3629
+ const orchestrator = normalizeOrchestratorDefaults(source.orchestrator);
3630
+ if (orchestrator) out.orchestrator = orchestrator;
3557
3631
  return out;
3558
3632
  }
3559
3633
  function profileRepoKey(path) {
@@ -4097,6 +4171,39 @@ function updateIntegrationsSettings(patch) {
4097
4171
  }
4098
4172
  return saveAppSettings({ ...current, integrations });
4099
4173
  }
4174
+ function applyOrchestratorDefaultsPatch(current, patch) {
4175
+ const next = { ...current };
4176
+ if ("agent" in patch) {
4177
+ if (patch.agent == null) {
4178
+ delete next.agent;
4179
+ } else if (isOrchestratorCapableAgent(patch.agent)) {
4180
+ next.agent = patch.agent;
4181
+ }
4182
+ }
4183
+ if ("model" in patch) {
4184
+ if (patch.model == null || patch.model.trim() === "") {
4185
+ delete next.model;
4186
+ } else {
4187
+ next.model = patch.model.trim();
4188
+ }
4189
+ }
4190
+ if ("effort" in patch) {
4191
+ if (patch.effort == null) {
4192
+ delete next.effort;
4193
+ } else {
4194
+ const effort = normalizeThinkingEffort(patch.effort);
4195
+ if (effort) next.effort = effort;
4196
+ }
4197
+ }
4198
+ if ("fast" in patch) {
4199
+ if (patch.fast == null) {
4200
+ delete next.fast;
4201
+ } else {
4202
+ next.fast = Boolean(patch.fast);
4203
+ }
4204
+ }
4205
+ return Object.keys(next).length > 0 ? next : void 0;
4206
+ }
4100
4207
  function updateDefaultsSettings(patch) {
4101
4208
  const current = loadAppSettings();
4102
4209
  const defaults = { ...current.defaults };
@@ -4134,6 +4241,18 @@ function updateDefaultsSettings(patch) {
4134
4241
  if (!notes) delete defaults.notes;
4135
4242
  else defaults.notes = notes;
4136
4243
  }
4244
+ if ("orchestrator" in patch) {
4245
+ if (patch.orchestrator == null) {
4246
+ delete defaults.orchestrator;
4247
+ } else {
4248
+ const orchestrator = applyOrchestratorDefaultsPatch(
4249
+ defaults.orchestrator,
4250
+ patch.orchestrator
4251
+ );
4252
+ if (orchestrator) defaults.orchestrator = orchestrator;
4253
+ else delete defaults.orchestrator;
4254
+ }
4255
+ }
4137
4256
  delete defaults.roles;
4138
4257
  delete defaults.role;
4139
4258
  return saveAppSettings({ ...current, defaults });
@@ -4239,8 +4358,22 @@ function resolveThreadDefaults(settings = loadAppSettings()) {
4239
4358
  fast: getDefaultFast(settings)
4240
4359
  };
4241
4360
  }
4242
- function resolveNewThreadOptions(overrides = {}, settings = loadAppSettings()) {
4243
- const defaults = resolveThreadDefaults(settings);
4361
+ function resolveOrchestratorDefaults(settings = loadAppSettings()) {
4362
+ const account = resolveThreadDefaults(settings);
4363
+ const orch = settings.defaults.orchestrator;
4364
+ const agent = coerceOrchestratorAgent(orch?.agent ?? account.agent);
4365
+ let model = orch ? orch.model?.trim() || null : account.model;
4366
+ if (model && agent !== "cursor" && /^(default|auto)$/i.test(model.trim())) {
4367
+ model = null;
4368
+ }
4369
+ return {
4370
+ agent,
4371
+ model,
4372
+ effort: orch?.effort ?? account.effort,
4373
+ fast: orch?.fast ?? account.fast
4374
+ };
4375
+ }
4376
+ function mergeThreadOptionOverrides(defaults, overrides) {
4244
4377
  const effort = overrides.effort === void 0 || overrides.effort === null ? defaults.effort : normalizeThinkingEffort(overrides.effort) ?? defaults.effort;
4245
4378
  const agent = overrides.agent ?? defaults.agent;
4246
4379
  let model = overrides.model === void 0 ? defaults.model : overrides.model?.trim() || null;
@@ -4254,6 +4387,12 @@ function resolveNewThreadOptions(overrides = {}, settings = loadAppSettings()) {
4254
4387
  fast: overrides.fast === void 0 || overrides.fast === null ? defaults.fast : Boolean(overrides.fast)
4255
4388
  };
4256
4389
  }
4390
+ function resolveNewThreadOptions(overrides = {}, settings = loadAppSettings()) {
4391
+ return mergeThreadOptionOverrides(resolveThreadDefaults(settings), overrides);
4392
+ }
4393
+ function resolveNewOrchestratorOptions(overrides = {}, settings = loadAppSettings()) {
4394
+ return mergeThreadOptionOverrides(resolveOrchestratorDefaults(settings), overrides);
4395
+ }
4257
4396
  function isLinearConnected(settings = loadAppSettings()) {
4258
4397
  return hasLinearCredentials(settings.integrations);
4259
4398
  }
@@ -4375,6 +4514,10 @@ function brightsyInjectWorktreeMcpEnabled(settings = loadAppSettings()) {
4375
4514
  }
4376
4515
  function brightsyCloudConnectAgent(settings = loadAppSettings()) {
4377
4516
  const fallback = settings.brightsy.cloudConnectAgent && CLOUD_CONNECT_AGENTS.has(settings.brightsy.cloudConnectAgent) ? settings.brightsy.cloudConnectAgent : "claude";
4517
+ const orchAgent = settings.defaults.orchestrator?.agent;
4518
+ if (orchAgent && CLOUD_CONNECT_AGENTS.has(orchAgent)) {
4519
+ return orchAgent;
4520
+ }
4378
4521
  const preferred = getDefaultAgent(settings);
4379
4522
  if (CLOUD_CONNECT_AGENTS.has(preferred)) {
4380
4523
  return preferred;
@@ -4585,6 +4728,7 @@ var init_app_settings = __esm({
4585
4728
  import_node_fs14 = require("fs");
4586
4729
  import_node_os5 = require("os");
4587
4730
  import_node_path16 = require("path");
4731
+ init_orchestrator_capable();
4588
4732
  init_thinking_effort();
4589
4733
  init_nested_electron_env();
4590
4734
  init_account_profile();
@@ -5563,6 +5707,7 @@ var init_workspace_scratch = __esm({
5563
5707
  var worktree_exports = {};
5564
5708
  __export(worktree_exports, {
5565
5709
  FAMOUS_SOCCER_TEAMS: () => FAMOUS_SOCCER_TEAMS,
5710
+ PR_DETAILS_JSON_FIELDS: () => PR_DETAILS_JSON_FIELDS,
5566
5711
  allocateTeamName: () => allocateTeamName,
5567
5712
  allocateTeamSlug: () => allocateTeamSlug,
5568
5713
  branchDisplayLabel: () => branchDisplayLabel,
@@ -5601,6 +5746,7 @@ __export(worktree_exports, {
5601
5746
  originFetchBranch: () => originFetchBranch,
5602
5747
  originGhRepoEnv: () => originGhRepoEnv,
5603
5748
  parseGithubSlugFromRemoteUrl: () => parseGithubSlugFromRemoteUrl,
5749
+ parsePrDetailsView: () => parsePrDetailsView,
5604
5750
  pushBranch: () => pushBranch,
5605
5751
  removeWorktree: () => removeWorktree,
5606
5752
  resolveDefaultBranch: () => resolveDefaultBranch,
@@ -5613,9 +5759,11 @@ __export(worktree_exports, {
5613
5759
  slugify: () => slugify,
5614
5760
  suggestSlug: () => suggestSlug,
5615
5761
  threadDisplayLabel: () => threadDisplayLabel,
5762
+ ticketSlugForBranch: () => ticketSlugForBranch,
5616
5763
  worktreeDisplayLabel: () => worktreeDisplayLabel,
5617
5764
  worktreeDisplayLabelForGroup: () => worktreeDisplayLabelForGroup,
5618
- worktreeNameFromPath: () => worktreeNameFromPath
5765
+ worktreeNameFromPath: () => worktreeNameFromPath,
5766
+ worktreeSlugForTicket: () => worktreeSlugForTicket
5619
5767
  });
5620
5768
  async function lookupGithubGraphqlReset(cwd) {
5621
5769
  const result = await gh(["api", "rate_limit"], cwd, { reject: false });
@@ -6176,48 +6324,15 @@ async function getPrMeta(cwd, selector) {
6176
6324
  return null;
6177
6325
  }
6178
6326
  }
6179
- async function getPrDetails(cwd, selector) {
6180
- const slug = await resolveGithubRepoSlug(cwd);
6181
- const viewArgs = [
6182
- "pr",
6183
- "view",
6184
- selector,
6185
- "--json",
6186
- [
6187
- "number",
6188
- "title",
6189
- "body",
6190
- "url",
6191
- "state",
6192
- "isDraft",
6193
- "reviewDecision",
6194
- "author",
6195
- "baseRefName",
6196
- "headRefName",
6197
- "additions",
6198
- "deletions",
6199
- "changedFiles",
6200
- "comments",
6201
- "reviews"
6202
- ].join(",")
6203
- ];
6204
- if (slug) viewArgs.push("--repo", slug);
6205
- const { stdout, exitCode, stderr } = await gh(viewArgs, cwd, { reject: false });
6206
- if (exitCode !== 0 || !stdout.trim()) {
6207
- if (stderr.trim()) {
6208
- if (/no pull requests found/i.test(stderr)) return null;
6209
- }
6210
- return null;
6211
- }
6212
- let view;
6213
- try {
6214
- view = JSON.parse(stdout);
6215
- } catch {
6216
- throw new Error(stderr.trim() || "gh pr view returned invalid JSON");
6217
- }
6327
+ function parsePrDetailsView(view) {
6218
6328
  const author = view.author ?? {};
6219
6329
  const comments = Array.isArray(view.comments) ? view.comments : [];
6220
6330
  const reviews = Array.isArray(view.reviews) ? view.reviews : [];
6331
+ const requested = {
6332
+ ids: reviewerIdentities(view.reviewRequests),
6333
+ users: humanReviewerLogins(view.reviewRequests),
6334
+ teams: teamReviewerSlugs(view.reviewRequests)
6335
+ };
6221
6336
  return {
6222
6337
  number: Number(view.number),
6223
6338
  title: String(view.title ?? ""),
@@ -6253,10 +6368,34 @@ async function getPrDetails(cwd, selector) {
6253
6368
  submittedAt: normalizeGhTime(row.submittedAt)
6254
6369
  };
6255
6370
  }),
6371
+ assignees: reviewerIdentities(view.assignees),
6372
+ labels: labelNames(view.labels),
6373
+ reviewRequests: requested.ids,
6374
+ reviewers: requested.users,
6375
+ teams: requested.teams,
6256
6376
  // CI lives in Checks tab via getPrChecks — nesting burned GraphQL points.
6257
6377
  checks: []
6258
6378
  };
6259
6379
  }
6380
+ async function getPrDetails(cwd, selector) {
6381
+ const slug = await resolveGithubRepoSlug(cwd);
6382
+ const viewArgs = ["pr", "view", selector, "--json", PR_DETAILS_JSON_FIELDS];
6383
+ if (slug) viewArgs.push("--repo", slug);
6384
+ const { stdout, exitCode, stderr } = await gh(viewArgs, cwd, { reject: false });
6385
+ if (exitCode !== 0 || !stdout.trim()) {
6386
+ if (stderr.trim()) {
6387
+ if (/no pull requests found/i.test(stderr)) return null;
6388
+ }
6389
+ return null;
6390
+ }
6391
+ let view;
6392
+ try {
6393
+ view = JSON.parse(stdout);
6394
+ } catch {
6395
+ throw new Error(stderr.trim() || "gh pr view returned invalid JSON");
6396
+ }
6397
+ return parsePrDetailsView(view);
6398
+ }
6260
6399
  async function fetchPrHead(repoPath, number, localBranch) {
6261
6400
  const slug = await resolveGithubRepoSlug(repoPath);
6262
6401
  const refspec = `+pull/${number}/head:${localBranch}`;
@@ -6967,7 +7106,7 @@ function allocateTeamSlug(repoPath) {
6967
7106
  }
6968
7107
  throw new Error("No available soccer team worktree directories left");
6969
7108
  }
6970
- var import_node_crypto4, import_node_fs15, import_node_os6, import_node_path18;
7109
+ var import_node_crypto4, import_node_fs15, import_node_os6, import_node_path18, PR_DETAILS_JSON_FIELDS;
6971
7110
  var init_worktree = __esm({
6972
7111
  "src/git/worktree.ts"() {
6973
7112
  "use strict";
@@ -6989,9 +7128,30 @@ var init_worktree = __esm({
6989
7128
  init_pr_gates();
6990
7129
  init_stack();
6991
7130
  init_teams();
7131
+ init_worktree_labels();
6992
7132
  init_git_auth_mode();
6993
7133
  init_workspace_scratch();
6994
7134
  init_worktree_labels();
7135
+ PR_DETAILS_JSON_FIELDS = [
7136
+ "number",
7137
+ "title",
7138
+ "body",
7139
+ "url",
7140
+ "state",
7141
+ "isDraft",
7142
+ "reviewDecision",
7143
+ "author",
7144
+ "baseRefName",
7145
+ "headRefName",
7146
+ "additions",
7147
+ "deletions",
7148
+ "changedFiles",
7149
+ "comments",
7150
+ "reviews",
7151
+ "assignees",
7152
+ "labels",
7153
+ "reviewRequests"
7154
+ ].join(",");
6995
7155
  }
6996
7156
  });
6997
7157
 
@@ -7257,36 +7417,6 @@ var init_stage_files = __esm({
7257
7417
  }
7258
7418
  });
7259
7419
 
7260
- // src/agents/orchestrator-capable.ts
7261
- function isOrchestratorCapableAgent(agent) {
7262
- return Boolean(
7263
- agent && ORCHESTRATOR_AGENT_KINDS.includes(agent)
7264
- );
7265
- }
7266
- function assertOrchestratorCapableAgent(agent, context = "orchestration") {
7267
- if (!isOrchestratorCapableAgent(agent)) {
7268
- throw new Error(
7269
- `${agent} cannot run ${context} \u2014 it does not support Sideboard MCP. Use Claude, Cursor, Codex, or OpenCode.`
7270
- );
7271
- }
7272
- return agent;
7273
- }
7274
- function coerceOrchestratorAgent(agent, fallback = "claude") {
7275
- return isOrchestratorCapableAgent(agent) ? agent : fallback;
7276
- }
7277
- var ORCHESTRATOR_AGENT_KINDS;
7278
- var init_orchestrator_capable = __esm({
7279
- "src/agents/orchestrator-capable.ts"() {
7280
- "use strict";
7281
- ORCHESTRATOR_AGENT_KINDS = [
7282
- "claude",
7283
- "codex",
7284
- "opencode",
7285
- "cursor"
7286
- ];
7287
- }
7288
- });
7289
-
7290
7420
  // src/store/workspaces.ts
7291
7421
  var workspaces_exports = {};
7292
7422
  __export(workspaces_exports, {
@@ -7751,7 +7881,7 @@ function createGlobalChat(opts) {
7751
7881
  const explicit = opts.title?.trim();
7752
7882
  const title = explicit && explicit !== CLOUD_ORCHESTRATOR_GOAL ? explicit : allocateTeamName(takenTeamSlugsForOrchestration()).name;
7753
7883
  const sourceRef = opts.sourceRef?.trim() || (isCloud ? CLOUD_ORCHESTRATOR_GOAL : title);
7754
- const resolved = resolveNewThreadOptions({
7884
+ const resolved = resolveNewOrchestratorOptions({
7755
7885
  agent: opts.agent,
7756
7886
  model: opts.model,
7757
7887
  effort: opts.effort,
@@ -7827,7 +7957,7 @@ function findSlackCoordinator(teamId, userId) {
7827
7957
  );
7828
7958
  }
7829
7959
  function ensureSlackCoordinator(teamId, userId, agent, opts) {
7830
- const defaults = resolveThreadDefaults();
7960
+ const defaults = resolveOrchestratorDefaults();
7831
7961
  const desired = assertOrchestratorCapableAgent(agent);
7832
7962
  const ref = slackCoordinatorSourceRef(teamId, userId);
7833
7963
  if (!opts?.forceNew) {
@@ -7862,7 +7992,7 @@ function ensureSlackCoordinator(teamId, userId, agent, opts) {
7862
7992
  });
7863
7993
  }
7864
7994
  function ensureCloudCoordinator(agent) {
7865
- const defaults = resolveThreadDefaults();
7995
+ const defaults = resolveOrchestratorDefaults();
7866
7996
  const desired = assertOrchestratorCapableAgent(agent);
7867
7997
  const existing = findCloudCoordinator();
7868
7998
  if (existing) {
@@ -8315,22 +8445,38 @@ var init_brightsy_targets = __esm({
8315
8445
  // src/agents/turn-input.ts
8316
8446
  function normalizeTurnInput(input) {
8317
8447
  if (typeof input === "string") return { prompt: input };
8318
- return { prompt: input.prompt, cachedPrefix: input.cachedPrefix?.trim() || void 0 };
8448
+ return {
8449
+ prompt: input.prompt,
8450
+ cachedPrefix: input.cachedPrefix?.trim() || void 0,
8451
+ systemPrompt: input.systemPrompt?.trim() || void 0
8452
+ };
8319
8453
  }
8320
8454
  function dropCachedPrefixOnResume(input, sessionId) {
8321
8455
  const turn = normalizeTurnInput(input);
8322
- if (sessionId) return { prompt: turn.prompt };
8456
+ if (sessionId) {
8457
+ return turn.systemPrompt ? { prompt: turn.prompt, systemPrompt: turn.systemPrompt } : { prompt: turn.prompt };
8458
+ }
8323
8459
  return turn;
8324
8460
  }
8325
8461
  function flattenTurnInput(input) {
8326
- const { cachedPrefix, prompt } = normalizeTurnInput(input);
8327
- if (!cachedPrefix) return prompt;
8328
- return `${cachedPrefix}
8462
+ const { cachedPrefix, prompt, systemPrompt } = normalizeTurnInput(input);
8463
+ if (cachedPrefix) return `${cachedPrefix}
8329
8464
 
8330
8465
  ---
8331
8466
 
8332
8467
  Current request:
8333
8468
  ${prompt}`;
8469
+ if (systemPrompt) return `${systemPrompt}
8470
+
8471
+ ${prompt}`;
8472
+ return prompt;
8473
+ }
8474
+ function splitTurnInputForSystemPrompt(input) {
8475
+ const turn = normalizeTurnInput(input);
8476
+ return {
8477
+ promptText: flattenTurnInput({ cachedPrefix: turn.cachedPrefix, prompt: turn.prompt }),
8478
+ systemPrompt: turn.systemPrompt
8479
+ };
8334
8480
  }
8335
8481
  var init_turn_input = __esm({
8336
8482
  "src/agents/turn-input.ts"() {
@@ -10164,7 +10310,7 @@ var init_claude = __esm({
10164
10310
  const claude = resolveClaudeExecutable();
10165
10311
  const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
10166
10312
  const effective = dropCachedPrefixOnResume(input, sessionId);
10167
- const promptText = flattenTurnInput(effective);
10313
+ const { promptText, systemPrompt } = splitTurnInputForSystemPrompt(effective);
10168
10314
  const useStdin = promptText.length > CLAUDE_PROMPT_ARG_MAX;
10169
10315
  if (process.env.SIDEBOARD_DEBUG_CLAUDE_TURN === "1") {
10170
10316
  console.error(
@@ -10216,6 +10362,9 @@ var init_claude = __esm({
10216
10362
  "--permission-mode",
10217
10363
  mode.claude
10218
10364
  ];
10365
+ if (systemPrompt) {
10366
+ args.push("--append-system-prompt", systemPrompt);
10367
+ }
10219
10368
  const mcpConfigPath = writeMcpServersConfig(injectedServers);
10220
10369
  if (mcpConfigPath) {
10221
10370
  args.push("--mcp-config", mcpConfigPath);
@@ -12092,7 +12241,50 @@ function agentGitPrompt(action, opts) {
12092
12241
  return "Merge PR.";
12093
12242
  }
12094
12243
  }
12095
- var AGENT_GIT_ACTIONS;
12244
+ function gitActionExpansion(action, base) {
12245
+ switch (action) {
12246
+ case "commit-push":
12247
+ return "Commit any uncommitted work with a purpose-stating message, then push to origin (`git push -u origin HEAD`; this updates an existing PR when one is linked). Do not start a checks loop unless a goal was given.";
12248
+ case "create-draft":
12249
+ return "Commit, push, then create a draft PR with `gh pr create --draft -R <origin-owner/name>` (title/body from the change purpose). Update the existing PR instead if one is linked.";
12250
+ case "create-web":
12251
+ return "Commit, push, then `gh pr create --web -R <origin-owner/name>`.";
12252
+ case "resolve-conflicts":
12253
+ return `Fetch the PR base${base ? ` (\`${base}\`)` : ""}, merge it into this branch, resolve conflicts carefully, then commit and push until the PR is mergeable.`;
12254
+ case "merge":
12255
+ return "This phrase is the explicit ask to merge this thread's open pull request on GitHub. If `gh stack view` shows a stack, use `gh stack merge`; otherwise `gh pr merge` (respect repo defaults / squash vs merge). Do not force-push main/master or merge locally into the main checkout.";
12256
+ }
12257
+ }
12258
+ function expandCanonicalGitRequest(prompt) {
12259
+ const trimmed = prompt.trim();
12260
+ let action = null;
12261
+ let base = null;
12262
+ for (const candidate of AGENT_GIT_ACTIONS) {
12263
+ if (candidate === "resolve-conflicts") continue;
12264
+ if (agentGitPrompt(candidate) === trimmed) {
12265
+ action = candidate;
12266
+ break;
12267
+ }
12268
+ }
12269
+ if (!action) {
12270
+ const m = RESOLVE_CONFLICTS_RE.exec(trimmed);
12271
+ if (m) {
12272
+ action = "resolve-conflicts";
12273
+ base = m[1] ?? null;
12274
+ }
12275
+ }
12276
+ let expansion = action ? gitActionExpansion(action, base) : null;
12277
+ if (!expansion && trimmed === "Address review comments.") {
12278
+ expansion = "Read the PR review feedback (`gh pr view --comments`, `gh api` review comments), make the requested changes, commit, and push.";
12279
+ } else if (!expansion && FIX_CI_RE.test(trimmed)) {
12280
+ expansion = "Investigate that failing check (`gh pr checks`, `gh run view --log-failed`), fix it, commit, and push. Loop only if a goal to keep going until it passes was given.";
12281
+ }
12282
+ if (!expansion) return prompt;
12283
+ return `${trimmed}
12284
+
12285
+ (Sideboard git request \u2014 do not ask for clarification: ${expansion})`;
12286
+ }
12287
+ var AGENT_GIT_ACTIONS, RESOLVE_CONFLICTS_RE, FIX_CI_RE;
12096
12288
  var init_agent_git_actions = __esm({
12097
12289
  "src/git/agent-git-actions.ts"() {
12098
12290
  "use strict";
@@ -12103,6 +12295,8 @@ var init_agent_git_actions = __esm({
12103
12295
  "resolve-conflicts",
12104
12296
  "merge"
12105
12297
  ];
12298
+ RESOLVE_CONFLICTS_RE = /^Merge the remote branch(?: \(([^)]+)\))? into your branch and resolve conflicts\. Then, commit and push your changes\.$/;
12299
+ FIX_CI_RE = /^Fix CI: .+\.$/;
12106
12300
  }
12107
12301
  });
12108
12302
 
@@ -12340,47 +12534,82 @@ function formatToolPart(t, detail) {
12340
12534
  }
12341
12535
  function formatMessagesAsTranscript(messages, opts) {
12342
12536
  const tools = opts?.tools ?? "full";
12343
- const blocks = [];
12344
- for (const m of messages) {
12345
- if (m.role === "summary") {
12346
- blocks.push(`## Prior summary
12347
- ${m.text}`);
12348
- continue;
12349
- }
12350
- if (m.role === "user") {
12351
- blocks.push(`### User
12352
- ${m.text}`);
12353
- continue;
12354
- }
12355
- const bits = [];
12356
- if (m.text?.trim()) {
12357
- bits.push(`### Agent
12537
+ const thinking = opts?.thinking ?? true;
12538
+ return messages.map((m) => formatMessageBlock(m, { tools, thinking })).join("\n\n");
12539
+ }
12540
+ function formatMessageBlock(m, opts) {
12541
+ if (m.role === "summary") return `## Prior summary
12542
+ ${m.text}`;
12543
+ if (m.role === "user") return `### User
12544
+ ${m.text}`;
12545
+ const bits = [];
12546
+ if (m.text?.trim()) {
12547
+ bits.push(`### Agent
12358
12548
  ${m.text}`);
12359
- } else {
12360
- bits.push("### Agent");
12361
- }
12362
- const thinking = (m.parts ?? []).filter((p) => p.type === "thinking");
12363
- for (const th of thinking) {
12549
+ } else {
12550
+ bits.push("### Agent");
12551
+ }
12552
+ if (opts.thinking) {
12553
+ for (const th of m.parts ?? []) {
12364
12554
  if (th.type !== "thinking" || !th.text.trim()) continue;
12365
12555
  bits.push("Thinking:");
12366
12556
  bits.push(th.text);
12367
12557
  }
12368
- const toolParts = (m.parts ?? []).filter(
12369
- (p) => p.type === "tool"
12370
- );
12371
- if (tools !== "none" && toolParts.length > 0) {
12372
- if (tools === "summary") bits.push("Tools:");
12373
- for (const t of toolParts) {
12374
- bits.push(formatToolPart(t, tools));
12375
- }
12558
+ }
12559
+ const toolParts = (m.parts ?? []).filter(
12560
+ (p) => p.type === "tool"
12561
+ );
12562
+ if (opts.tools !== "none" && toolParts.length > 0) {
12563
+ if (opts.tools === "summary") bits.push("Tools:");
12564
+ for (const t of toolParts) {
12565
+ bits.push(formatToolPart(t, opts.tools));
12376
12566
  }
12377
- blocks.push(bits.join("\n"));
12378
12567
  }
12379
- return blocks.join("\n\n");
12568
+ return bits.join("\n");
12380
12569
  }
12381
12570
  function buildSessionSeed(messages, opts) {
12382
12571
  if (messages.length === 0) return null;
12383
- const body = formatMessagesAsTranscript(messages, { tools: opts?.tools ?? "full" });
12572
+ const tools = opts?.tools ?? "full";
12573
+ const fullCount = opts?.fullToolMessages ?? SEED_FULL_TOOL_MESSAGES;
12574
+ const maxChars = opts?.maxChars ?? SEED_MAX_CHARS;
12575
+ const olderTools = tools === "none" ? "none" : "summary";
12576
+ const fullFrom = Math.max(0, messages.length - fullCount);
12577
+ const blocks = messages.map(
12578
+ (m, i) => formatMessageBlock(m, {
12579
+ tools: i >= fullFrom ? tools : olderTools,
12580
+ thinking: false
12581
+ })
12582
+ );
12583
+ const keep = new Array(blocks.length).fill(false);
12584
+ let used = 0;
12585
+ for (let i = 0; i < blocks.length; i++) {
12586
+ if (messages[i].role === "summary") {
12587
+ keep[i] = true;
12588
+ used += blocks[i].length + 2;
12589
+ }
12590
+ }
12591
+ for (let i = blocks.length - 1; i >= 0; i--) {
12592
+ if (keep[i]) continue;
12593
+ const next = used + blocks[i].length + 2;
12594
+ if (next > maxChars && used > 0) break;
12595
+ keep[i] = true;
12596
+ used = next;
12597
+ }
12598
+ const dropped = keep.filter((k) => !k).length;
12599
+ const kept = [];
12600
+ let markerPlaced = false;
12601
+ for (let i = 0; i < blocks.length; i++) {
12602
+ if (!keep[i]) continue;
12603
+ if (dropped > 0 && !markerPlaced && messages[i].role !== "summary") {
12604
+ kept.push(`_(${dropped} older message${dropped === 1 ? "" : "s"} omitted for length)_`);
12605
+ markerPlaced = true;
12606
+ }
12607
+ kept.push(blocks[i]);
12608
+ }
12609
+ if (dropped > 0 && !markerPlaced) {
12610
+ kept.push(`_(${dropped} older message${dropped === 1 ? "" : "s"} omitted for length)_`);
12611
+ }
12612
+ const body = kept.join("\n\n");
12384
12613
  if (!body.trim()) return null;
12385
12614
  return [
12386
12615
  "Sideboard conversation context (restored after compaction or a new session):",
@@ -12433,7 +12662,7 @@ function findLastBrightsyContextSummary(messages) {
12433
12662
  function buildBrightsySessionSeed(messages) {
12434
12663
  const match = findLastBrightsyContextSummary(messages);
12435
12664
  const tail = match ? messages.slice(match.index + 1) : messages;
12436
- const body = formatMessagesAsTranscript(tail, { tools: "none" });
12665
+ const body = formatMessagesAsTranscript(tail, { tools: "none", thinking: false });
12437
12666
  if (!match && !body.trim()) return null;
12438
12667
  const blocks = [
12439
12668
  "Sideboard conversation context (restored after compaction or a new session):",
@@ -12500,7 +12729,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
12500
12729
  olderCount: older.length
12501
12730
  };
12502
12731
  }
12503
- var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAGES, CONTEXT_MIN_MESSAGES, SESSION_RESET_OCCUPANCY_TOKENS, BRIGHTSY_SUMMARIZE_CONTEXT_TOOL;
12732
+ var CONTEXT_COMPACT_CHARS, CONTEXT_KEEP_RECENT_CHARS, CONTEXT_KEEP_RECENT_MESSAGES, CONTEXT_MIN_MESSAGES, SESSION_RESET_OCCUPANCY_TOKENS, SEED_MAX_CHARS, SEED_FULL_TOOL_MESSAGES, BRIGHTSY_SUMMARIZE_CONTEXT_TOOL;
12504
12733
  var init_context_compact = __esm({
12505
12734
  "src/composer/context-compact.ts"() {
12506
12735
  "use strict";
@@ -12513,6 +12742,8 @@ var init_context_compact = __esm({
12513
12742
  CONTEXT_KEEP_RECENT_MESSAGES = 12;
12514
12743
  CONTEXT_MIN_MESSAGES = 10;
12515
12744
  SESSION_RESET_OCCUPANCY_TOKENS = 75e4;
12745
+ SEED_MAX_CHARS = 6e4;
12746
+ SEED_FULL_TOOL_MESSAGES = 6;
12516
12747
  BRIGHTSY_SUMMARIZE_CONTEXT_TOOL = "summarize_context";
12517
12748
  }
12518
12749
  });
@@ -15310,10 +15541,11 @@ async function createThread(input, _onSetupLine) {
15310
15541
  throw new Error("Use adoptThread() for adopt sources");
15311
15542
  }
15312
15543
  const team = allocateTeamSlug(repoPath);
15544
+ const slug = sourceType === "ticket" ? worktreeSlugForTicket(persistedSourceRef, team.slug) : team.slug;
15313
15545
  const { branchName, worktreePath } = await createThreadWorktree({
15314
15546
  repoPath,
15315
15547
  sourceRef,
15316
- slug: team.slug
15548
+ slug
15317
15549
  });
15318
15550
  copyConfiguredFiles(repoPath, worktreePath);
15319
15551
  const explicitTitle = title?.trim();
@@ -17586,12 +17818,17 @@ function normPath3(p) {
17586
17818
  function formatRenameBranchDirective(thread, opts) {
17587
17819
  if (!isPlaceholderBranch(thread.branchName, thread.worktreePath)) return null;
17588
17820
  const dir = worktreeNameFromPath(thread.worktreePath);
17821
+ const ticket = thread.sourceType === "ticket" ? ticketSlugForBranch(thread.sourceRef ?? "") : null;
17822
+ const example = ticket ? `\`feat/${ticket}-dark-mode\` or \`fix/${ticket}-panel-width\`` : "`fix/panel-width` or `feat/dark-mode`";
17589
17823
  const lines = [
17590
17824
  "Branch naming (do this early in the turn):",
17591
- `- Current branch \`${thread.branchName}\` is a temporary placeholder. The worktree folder \`${dir}\` is a stable soccer-team nickname \u2014 do not rename or leave that directory.`,
17592
- "- Rename the git branch to a short kebab-case name that describes this task (what you are changing), e.g. `fix/panel-width` or `feat/dark-mode`:",
17825
+ `- Current branch \`${thread.branchName}\` is a temporary placeholder. The worktree folder \`${dir}\` is a stable nickname \u2014 do not rename or leave that directory.`,
17826
+ `- Rename the git branch to a short kebab-case name that describes this task (what you are changing), e.g. ${example}:`,
17593
17827
  " `git branch -m <new-name>`",
17594
17828
  "- Prefer Conventional Commits style prefixes when they fit (`fix/`, `feat/`, `chore/`, `docs/`).",
17829
+ ...ticket ? [
17830
+ `- Keep ticket \`${ticket}\` in the new branch name so the issue stays findable.`
17831
+ ] : [],
17595
17832
  "- Never push this placeholder to main/master."
17596
17833
  ];
17597
17834
  const custom = opts?.customPrompt?.trim();
@@ -17625,67 +17862,37 @@ function formatWorktreeDirective(thread, opts) {
17625
17862
  lines.push(
17626
17863
  "- Always use this worktree's `origin` remote (`git remote get-url origin` from this cwd). Never push to or open PRs against `upstream` (template remotes)."
17627
17864
  );
17628
- lines.push(
17629
- "- Push with `git push -u origin HEAD` (or the current branch name). Do not `git push upstream`."
17630
- );
17631
- lines.push("");
17632
17865
  lines.push(formatGitAuthModeDirective(opts?.gitAuthMode ?? "auto"));
17633
17866
  lines.push(
17634
- "- Derive the PR title and body from what the changes actually do and why \u2014 inspect the diff/commits and the user request. Do not use the soccer-team worktree nickname or placeholder branch as the PR title."
17867
+ "- PR title/body and commit messages state what the change does and why (inspect the diff/commits and the user request) \u2014 never the worktree nickname or placeholder branch. Concise imperative title (Conventional Commits when it fits: feat:/fix:/chore:/docs:); body = intent, key changes, test notes."
17635
17868
  );
17636
17869
  lines.push(
17637
- "- Prefer a concise imperative title (Conventional Commits style when it fits: feat:/fix:/chore:/docs:). Body should summarize intent, key changes, and test notes."
17638
- );
17639
- lines.push(
17640
- "- Commit with messages that state the purpose of the change (same standard as the PR). Stay on this thread branch. Never push directly to main/master or merge locally into the main checkout. Do not merge the PR unless this turn is a Merge PR request or the user explicitly asked. When merging, use GitHub from this worktree (`gh pr merge` / `gh stack merge`)."
17870
+ "- Stay on this thread branch. Never push to main/master or merge locally into the main checkout. Merge the PR only when this turn is a Merge PR request or the user explicitly asked \u2014 via GitHub from this worktree (`gh pr merge` / `gh stack merge`)."
17641
17871
  );
17642
17872
  if (thread.prUrl) {
17643
17873
  lines.push(
17644
17874
  `- A PR already exists (${thread.prUrl}). Update it (push + edit title/body if the purpose drifted) instead of opening a duplicate.`
17645
17875
  );
17646
- } else if (opts?.githubSlug) {
17647
- lines.push(
17648
- `- Prefer a draft PR first: \`gh pr create --draft -R ${opts.githubSlug}\` (or update via \`gh pr edit -R ${opts.githubSlug}\`) once the change set is coherent. Always pass \`-R ${opts.githubSlug}\` (this worktree's origin). Bare \`gh pr create\` may target upstream instead of origin on dual-remote checkouts. Mark ready for review only when asked. Title/body must reflect the change purpose, not the worktree name. If GitHub rejects the GraphQL body as too long, retry with a short \`--body-file\` (limit 65,536 characters) \u2014 do not paste a changelog or diff into \`--body\`.`
17649
- );
17650
17876
  } else {
17877
+ const slug = opts?.githubSlug ?? "<origin-owner/name>";
17878
+ const resolve = opts?.githubSlug ? "" : " Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree.";
17651
17879
  lines.push(
17652
- "- Prefer a draft PR first: `gh pr create --draft -R <origin-owner/name>` (or update via `gh pr edit -R \u2026`) once the change set is coherent. Resolve `<origin-owner/name>` with `git remote get-url origin` in this worktree \u2014 never from `upstream`. Mark ready for review only when asked. Title/body must reflect the change purpose, not the worktree name. If GitHub rejects the GraphQL body as too long, retry with a short `--body-file` (limit 65,536 characters) \u2014 do not paste a changelog or diff into `--body`."
17880
+ `- Prefer a draft PR first: \`gh pr create --draft -R ${slug}\` (update via \`gh pr edit -R ${slug}\`). Always pass \`-R\` \u2014 bare \`gh pr create\` may target upstream on dual-remote checkouts.${resolve} Mark ready for review only when asked. If GitHub rejects the body as too long, retry with a short \`--body-file\` \u2014 do not paste a changelog or diff into \`--body\`.`
17653
17881
  );
17654
17882
  }
17655
- lines.push("");
17656
- lines.push(
17657
- "Short git requests from the Sideboard UI are complete instructions \u2014 expand them using the rules above without asking for clarification:"
17658
- );
17659
- lines.push(
17660
- '- "Commit and push." \u2192 commit any uncommitted work with a purpose-stating message, then push to origin (updates an existing PR if one is linked). Do not start a checks loop unless they gave a goal.'
17661
- );
17662
- lines.push(
17663
- '- "Commit, push, and open a draft PR." \u2192 commit, push, then create a draft PR with `gh pr create --draft -R \u2026` (title/body from the change purpose).'
17664
- );
17665
- lines.push(
17666
- '- "Commit, push, and open a PR in the browser." \u2192 commit, push, then `gh pr create --web -R \u2026`.'
17667
- );
17668
- lines.push(
17669
- '- "Fix CI: <name>." \u2192 investigate that failing check, fix it, commit, and push. Loop only if they gave a goal to keep going until it passes.'
17670
- );
17671
- lines.push(
17672
- '- "Merge the remote branch (<base>) into your branch and resolve conflicts. Then, commit and push your changes." \u2192 fetch the PR base, merge it into this branch, resolve conflicts carefully, commit, and push until the PR is mergeable.'
17673
- );
17674
17883
  lines.push(
17675
- '- "Address review comments." \u2192 read PR review feedback, make the requested changes, commit, and push.'
17676
- );
17677
- lines.push(
17678
- '- "Merge PR." \u2192 merge this thread\'s open pull request on GitHub (this phrase is the explicit ask). If `gh stack view` shows a stack, use `gh stack merge`; otherwise `gh pr merge` (respect repo defaults / squash vs merge). Do not force-push main/master or merge locally into the main checkout.'
17884
+ '- Sideboard git buttons send short phrases ("Commit and push.", "Merge PR.", \u2026) with their meaning attached \u2014 act on them without asking for clarification. If the user gives a goal (Greptile 5/5, CI green, until checks pass), you get the watch-fix-push playbook with that request.'
17679
17885
  );
17680
17886
  lines.push("");
17681
- lines.push(formatPrGateDirective());
17682
- lines.push("");
17683
- lines.push(formatProcessGuideDirective());
17887
+ lines.push(formatProcessGuideDirective({ worktreePath: thread.worktreePath }));
17684
17888
  return lines.join("\n");
17685
17889
  }
17686
17890
  function formatWorktreeReminder() {
17687
17891
  return "Sideboard worktree: stay in this cwd for all file and git work. Push and open PRs against origin, never upstream. Do not edit the main repo checkout. If a goal is given (Greptile 5/5, CI green), watch-fix-push until it lands \u2014 do not watch after every push.";
17688
17892
  }
17893
+ function mentionsPrGoal(prompt) {
17894
+ return PR_GOAL_RE.test(prompt);
17895
+ }
17689
17896
  function issueTicketFromThread(thread, preferredSource = "github") {
17690
17897
  if (thread?.sourceType !== "ticket") return null;
17691
17898
  const ref = thread.sourceRef?.trim() ?? "";
@@ -17752,29 +17959,27 @@ function formatPrGateDirective() {
17752
17959
  "- Greptile 5/5: wait for the greptile check; read the latest `N/5` from the PR body or Greptile\u2019s most recently *updated* issue comment; also fetch unresolved review comments / the \u201CPrompt to fix all with AI\u201D block. Done = **5/5 and zero unresolved Greptile comments**. If Greptile is idle, comment `@greptile review`. Cap about 5 iterations, then report leftovers. Do not merge unless asked."
17753
17960
  ].join("\n");
17754
17961
  }
17755
- function formatProcessGuideDirective() {
17962
+ function formatProcessGuideDirective(opts) {
17963
+ const worktree = opts?.worktreePath?.trim();
17964
+ const hasGraphSkill = !!worktree && (0, import_node_fs46.existsSync)((0, import_node_path43.join)(worktree, ".claude/skills/graph-engineering/SKILL.md"));
17965
+ const hasReviewSkill = !!worktree && (0, import_node_fs46.existsSync)((0, import_node_path43.join)(worktree, ".claude/skills/review/SKILL.md"));
17756
17966
  return [
17757
17967
  "Process guides (recurring work only):",
17758
- "- Long jobs (pack, test, deploy, anything that may run more than ~30s): Sideboard always provides `/long-running`. Detach and wait; `stop_job` if hanging or wrong. Do not ask the human to poll.",
17759
- "- If `.claude/skills/graph-engineering/SKILL.md` exists, follow it (`/graph-engineering`) for migrations, ports, batch fixes, and other fan-out. Judge first; state on disk; grow the rulebook; do not patch around it.",
17760
- "- If this same shape of work will happen again, write `.claude/skills/<kebab-name>/SKILL.md` in this worktree (Claude Code project skill). Sideboard `/name`, Claude Code, and `attach` all load that path. Do not leave the method only in chat.",
17761
- "- Merge-readiness notes: if `.claude/skills/review/SKILL.md` already exists, edit it. Otherwise write them to `.context/review.md` (copied from `.sideboard/review.md` when that file exists). Do not create a review skill.",
17762
- "- Do not write new skills under `.sideboard/skills/` (that folder only \u2014 other `.sideboard/` files such as settings.toml are fine). Point Codex/OpenCode at the Claude skill from `AGENTS.md`. Optional: symlink `.cursor/skills/<name>` to the Claude skill.",
17968
+ hasGraphSkill ? "- Migrations, ports, batch fixes, and other fan-out: follow `.claude/skills/graph-engineering/SKILL.md` (`/graph-engineering`). Judge first; state on disk; grow the rulebook; do not patch around it." : null,
17969
+ "- If this same shape of work will happen again, write `.claude/skills/<kebab-name>/SKILL.md` in this worktree (Claude Code project skill; Sideboard `/name`, Claude Code, and `attach` all load it). Not under `.sideboard/skills/`. Point Codex/OpenCode at it from `AGENTS.md`.",
17970
+ hasReviewSkill ? "- Merge-readiness notes go in `.claude/skills/review/SKILL.md` (edit it; do not create another review skill)." : "- Merge-readiness notes go in `.context/review.md` (copied from `.sideboard/review.md` when that file exists). Do not create a review skill.",
17763
17971
  "- Skip a guide for a one-off. If a matching skill exists, follow it. Same miss twice \u2192 edit the skill, do not patch around it."
17764
- ].join("\n");
17972
+ ].filter(Boolean).join("\n");
17765
17973
  }
17766
17974
  function formatLongRunningDirective(opts) {
17767
17975
  const invoke = formatDetachedJobInvoke(opts?.scriptPath);
17768
17976
  return [
17769
- "Long-running jobs (mandatory when a command may run more than ~30s):",
17770
- "A Sideboard 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. Do not ask the human to poll.",
17771
- `Helper (same tool as \`scripts/detached-job.cjs\` when that file exists in the worktree): \`${invoke}\``,
17772
- `- Start once: \`${invoke} start <id> -- <command> [args...]\` (cwd = this worktree). If JSON says already-running, do not start again.`,
17773
- "- Immediately `present_artifact` `type=log` with `artifact_id=<id>` and `status=running` \u2014 the side column is the live view.",
17774
- `- 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.`,
17775
- `- If it is hanging, producing no useful output, or doing the wrong thing: MCP \`stop_job\` (same id, optional reason) or \`${invoke} stop <id>\`. Do not loop forever. Do not stop a pack/test/deploy that is clearly making progress.`,
17776
- "- Do not end the turn with \u201CI\u2019ll let you know when it\u2019s done.\u201D Stay in the loop until stillRunning is false (or you stopped it).",
17777
- "- ok \u2192 finish the task. failed / stopped \u2192 read the log, fix or narrow the command, start once.",
17977
+ "Long-running jobs (mandatory when a command may run more than ~30s \u2014 pack, test, deploy, `gh pr checks --watch`):",
17978
+ "A Sideboard 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. Detach instead, and never ask the human to poll.",
17979
+ `- Start once: \`${invoke} start <id> -- <command> [args...]\` (cwd = this worktree; same tool as \`scripts/detached-job.cjs\` when the worktree has it). If JSON says already-running, do not start again.`,
17980
+ "- Immediately `present_artifact` `type=log` with `artifact_id=<id>` and `status=running`, then loop Sideboard MCP `wait_for_job` (returns in ~45s). stillRunning \u2192 present the same id with `content=delta` only \u2192 wait again. Shell fallback: the same helper with `wait <id>`.",
17981
+ "- Hanging, no useful output, or the wrong thing \u2192 `stop_job` (or the helper with `stop <id>`). Do not stop a pack/test/deploy that is clearly making progress.",
17982
+ "- Stay in the loop until stillRunning is false (or you stopped it). ok \u2192 finish the task. failed / stopped \u2192 read the log, fix or narrow the command, start once.",
17778
17983
  "State: `.context/.sideboard/detached-jobs/<id>/` (local scratch). Full guide: `/long-running` (always available)."
17779
17984
  ].join("\n");
17780
17985
  }
@@ -17784,33 +17989,17 @@ function formatLongRunningReminder(opts) {
17784
17989
  }
17785
17990
  function formatArtifactDirective() {
17786
17991
  return [
17787
- "Sideboard side column (desktop UI):",
17788
- "claude.ai\u2019s \u201CArtifact\u201D tool does NOT exist in Claude Code. That is expected.",
17789
- "Do not unprompted-duplicate a payload. Chat markdown (including tables) already renders in the transcript \u2014 do not also call present_schema with those same rows just to display them. If the user asks for an editable / interactive table, call present_schema even if markdown already showed the data. Do not also call present_artifact for a document you already fenced in chat.",
17790
- "Documents (HTML/SVG/markdown):",
17791
- "1) Emit a fenced code block tagged `html` (preferred), `svg`, or `markdown` with the FULL document \u2014 Sideboard opens a side column. Example:",
17792
- "```html",
17793
- "<!DOCTYPE html><html><head><title>Demo</title></head><body><h1>Hi</h1></body></html>",
17794
- "```",
17795
- "2) Or call Sideboard MCP `present_artifact` with title, type (html|svg|markdown|react|log), and content \u2014 not both a fence and this tool for the same body.",
17796
- " type=log is append-only: same `artifact_id`, `content` = new lines only (plus optional status/phase). Do not resend the full log or wrap it in HTML.",
17797
- "CMS / JSON Schema forms & tables (Brightsy or any schema+schemaUi source):",
17798
- "3) Call Sideboard MCP `present_schema` when the user needs to filter, edit, publish, or persist rows \u2014 including after you already showed a markdown table, if they then ask for an editable table. If they only need to read the data, a markdown table is enough. When you do call it, pass title, mode (table|form), and either:",
17799
- " - datasource=brightsy + resource_id (record type UUID) after fetching types via Brightsy MCP, or",
17800
- " - datasource=inline + resource: { id, title, schema, schemaUi } and optional records/record.",
17801
- "Files / media browser (CMS file manager column):",
17802
- "4) Call Sideboard MCP `present_files` with optional title, path, and datasource (brightsy|memory).",
17803
- " Opens the Files column for browse/upload/pick. Do NOT say a file manager UI is missing.",
17804
- "Multiple-choice questions:",
17805
- "5) Call Sideboard MCP `ask_user` only when work is blocked on choosing among a few concrete options (approach forks, which API, auth vs cookies). First write a short chat message that explains the decision and what each option means (tradeoffs, when to pick it). Include a description on every option. After calling, stop and wait for their next message with answers. If you are asking a real multiple-choice, use ask_user rather than chat bullets so Sideboard shows the composer picker.",
17806
- "Do not call ask_user 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. If one option is the obvious default, proceed without asking.",
17807
- "Never say artifacts, CMS UI, or the Files column are unavailable. present_schema is for interactive list/edit/publish (use it when they ask to edit, even if chat already had a markdown table); present_files for storage UI; html fences for standalone pages; ask_user only for those blocked predefined-option questions."
17992
+ "Sideboard side column (desktop UI) \u2014 Sideboard MCP tools present_artifact, present_schema, present_files, ask_user (see their descriptions). claude.ai\u2019s \u201CArtifact\u201D tool does not exist here; never say artifacts, the CMS UI, or the Files column are unavailable.",
17993
+ "- Standalone documents: a fenced block tagged `html` (preferred), `svg`, or `markdown` containing the FULL document opens the side column by itself. Use that or present_artifact \u2014 never both for the same body.",
17994
+ "- present_artifact type=log appends: same artifact_id, content = new lines only (plus status/phase). Do not resend the full log or wrap it in HTML.",
17995
+ "- Data: a markdown table is enough to read. Call present_schema only when the user needs to filter/edit/publish/persist rows \u2014 including when they ask for an editable table after you already showed markdown. Never re-present rows you already wrote just to display them.",
17996
+ "- ask_user only when work is blocked on a few concrete options (approach fork, which API, auth vs cookies): first a short chat message explaining the decision and each option, then the call (description on every option), then stop and wait. Not for greetings, check-ins, or an invented menu of next tasks \u2014 reply in chat. If one option is the obvious default, proceed."
17808
17997
  ].join("\n");
17809
17998
  }
17810
17999
  function formatUiReminder() {
17811
18000
  return "Sideboard UI: markdown table is enough to read data; present_schema if they ask to edit/filter (even after markdown); present_files for the file manager. html fence or present_artifact, not both for the same document. type=log appends (same artifact_id, new lines only). ask_user only for a real multiple-choice (not hellos or \u201Cwhat next?\u201D) \u2014 reply in chat. Do not say artifacts/CMS UI are unavailable.";
17812
18001
  }
17813
- var import_node_fs46, import_node_path43, GITHUB_TICKET_REF, KEYED_TICKET_REF;
18002
+ var import_node_fs46, import_node_path43, PR_GOAL_RE, GITHUB_TICKET_REF, KEYED_TICKET_REF;
17814
18003
  var init_instructions = __esm({
17815
18004
  "src/agents/instructions.ts"() {
17816
18005
  "use strict";
@@ -17819,6 +18008,7 @@ var init_instructions = __esm({
17819
18008
  init_git_auth_mode();
17820
18009
  init_worktree_labels();
17821
18010
  init_detached_job_path();
18011
+ PR_GOAL_RE = /greptile|\bci\b.*\b(green|pass)|checks?\s+(pass|green)|until\s+.*\b(pass|green|merge|clean)|\bfix ci\b|watch[- ]fix[- ]push/i;
17822
18012
  GITHUB_TICKET_REF = /^(?:#?\d+|gh-\d+)$/i;
17823
18013
  KEYED_TICKET_REF = /^(?:[A-Z][A-Z0-9]{0,9}-\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
17824
18014
  }
@@ -18507,7 +18697,7 @@ async function defaultDeps() {
18507
18697
  }
18508
18698
  function pickAgent(schedule) {
18509
18699
  if (schedule.agent) return schedule.agent;
18510
- return coerceOrchestratorAgent(resolveThreadDefaults().agent);
18700
+ return coerceOrchestratorAgent(resolveOrchestratorDefaults().agent);
18511
18701
  }
18512
18702
  async function fireSchedule(id) {
18513
18703
  const schedule = getSchedule(id);
@@ -19587,17 +19777,21 @@ var init_orchestrator = __esm({
19587
19777
  const slackReplyContext = formatSlackRepliesForTurn(
19588
19778
  pendingSlackExternalReplies(thread.messages)
19589
19779
  );
19590
- const agentPrompt = [
19591
- thread.planMode ? PLAN_MODE_INSTRUCTION : null,
19592
- orchestrationReminder,
19780
+ const turnReminders = [
19593
19781
  worktreeReminder,
19594
19782
  optionalServicesReminder,
19595
19783
  issueToolsReminder,
19596
19784
  viewerContextReminder,
19597
19785
  artifactReminder,
19598
- longRunningReminder,
19786
+ longRunningReminder
19787
+ ].filter(Boolean).join("\n\n") || void 0;
19788
+ const prGateDirective = thread.agent !== "brightsy" && !isOrchestratorThread(thread) && mentionsPrGoal(expandedPrompt) ? formatPrGateDirective() : null;
19789
+ const agentPrompt = [
19790
+ thread.planMode ? PLAN_MODE_INSTRUCTION : null,
19791
+ orchestrationReminder,
19792
+ prGateDirective,
19599
19793
  slackReplyContext,
19600
- expandedPrompt
19794
+ expandCanonicalGitRequest(expandedPrompt)
19601
19795
  ].filter(Boolean).join("\n\n");
19602
19796
  if (thread.attachments.length > 0) {
19603
19797
  updateThread(threadId, { attachments: [] });
@@ -19687,7 +19881,7 @@ var init_orchestrator = __esm({
19687
19881
  const stderrTail = [];
19688
19882
  const handle = await spawnAgentTurn(
19689
19883
  fresh,
19690
- { cachedPrefix, prompt: agentPrompt },
19884
+ { cachedPrefix, prompt: agentPrompt, systemPrompt: turnReminders },
19691
19885
  (event) => {
19692
19886
  this.emit({ type: "turn_output", threadId, event });
19693
19887
  noteTurnLiveEvent(threadId, event);
@@ -19796,7 +19990,7 @@ var init_orchestrator = __esm({
19796
19990
  ].filter(Boolean).join("\n\n---\n\n");
19797
19991
  const retryHandle = await spawnAgentTurn(
19798
19992
  retryThread,
19799
- { cachedPrefix: retryPrefix, prompt: agentPrompt },
19993
+ { cachedPrefix: retryPrefix, prompt: agentPrompt, systemPrompt: turnReminders },
19800
19994
  (event) => {
19801
19995
  this.emit({ type: "turn_output", threadId, event });
19802
19996
  if (event.type === "session_id") {