@sideboard-ai/core 0.1.46 → 0.1.49

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 (40) hide show
  1. package/dist/{agents-6L6VENDT.js → agents-3P4N6KHC.js} +1 -1
  2. package/dist/agents-LUB3Q773.js +82 -0
  3. package/dist/app-settings-7XVDQJ7F.js +80 -0
  4. package/dist/{chunk-SZAKJ4TR.js → chunk-3NAS4MWH.js} +1 -1
  5. package/dist/{chunk-SPCU3MFY.js → chunk-5WYEJ3F3.js} +3 -3
  6. package/dist/chunk-7MV3RXSC.js +277 -0
  7. package/dist/chunk-DF5VQQKA.js +191 -0
  8. package/dist/chunk-E35APBHV.js +269 -0
  9. package/dist/chunk-EOYDCKQC.js +172 -0
  10. package/dist/chunk-GZXBYHJC.js +148 -0
  11. package/dist/chunk-HBJSHRY2.js +494 -0
  12. package/dist/chunk-ISY4EGF6.js +77 -0
  13. package/dist/chunk-KGZBYWZ3.js +23 -0
  14. package/dist/chunk-RQI4TOXK.js +2232 -0
  15. package/dist/chunk-S2LL5HA4.js +33 -0
  16. package/dist/{chunk-TQK2OYPU.js → chunk-SS34TVSY.js} +1 -1
  17. package/dist/chunk-V4JRXYYU.js +122 -0
  18. package/dist/{chunk-6NAPN2N5.js → chunk-ZQCQIWIP.js} +1 -1
  19. package/dist/chunk-ZVV5EEQN.js +1892 -0
  20. package/dist/connected-teams-UBXHZGO4.js +24 -0
  21. package/dist/{coordinator-prompt-2XFYG3C5.js → coordinator-prompt-SPGDT7J5.js} +1 -1
  22. package/dist/coordinator-prompt-VG4BZ5JL.js +25 -0
  23. package/dist/cursor-recover-NNK7JPQM.js +44 -0
  24. package/dist/{global-workspace-XFOXEQCP.js → global-workspace-KSR3E63K.js} +2 -2
  25. package/dist/global-workspace-OJF4ENH4.js +40 -0
  26. package/dist/index.cjs +243 -19
  27. package/dist/index.d.cts +49 -10
  28. package/dist/index.d.ts +49 -10
  29. package/dist/index.js +5768 -192
  30. package/dist/mcp/run-stdio.cjs +203 -21
  31. package/dist/mcp/run-stdio.js +5618 -15
  32. package/dist/paths-2OFB7UJG.js +28 -0
  33. package/dist/run-HMRSRG3U.js +12 -0
  34. package/dist/thread-store-XICUWFNM.js +32 -0
  35. package/dist/title-4WAKWZRF.js +27 -0
  36. package/dist/workspaces-OZE7LRDO.js +24 -0
  37. package/dist/{workspaces-SRL2HZTB.js → workspaces-UJX7JIGH.js} +3 -3
  38. package/dist/worktree-XG5PCLZY.js +94 -0
  39. package/package.json +2 -2
  40. package/dist/chunk-7DTJFP4D.js +0 -5680
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ run
5
+ } from "./chunk-ISY4EGF6.js";
6
+ import {
7
+ appDataDir
8
+ } from "./chunk-7MV3RXSC.js";
9
+
10
+ // src/brightsy/connected-teams.ts
11
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
12
+ import { join as join2 } from "path";
13
+
14
+ // src/brightsy/config.ts
15
+ import { existsSync, readFileSync, writeFileSync } from "fs";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+ function brightsyConfigPath() {
19
+ return join(homedir(), ".brightsy", "config.json");
20
+ }
21
+ function loadBrightsyConfig() {
22
+ const path = brightsyConfigPath();
23
+ if (!existsSync(path)) {
24
+ throw new Error("Brightsy not logged in \u2014 run `brightsy login` first");
25
+ }
26
+ const raw = JSON.parse(readFileSync(path, "utf8"));
27
+ if (!raw.access_token || !raw.account_id) {
28
+ throw new Error("Brightsy config incomplete \u2014 run `brightsy login`");
29
+ }
30
+ return raw;
31
+ }
32
+ function saveBrightsyConfig(cfg) {
33
+ writeFileSync(brightsyConfigPath(), `${JSON.stringify(cfg, null, 2)}
34
+ `, {
35
+ mode: 384
36
+ });
37
+ }
38
+
39
+ // src/brightsy/accounts.ts
40
+ async function runBrightsyTeamsJson(args) {
41
+ const listed = await run("brightsy", ["teams", ...args, "--json"], {
42
+ reject: false
43
+ });
44
+ if (listed.exitCode !== 0) {
45
+ throw new Error(
46
+ listed.stderr.trim() || listed.stdout.trim() || "brightsy teams failed \u2014 is `@brightsy/cli` installed and logged in?"
47
+ );
48
+ }
49
+ const raw = listed.stdout.trim();
50
+ if (!raw) {
51
+ throw new Error("brightsy teams returned empty output");
52
+ }
53
+ try {
54
+ return JSON.parse(raw);
55
+ } catch {
56
+ throw new Error("brightsy teams returned invalid JSON");
57
+ }
58
+ }
59
+ async function listBrightsyAccounts() {
60
+ const data = await runBrightsyTeamsJson([]);
61
+ return Array.isArray(data.teams) ? data.teams : [];
62
+ }
63
+ async function performBrightsyAccountSwitch(accountIdOrSlug) {
64
+ const data = await runBrightsyTeamsJson([
65
+ "switch",
66
+ accountIdOrSlug
67
+ ]);
68
+ if (!data.active?.id) {
69
+ throw new Error(`Team switch failed for: ${accountIdOrSlug}`);
70
+ }
71
+ const cfg = loadBrightsyConfig();
72
+ if (data.active.slug && data.active.slug !== cfg.account_slug) {
73
+ cfg.account_slug = data.active.slug;
74
+ saveBrightsyConfig(cfg);
75
+ }
76
+ return { cfg, target: data.active };
77
+ }
78
+
79
+ // src/brightsy/connected-teams.ts
80
+ function storePath() {
81
+ return join2(appDataDir(), "brightsy-teams.json");
82
+ }
83
+ function readStore() {
84
+ const path = storePath();
85
+ if (!existsSync2(path)) return [];
86
+ try {
87
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
88
+ return Array.isArray(parsed.teams) ? parsed.teams : [];
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+ function writeStore(teams) {
94
+ mkdirSync(appDataDir(), { recursive: true });
95
+ const path = storePath();
96
+ writeFileSync2(path, `${JSON.stringify({ teams }, null, 2)}
97
+ `, {
98
+ mode: 384
99
+ });
100
+ return teams;
101
+ }
102
+ function listConnectedBrightsyTeams() {
103
+ return readStore().map(({ id, slug, name, expires_at }) => ({
104
+ id,
105
+ slug,
106
+ name,
107
+ expires_at
108
+ }));
109
+ }
110
+ function getConnectedBrightsyTeamsRaw() {
111
+ return readStore();
112
+ }
113
+ function applyConnectedTeamToCli(team) {
114
+ let base = {};
115
+ try {
116
+ base = loadBrightsyConfig();
117
+ } catch {
118
+ }
119
+ saveBrightsyConfig({
120
+ ...base,
121
+ access_token: team.access_token,
122
+ refresh_token: team.refresh_token ?? base.refresh_token,
123
+ expires_at: team.expires_at ?? base.expires_at,
124
+ account_id: team.id,
125
+ account_slug: team.slug,
126
+ endpoint: team.endpoint ?? base.endpoint ?? "https://brightsy.ai",
127
+ oauth_client_id: base.oauth_client_id
128
+ });
129
+ }
130
+ async function refreshTeamToken(team) {
131
+ if (!team.refresh_token) return team;
132
+ const endpoint = (team.endpoint || "https://brightsy.ai").replace(/\/$/, "");
133
+ const cfg = (() => {
134
+ try {
135
+ return loadBrightsyConfig();
136
+ } catch {
137
+ return null;
138
+ }
139
+ })();
140
+ const clientId = cfg?.oauth_client_id || "brightsy-cli";
141
+ const res = await fetch(`${endpoint}/oauth/token`, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
144
+ body: new URLSearchParams({
145
+ grant_type: "refresh_token",
146
+ refresh_token: team.refresh_token,
147
+ client_id: clientId
148
+ })
149
+ });
150
+ if (!res.ok) return team;
151
+ const data = await res.json();
152
+ if (!data.access_token) return team;
153
+ return {
154
+ ...team,
155
+ access_token: data.access_token,
156
+ refresh_token: data.refresh_token || team.refresh_token,
157
+ expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : team.expires_at
158
+ };
159
+ }
160
+ async function ensureConnectedBrightsyTeamTokens() {
161
+ const teams = readStore();
162
+ if (teams.length === 0) return [];
163
+ const next = [];
164
+ let changed = false;
165
+ for (const team of teams) {
166
+ const expired = typeof team.expires_at === "number" && Date.now() >= team.expires_at - 6e4;
167
+ if (!expired) {
168
+ next.push(team);
169
+ continue;
170
+ }
171
+ const refreshed = await refreshTeamToken(team);
172
+ if (refreshed.access_token !== team.access_token) changed = true;
173
+ next.push(refreshed);
174
+ }
175
+ if (changed) writeStore(next);
176
+ try {
177
+ const cfg = loadBrightsyConfig();
178
+ const active = next.find((t) => t.id === cfg.account_id);
179
+ if (active && active.access_token !== cfg.access_token) {
180
+ applyConnectedTeamToCli(active);
181
+ }
182
+ } catch {
183
+ }
184
+ return next;
185
+ }
186
+ function ensureCliTeamTracked(meta) {
187
+ try {
188
+ const cfg = loadBrightsyConfig();
189
+ const existing = readStore();
190
+ if (existing.some((t) => t.id === cfg.account_id)) {
191
+ return listConnectedBrightsyTeams();
192
+ }
193
+ const team = {
194
+ id: cfg.account_id,
195
+ slug: meta?.slug || cfg.account_slug || cfg.account_id,
196
+ name: meta?.name || cfg.account_slug || cfg.account_id,
197
+ access_token: cfg.access_token,
198
+ refresh_token: cfg.refresh_token,
199
+ expires_at: cfg.expires_at,
200
+ endpoint: cfg.endpoint
201
+ };
202
+ writeStore([...existing, team]);
203
+ } catch {
204
+ }
205
+ return listConnectedBrightsyTeams();
206
+ }
207
+ async function connectBrightsyTeam(accountIdOrSlug) {
208
+ const accounts = await listBrightsyAccounts();
209
+ const target = accounts.find((a) => a.id === accountIdOrSlug) ?? accounts.find((a) => a.slug === accountIdOrSlug);
210
+ if (!target) {
211
+ throw new Error(`Brightsy team not found: ${accountIdOrSlug}`);
212
+ }
213
+ const existing = readStore();
214
+ const already = existing.find((t) => t.id === target.id);
215
+ if (already) {
216
+ applyConnectedTeamToCli(already);
217
+ return listConnectedBrightsyTeams();
218
+ }
219
+ const { cfg: minted } = await performBrightsyAccountSwitch(target.id);
220
+ const team = {
221
+ id: target.id,
222
+ slug: target.slug,
223
+ name: target.name,
224
+ access_token: minted.access_token,
225
+ refresh_token: minted.refresh_token,
226
+ expires_at: minted.expires_at,
227
+ endpoint: minted.endpoint
228
+ };
229
+ writeStore([...existing, team]);
230
+ return listConnectedBrightsyTeams();
231
+ }
232
+ async function disconnectBrightsyTeam(accountIdOrSlug) {
233
+ const before = readStore();
234
+ const removed = before.find(
235
+ (t) => t.id === accountIdOrSlug || t.slug === accountIdOrSlug
236
+ );
237
+ const teams = before.filter(
238
+ (t) => t.id !== accountIdOrSlug && t.slug !== accountIdOrSlug
239
+ );
240
+ writeStore(teams);
241
+ if (removed) {
242
+ try {
243
+ const cfg = loadBrightsyConfig();
244
+ if (cfg.account_id === removed.id) {
245
+ if (teams[0]) {
246
+ applyConnectedTeamToCli(teams[0]);
247
+ }
248
+ }
249
+ } catch {
250
+ }
251
+ }
252
+ return listConnectedBrightsyTeams();
253
+ }
254
+ function brightsyMcpServerName(slug) {
255
+ const cleaned = slug.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+|_+$/g, "");
256
+ return `brightsy_${cleaned || "team"}`;
257
+ }
258
+
259
+ export {
260
+ loadBrightsyConfig,
261
+ listConnectedBrightsyTeams,
262
+ getConnectedBrightsyTeamsRaw,
263
+ applyConnectedTeamToCli,
264
+ ensureConnectedBrightsyTeamTokens,
265
+ ensureCliTeamTracked,
266
+ connectBrightsyTeam,
267
+ disconnectBrightsyTeam,
268
+ brightsyMcpServerName
269
+ };
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ normalizeThinkingEffort
5
+ } from "./chunk-KGZBYWZ3.js";
6
+ import {
7
+ threadFilePath,
8
+ threadLockPath,
9
+ threadsDir
10
+ } from "./chunk-7MV3RXSC.js";
11
+
12
+ // src/store/thread-store.ts
13
+ import { randomUUID } from "crypto";
14
+ import {
15
+ existsSync,
16
+ readFileSync,
17
+ renameSync,
18
+ unlinkSync,
19
+ writeFileSync,
20
+ readdirSync
21
+ } from "fs";
22
+ import lockfile from "proper-lockfile";
23
+ function nowIso() {
24
+ return (/* @__PURE__ */ new Date()).toISOString();
25
+ }
26
+ function resolveThreadEffort(raw) {
27
+ const fromField = normalizeThinkingEffort(raw.effort);
28
+ if (fromField) return fromField;
29
+ if (raw.fast) return "low";
30
+ return "high";
31
+ }
32
+ function normalizeThread(raw) {
33
+ return {
34
+ ...raw,
35
+ model: raw.model ?? null,
36
+ effort: resolveThreadEffort(raw),
37
+ fast: Boolean(raw.fast),
38
+ planMode: Boolean(raw.planMode),
39
+ autonomy: raw.autonomy ?? "default",
40
+ lastError: raw.lastError ?? null,
41
+ agentPid: raw.agentPid ?? null,
42
+ attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
43
+ prTitle: raw.prTitle ?? null,
44
+ userSetTitle: Boolean(raw.userSetTitle),
45
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
46
+ quotaResumeAt: raw.quotaResumeAt ?? null,
47
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
48
+ };
49
+ }
50
+ function createEmptyThread(partial) {
51
+ const ts = nowIso();
52
+ return {
53
+ id: randomUUID(),
54
+ sessionId: partial.sessionId ?? null,
55
+ autonomy: partial.autonomy ?? "default",
56
+ model: partial.model ?? null,
57
+ effort: partial.effort ?? "high",
58
+ fast: partial.fast ?? false,
59
+ planMode: partial.planMode ?? false,
60
+ sourceIsFork: partial.sourceIsFork ?? false,
61
+ status: partial.status ?? "idle",
62
+ queue: partial.queue ?? [],
63
+ parentThreadId: partial.parentThreadId ?? null,
64
+ devPort: partial.devPort ?? null,
65
+ activeRuns: partial.activeRuns ?? [],
66
+ prUrl: partial.prUrl ?? null,
67
+ prTitle: partial.prTitle ?? null,
68
+ userSetTitle: partial.userSetTitle ?? false,
69
+ messages: partial.messages ?? [],
70
+ attachments: partial.attachments ?? [],
71
+ createdAt: ts,
72
+ updatedAt: ts,
73
+ title: partial.title,
74
+ sourceType: partial.sourceType,
75
+ sourceRef: partial.sourceRef,
76
+ branchName: partial.branchName,
77
+ worktreePath: partial.worktreePath,
78
+ repoPath: partial.repoPath,
79
+ agent: partial.agent,
80
+ lastError: null,
81
+ agentPid: null
82
+ };
83
+ }
84
+ async function withThreadLock(id, fn) {
85
+ const lockPath = threadLockPath(id);
86
+ writeFileSync(lockPath, "", { flag: "a" });
87
+ let release;
88
+ try {
89
+ release = await lockfile.lock(lockPath, {
90
+ retries: { retries: 10, minTimeout: 50, maxTimeout: 200 },
91
+ stale: 6e4
92
+ });
93
+ return await fn();
94
+ } finally {
95
+ if (release) await release();
96
+ }
97
+ }
98
+ function readThread(id) {
99
+ const path = threadFilePath(id);
100
+ if (!existsSync(path)) return null;
101
+ const raw = readFileSync(path, "utf8");
102
+ return normalizeThread(JSON.parse(raw));
103
+ }
104
+ function writeThread(thread) {
105
+ const path = threadFilePath(idPath(thread.id));
106
+ const tmp = `${path}.${process.pid}.tmp`;
107
+ const next = { ...thread, updatedAt: nowIso() };
108
+ writeFileSync(tmp, JSON.stringify(next, null, 2), "utf8");
109
+ renameSync(tmp, path);
110
+ }
111
+ function idPath(id) {
112
+ return id;
113
+ }
114
+ function listThreads(opts) {
115
+ const files = readdirSync(threadsDir()).filter((f) => f.endsWith(".json"));
116
+ const threads = files.map((f) => {
117
+ try {
118
+ return normalizeThread(
119
+ JSON.parse(readFileSync(threadFilePath(f.replace(/\.json$/, "")), "utf8"))
120
+ );
121
+ } catch {
122
+ return null;
123
+ }
124
+ }).filter((t) => t !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
125
+ if (opts?.includeArchived) return threads;
126
+ return threads.filter((t) => t.status !== "archived");
127
+ }
128
+ function deleteThreadRecord(id) {
129
+ const path = threadFilePath(id);
130
+ if (existsSync(path)) unlinkSync(path);
131
+ const lock = threadLockPath(id);
132
+ if (existsSync(lock)) {
133
+ try {
134
+ unlinkSync(lock);
135
+ } catch {
136
+ }
137
+ }
138
+ }
139
+ function updateThread(id, patch) {
140
+ const current = readThread(id);
141
+ if (!current) throw new Error(`Thread not found: ${id}`);
142
+ const next = { ...current, ...patch, id: current.id, updatedAt: nowIso() };
143
+ writeThread(next);
144
+ return next;
145
+ }
146
+ function appendMessage(id, message) {
147
+ const current = readThread(id);
148
+ if (!current) throw new Error(`Thread not found: ${id}`);
149
+ return updateThread(id, { messages: [...current.messages, message] });
150
+ }
151
+ function setStatus(id, status, lastError) {
152
+ return updateThread(id, { status, lastError: lastError ?? null });
153
+ }
154
+ function findThreadByRef(ref) {
155
+ const all = listThreads({ includeArchived: true });
156
+ return all.find((t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref) ?? null;
157
+ }
158
+
159
+ export {
160
+ resolveThreadEffort,
161
+ normalizeThread,
162
+ createEmptyThread,
163
+ withThreadLock,
164
+ readThread,
165
+ writeThread,
166
+ listThreads,
167
+ deleteThreadRecord,
168
+ updateThread,
169
+ appendMessage,
170
+ setStatus,
171
+ findThreadByRef
172
+ };
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ resolveGithubRepoSlug
5
+ } from "./chunk-ZVV5EEQN.js";
6
+ import {
7
+ globalAgentCwd,
8
+ sideboardReposDir
9
+ } from "./chunk-7MV3RXSC.js";
10
+
11
+ // src/orchestrator/coordinator-prompt.ts
12
+ import { mkdirSync, writeFileSync } from "fs";
13
+ import { join } from "path";
14
+ function formatWorkspaceInventory(workspaces) {
15
+ if (workspaces.length === 0) return "(no registered workspaces)";
16
+ return workspaces.map((w) => {
17
+ const slug = w.githubSlug?.trim() ? ` github:${w.githubSlug.trim()}` : "";
18
+ return `- ${w.name}: ${w.path}${slug}`;
19
+ }).join("\n");
20
+ }
21
+ async function enrichWorkspacesWithGithub(workspaces) {
22
+ return Promise.all(
23
+ workspaces.map(async (w) => {
24
+ const githubSlug = await resolveGithubRepoSlug(w.path).catch(() => null);
25
+ return { ...w, githubSlug };
26
+ })
27
+ );
28
+ }
29
+ function coordinatorGreenfieldPlaybook(reposDir) {
30
+ return [
31
+ "Greenfield (new app / new GitHub repo) \u2014 use Bash + MCP:",
32
+ `- Create or clone under \`${reposDir}/<name>\` (never inside this synthetic home cwd).`,
33
+ "- Examples:",
34
+ ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
35
+ ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
36
+ "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn \u2192 send_to_thread (`gh pr create --draft -R <origin-owner/name>`).",
37
+ "- Always target the child worktree's **origin** (`github:` slug from list_workspaces / `git remote get-url origin` in that worktree). Never open PRs against `upstream`.",
38
+ "- Do coding work in the child worktree thread, not by editing files in this home cwd."
39
+ ].join("\n");
40
+ }
41
+ var COORDINATOR_TOOL_PLAYBOOK = [
42
+ "Role: you oversee Sideboard worktree agents across registered repos. You do not live inside one of those worktrees.",
43
+ "Sideboard MCP (fleet control \u2014 prefer these for status and orchestration):",
44
+ "Discover:",
45
+ "- list_workspaces \u2014 registered repos (path + github slug when known)",
46
+ "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
47
+ "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
48
+ "- list_threads / get_thread \u2014 fleet status (what is going on)",
49
+ "Workspaces:",
50
+ "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
51
+ "Worktree threads (chats):",
52
+ "- create_thread \u2014 create a worktree + chat from branch | pr | ticket; pass repoPath + parentThreadId",
53
+ "- fork_worktree \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
54
+ "- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
55
+ "- send_to_thread \u2014 queue a prompt (start/continue a chat turn); pass force_stop: true to interrupt mid-turn / clear stale queued prompts before replacing with a new request",
56
+ "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
57
+ "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
58
+ "- archive_thread / restore_thread \u2014 archive (tears down worktree when last tab) or restore",
59
+ "Setup / run:",
60
+ "- run_setup \u2014 re-run worktree setup",
61
+ "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
62
+ "Inspect / review / PRs:",
63
+ "- get_diff \u2014 compact diff summary",
64
+ '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review."); then wait_for_turn / get_turn_result on the returned id',
65
+ "- Ask the worktree agent via send_to_thread to open a draft PR with `gh pr create --draft -R <origin-owner/name>` (workspace `github:` slug / that worktree's origin \u2014 never upstream). Do not open PRs from the orchestrator yourself.",
66
+ "Human-only (do not attempt): merge, ready-for-review land, purge_thread.",
67
+ "Thread links in replies: when mentioning a chat/thread for the user, include a markdown link `[Title](sideboard://thread/<id>)` using the full id (or the link field from create_thread / list_threads). Sideboard renders these as clickable opens.",
68
+ "Bash / Read / etc: allowed for (1) inspecting target worktrees / registered repo paths from MCP, and (2) greenfield setup under ~/sideboard/repos (git clone, gh repo create, git init+remote). Never git init/clone *inside* this synthetic home cwd \u2014 emptiness here is expected, not a bug."
69
+ ].join("\n");
70
+ function coordinatorTurnReminder(opts) {
71
+ const goal = opts.goal?.trim();
72
+ return [
73
+ "Sideboard Orchestration (mandatory):",
74
+ "- You oversee Sideboard worktree agents. You are not yourself checked out in a project worktree.",
75
+ "- This cwd is a synthetic empty home (not a git repo). Emptiness here is expected \u2014 it is not a problem to fix.",
76
+ "- Registered workspaces / child threads are the fleet you manage via Sideboard MCP.",
77
+ `- Parent thread id (pass as parentThreadId when creating children): ${opts.parentId}`,
78
+ goal ? `- Goal / title: ${goal}` : null,
79
+ `- For "what's going on": call list_threads (and list_workspaces if needed). Summarize fleet status \u2014 do not ls/git-status this synthetic home.`,
80
+ "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn.",
81
+ "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 draft PR.",
82
+ "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`."
83
+ ].filter(Boolean).join("\n");
84
+ }
85
+ function ensureGlobalCoordinatorCwd() {
86
+ const dir = globalAgentCwd();
87
+ mkdirSync(dir, { recursive: true });
88
+ const reposDir = sideboardReposDir();
89
+ const body = [
90
+ "# Sideboard Orchestration",
91
+ "",
92
+ "You are the Sideboard **Orchestration** agent \u2014 you oversee worktree agents in the Sideboard app.",
93
+ "You are **not** connected to a single project workspace. This directory is a synthetic empty cwd (not a git worktree).",
94
+ "It being empty / not a git repo is **normal**. Do not initialize git here or ask the user to point you at a repo for *your* checkout.",
95
+ "Repos from `list_workspaces` and threads from `list_threads` are the fleet you orchestrate.",
96
+ 'For status questions ("what\'s going on?"), use `list_threads` / `list_workspaces` \u2014 never diagnose this synthetic home as a broken worktree.',
97
+ "Bash is fine for inspecting **child worktree** / registered-repo paths, and for greenfield repo setup under the Sideboard repos directory \u2014 not for treating this home as the project.",
98
+ "",
99
+ COORDINATOR_TOOL_PLAYBOOK,
100
+ "",
101
+ coordinatorGreenfieldPlaybook(reposDir),
102
+ "",
103
+ "When creating threads, pass `repoPath` from `list_workspaces` (or the path you just registered) and `parentThreadId` for children.",
104
+ "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn.",
105
+ "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 \u2192 draft PR via worktree agent.",
106
+ "Always ask worktree agents to open draft PRs (`send_to_thread` + `gh pr create --draft -R <origin>`); never open PRs from the orchestrator."
107
+ ].join("\n");
108
+ writeFileSync(join(dir, "CLAUDE.md"), `${body}
109
+ `, "utf8");
110
+ writeFileSync(join(dir, "AGENTS.md"), `${body}
111
+ `, "utf8");
112
+ return dir;
113
+ }
114
+ function coordinatorSystemPrompt(opts) {
115
+ const audience = opts.audience ?? "cloud";
116
+ const reposDir = sideboardReposDir();
117
+ const intro = audience === "cloud" ? [
118
+ "You are a Sideboard coordinator responding to a request from a Brightsy cloud agent (Slack, Discord, Teams, or other chat).",
119
+ "Your reply will be sent back to that cloud agent \u2014 be concise and actionable."
120
+ ] : [
121
+ "You are a Sideboard orchestration agent: you oversee worktree agents across registered workspaces in the Sideboard app.",
122
+ "Stay concise and actionable; prefer Sideboard MCP for fleet status; use Bash for greenfield repo setup and inspecting target worktree paths."
123
+ ];
124
+ return [
125
+ ...intro,
126
+ "You operate across ALL registered workspaces below.",
127
+ "You have no project git home \u2014 this process cwd is synthetic and empty on purpose.",
128
+ COORDINATOR_TOOL_PLAYBOOK,
129
+ coordinatorGreenfieldPlaybook(reposDir),
130
+ "When creating threads, pass the correct repoPath for the target workspace and parentThreadId for children.",
131
+ "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn \u2192 send_to_thread (ask for `gh pr create --draft -R <origin-owner/name>` using the workspace github slug) \u2192 wait_for_turn. Never target upstream. Never open PRs from the orchestrator.",
132
+ "Typical flow (new app): Bash under repos dir (clone or gh repo create) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 draft PR via worktree agent.",
133
+ `Goal: ${opts.goal}`,
134
+ `Parent thread id (pass as parentThreadId when creating children): ${opts.parentId}`,
135
+ "Registered workspaces:",
136
+ formatWorkspaceInventory(opts.workspaces)
137
+ ].join("\n");
138
+ }
139
+
140
+ export {
141
+ formatWorkspaceInventory,
142
+ enrichWorkspacesWithGithub,
143
+ coordinatorGreenfieldPlaybook,
144
+ COORDINATOR_TOOL_PLAYBOOK,
145
+ coordinatorTurnReminder,
146
+ ensureGlobalCoordinatorCwd,
147
+ coordinatorSystemPrompt
148
+ };