@sideboard-ai/core 0.1.194 → 0.1.196

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2956,762 +2956,10 @@ var init_thread_store = __esm({
2956
2956
  }
2957
2957
  });
2958
2958
 
2959
- // src/store/setup-log.ts
2960
- function setupLogPath(threadId) {
2961
- return (0, import_node_path11.join)(threadsDir(), `${threadId}.setup.log.json`);
2962
- }
2963
- function emptySetupLog() {
2964
- return { ...EMPTY };
2965
- }
2966
- function mergeSetupOutput(prev, incoming) {
2967
- if (!prev) return incoming;
2968
- if (!incoming) return prev;
2969
- if (prev === incoming) return prev;
2970
- if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
2971
- if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
2972
- return incoming.length >= prev.length ? incoming : prev;
2973
- }
2974
- function clone(snap) {
2975
- return { ...snap };
2976
- }
2977
- function persistNow(threadId) {
2978
- const timer = persistTimers.get(threadId);
2979
- if (timer) {
2980
- clearTimeout(timer);
2981
- persistTimers.delete(threadId);
2982
- }
2983
- const snap = memory.get(threadId);
2984
- if (!snap) return;
2985
- try {
2986
- (0, import_node_fs10.writeFileSync)(setupLogPath(threadId), `${JSON.stringify(snap)}
2987
- `, "utf8");
2988
- } catch {
2989
- }
2990
- }
2991
- function schedulePersist(threadId) {
2992
- if (persistTimers.has(threadId)) return;
2993
- persistTimers.set(
2994
- threadId,
2995
- setTimeout(() => {
2996
- persistTimers.delete(threadId);
2997
- persistNow(threadId);
2998
- }, 100)
2999
- );
3000
- }
3001
- function loadFromDisk(threadId) {
3002
- const path2 = setupLogPath(threadId);
3003
- if (!(0, import_node_fs10.existsSync)(path2)) return null;
3004
- try {
3005
- const raw = JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
3006
- return {
3007
- output: typeof raw.output === "string" ? raw.output : "",
3008
- running: Boolean(raw.running),
3009
- exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
3010
- source: typeof raw.source === "string" ? raw.source : null
3011
- };
3012
- } catch {
3013
- return null;
3014
- }
3015
- }
3016
- function readSetupLog(threadId) {
3017
- const cached = memory.get(threadId);
3018
- if (cached) return clone(cached);
3019
- const disk = loadFromDisk(threadId);
3020
- if (disk) {
3021
- memory.set(threadId, disk);
3022
- return clone(disk);
3023
- }
3024
- return emptySetupLog();
3025
- }
3026
- function beginSetupLog(threadId) {
3027
- const snap = {
3028
- output: "",
3029
- running: true,
3030
- exitCode: null,
3031
- source: null
3032
- };
3033
- memory.set(threadId, snap);
3034
- persistNow(threadId);
3035
- return clone(snap);
3036
- }
3037
- function appendSetupLog(threadId, line) {
3038
- const current = memory.get(threadId) ?? readSetupLog(threadId);
3039
- const snap = {
3040
- ...current,
3041
- output: current.output ? `${current.output}
3042
- ${line}` : line,
3043
- running: true
3044
- };
3045
- memory.set(threadId, snap);
3046
- schedulePersist(threadId);
3047
- return clone(snap);
3048
- }
3049
- function finishSetupLog(threadId, exitCode, source) {
3050
- const current = memory.get(threadId) ?? readSetupLog(threadId);
3051
- const snap = {
3052
- ...current,
3053
- running: false,
3054
- exitCode,
3055
- source: source ?? current.source
3056
- };
3057
- memory.set(threadId, snap);
3058
- persistNow(threadId);
3059
- return clone(snap);
3060
- }
3061
- var import_node_fs10, import_node_path11, EMPTY, memory, persistTimers;
3062
- var init_setup_log = __esm({
3063
- "src/store/setup-log.ts"() {
3064
- "use strict";
3065
- import_node_fs10 = require("fs");
3066
- import_node_path11 = require("path");
3067
- init_paths();
3068
- EMPTY = {
3069
- output: "",
3070
- running: false,
3071
- exitCode: null,
3072
- source: null
3073
- };
3074
- memory = /* @__PURE__ */ new Map();
3075
- persistTimers = /* @__PURE__ */ new Map();
3076
- }
3077
- });
3078
-
3079
- // src/store/schedules.ts
3080
- function schedulesPath() {
3081
- return (0, import_node_path12.join)(appDataDir(), "schedules.json");
3082
- }
3083
- function parseDurationMs(every) {
3084
- const m = every.trim().match(DURATION_RE);
3085
- if (!m) return null;
3086
- const n = Number.parseInt(m[1], 10);
3087
- if (!Number.isFinite(n) || n <= 0) return null;
3088
- const unit = m[2].toLowerCase();
3089
- return n * UNIT_MS[unit];
3090
- }
3091
- function defaultScheduleName(prompt) {
3092
- const line = prompt.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
3093
- if (!line) return "Untitled schedule";
3094
- return line.length > 60 ? `${line.slice(0, 57).trimEnd()}\u2026` : line;
3095
- }
3096
- function resolveScheduleThreadId(raw, env = process.env) {
3097
- const value = raw?.trim() || "";
3098
- if (!value) return null;
3099
- if (value.toLowerCase() === "self") {
3100
- return env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
3101
- }
3102
- return value;
3103
- }
3104
- function computeNextRunAt(when, from = /* @__PURE__ */ new Date()) {
3105
- if (when.kind === "once") {
3106
- const at = new Date(when.at);
3107
- if (Number.isNaN(at.getTime())) {
3108
- throw new Error(`Invalid once-at datetime: ${when.at}`);
3109
- }
3110
- return at;
3111
- }
3112
- if (when.kind === "every") {
3113
- const ms = parseDurationMs(when.every);
3114
- if (!ms) {
3115
- throw new Error(`Invalid interval: ${when.every} (use 15m, 1h, 6h, 1d)`);
3116
- }
3117
- return new Date(from.getTime() + ms);
3118
- }
3119
- try {
3120
- const job = new import_croner.Cron(when.expr, {
3121
- timezone: when.tz?.trim() || void 0,
3122
- paused: true
3123
- });
3124
- const next = job.nextRun(from);
3125
- if (!next) {
3126
- throw new Error(`Cron expression has no next run: ${when.expr}`);
3127
- }
3128
- return next;
3129
- } catch (err) {
3130
- const message = err instanceof Error ? err.message : String(err);
3131
- throw new Error(`Invalid cron: ${message}`);
3132
- }
3133
- }
3134
- function formatScheduleWhen(when) {
3135
- if (when.kind === "once") return `once at ${when.at}`;
3136
- if (when.kind === "every") return `every ${when.every}`;
3137
- const tz = when.tz?.trim();
3138
- return tz ? `cron ${when.expr} (${tz})` : `cron ${when.expr}`;
3139
- }
3140
- function nowIso2(d = /* @__PURE__ */ new Date()) {
3141
- return d.toISOString();
3142
- }
3143
- function normalizeWhen(raw) {
3144
- if (!raw || typeof raw !== "object") {
3145
- throw new Error("Schedule is missing `when`");
3146
- }
3147
- const rec = raw;
3148
- const kind = rec.kind;
3149
- if (kind === "once" && typeof rec.at === "string") {
3150
- return { kind: "once", at: rec.at };
3151
- }
3152
- if (kind === "every" && typeof rec.every === "string") {
3153
- return { kind: "every", every: rec.every };
3154
- }
3155
- if (kind === "cron" && typeof rec.expr === "string") {
3156
- return {
3157
- kind: "cron",
3158
- expr: rec.expr,
3159
- tz: typeof rec.tz === "string" && rec.tz.trim() ? rec.tz.trim() : void 0
3160
- };
3161
- }
3162
- throw new Error("Schedule `when` must be once, every, or cron");
3163
- }
3164
- function normalizeTask(raw) {
3165
- const when = normalizeWhen(raw.when);
3166
- const agent = raw.agent && isOrchestratorCapableAgent(raw.agent) ? raw.agent : null;
3167
- return {
3168
- id: raw.id,
3169
- name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : defaultScheduleName(raw.prompt),
3170
- prompt: String(raw.prompt ?? ""),
3171
- enabled: Boolean(raw.enabled),
3172
- when,
3173
- threadId: raw.threadId?.trim() || null,
3174
- agent,
3175
- model: raw.model?.trim() || null,
3176
- nextRunAt: raw.nextRunAt,
3177
- lastRunAt: raw.lastRunAt ?? null,
3178
- lastThreadId: raw.lastThreadId ?? null,
3179
- lastError: raw.lastError ?? null,
3180
- createdBy: raw.createdBy === "mcp" || raw.createdBy === "cli" || raw.createdBy === "ui" ? raw.createdBy : "ui",
3181
- createdAt: raw.createdAt,
3182
- updatedAt: raw.updatedAt
3183
- };
3184
- }
3185
- function readAll() {
3186
- const path2 = schedulesPath();
3187
- if (!(0, import_node_fs11.existsSync)(path2)) return [];
3188
- try {
3189
- const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
3190
- if (!Array.isArray(parsed)) return [];
3191
- return parsed.map((row) => {
3192
- try {
3193
- return normalizeTask(row);
3194
- } catch {
3195
- return null;
3196
- }
3197
- }).filter((row) => Boolean(row?.id && row.prompt));
3198
- } catch {
3199
- return [];
3200
- }
3201
- }
3202
- function writeAll(rows) {
3203
- writePrivateFile(schedulesPath(), `${JSON.stringify(rows, null, 2)}
3204
- `);
3205
- }
3206
- function buildTask(input, now) {
3207
- const prompt = input.prompt.trim();
3208
- if (!prompt) throw new Error("Schedule prompt is required");
3209
- const when = normalizeWhen(input.when);
3210
- const nextRunAt = computeNextRunAt(when, now).toISOString();
3211
- const ts = nowIso2(now);
3212
- const agent = input.agent ? coerceOrchestratorAgent(input.agent) : null;
3213
- return {
3214
- id: (0, import_node_crypto4.randomUUID)(),
3215
- name: input.name?.trim() || defaultScheduleName(prompt),
3216
- prompt,
3217
- enabled: input.enabled !== false,
3218
- when,
3219
- threadId: input.threadId?.trim() || null,
3220
- agent,
3221
- model: input.model?.trim() || null,
3222
- nextRunAt,
3223
- lastRunAt: null,
3224
- lastThreadId: null,
3225
- lastError: null,
3226
- createdBy: input.createdBy,
3227
- createdAt: ts,
3228
- updatedAt: ts
3229
- };
3230
- }
3231
- function listSchedules() {
3232
- return readAll().sort((a, b) => a.nextRunAt.localeCompare(b.nextRunAt));
3233
- }
3234
- function hasEnabledSchedules() {
3235
- return listSchedules().some((row) => row.enabled);
3236
- }
3237
- function getSchedule(id) {
3238
- const key = id.trim();
3239
- if (!key) return null;
3240
- return readAll().find(
3241
- (row) => row.id === key || row.id.startsWith(key) && key.length >= 8
3242
- ) ?? null;
3243
- }
3244
- function createSchedule(input) {
3245
- const task = buildTask(input, /* @__PURE__ */ new Date());
3246
- const rows = readAll();
3247
- rows.push(task);
3248
- writeAll(rows);
3249
- return task;
3250
- }
3251
- function updateSchedule(id, patch) {
3252
- const rows = readAll();
3253
- const idx = rows.findIndex((row) => row.id === id || id.length >= 8 && row.id.startsWith(id));
3254
- if (idx < 0) throw new Error(`Schedule not found: ${id}`);
3255
- const prev = rows[idx];
3256
- const when = patch.when ? normalizeWhen(patch.when) : prev.when;
3257
- const prompt = patch.prompt !== void 0 ? patch.prompt.trim() : prev.prompt;
3258
- if (!prompt) throw new Error("Schedule prompt is required");
3259
- const whenChanged = patch.when !== void 0;
3260
- const next = {
3261
- ...prev,
3262
- name: patch.name !== void 0 ? patch.name.trim() || defaultScheduleName(prompt) : prev.name,
3263
- prompt,
3264
- enabled: patch.enabled ?? prev.enabled,
3265
- when,
3266
- threadId: patch.threadId !== void 0 ? patch.threadId?.trim() || null : prev.threadId,
3267
- agent: patch.agent !== void 0 ? patch.agent ? coerceOrchestratorAgent(patch.agent) : null : prev.agent,
3268
- model: patch.model !== void 0 ? patch.model?.trim() || null : prev.model,
3269
- nextRunAt: whenChanged ? computeNextRunAt(when).toISOString() : prev.nextRunAt,
3270
- lastError: whenChanged ? null : prev.lastError,
3271
- updatedAt: nowIso2()
3272
- };
3273
- rows[idx] = next;
3274
- writeAll(rows);
3275
- return next;
3276
- }
3277
- function deleteSchedule(id) {
3278
- const rows = readAll();
3279
- const next = rows.filter(
3280
- (row) => row.id !== id && !(id.length >= 8 && row.id.startsWith(id))
3281
- );
3282
- if (next.length === rows.length) throw new Error(`Schedule not found: ${id}`);
3283
- writeAll(next);
3284
- }
3285
- function recordScheduleRun(id, result) {
3286
- const rows = readAll();
3287
- const idx = rows.findIndex((row) => row.id === id);
3288
- if (idx < 0) throw new Error(`Schedule not found: ${id}`);
3289
- const prev = rows[idx];
3290
- const firedAt = result.firedAt ?? /* @__PURE__ */ new Date();
3291
- const next = {
3292
- ...prev,
3293
- enabled: prev.when.kind === "once" ? false : prev.enabled,
3294
- nextRunAt: prev.when.kind === "once" ? prev.nextRunAt : computeNextRunAt(prev.when, firedAt).toISOString(),
3295
- lastRunAt: nowIso2(firedAt),
3296
- lastThreadId: result.lastThreadId !== void 0 ? result.lastThreadId : prev.lastThreadId,
3297
- lastError: result.lastError ?? null,
3298
- updatedAt: nowIso2(firedAt)
3299
- };
3300
- rows[idx] = next;
3301
- writeAll(rows);
3302
- return next;
3303
- }
3304
- var import_node_crypto4, import_node_fs11, import_node_path12, import_croner, DURATION_RE, UNIT_MS;
3305
- var init_schedules = __esm({
3306
- "src/store/schedules.ts"() {
3307
- "use strict";
3308
- import_node_crypto4 = require("crypto");
3309
- import_node_fs11 = require("fs");
3310
- import_node_path12 = require("path");
3311
- import_croner = require("croner");
3312
- init_orchestrator_capable();
3313
- init_paths();
3314
- init_private_file();
3315
- DURATION_RE = /^(\d+)(s|m|h|d)$/i;
3316
- UNIT_MS = {
3317
- s: 1e3,
3318
- m: 6e4,
3319
- h: 36e5,
3320
- d: 864e5
3321
- };
3322
- }
3323
- });
3324
-
3325
- // src/store/desktop-host.ts
3326
- function desktopHostPidPath() {
3327
- return (0, import_node_path13.join)(appDataDir(), "desktop-host.pid");
3328
- }
3329
- function claimDesktopHost(pid = process.pid) {
3330
- (0, import_node_fs12.writeFileSync)(desktopHostPidPath(), `${pid}
3331
- `, "utf8");
3332
- }
3333
- function releaseDesktopHost(pid = process.pid) {
3334
- if (readDesktopHostPid() !== pid) return;
3335
- try {
3336
- (0, import_node_fs12.unlinkSync)(desktopHostPidPath());
3337
- } catch {
3338
- }
3339
- }
3340
- function readDesktopHostPid() {
3341
- try {
3342
- const pid = Number.parseInt((0, import_node_fs12.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
3343
- if (!Number.isFinite(pid) || pid <= 0) return null;
3344
- return pid;
3345
- } catch {
3346
- return null;
3347
- }
3348
- }
3349
- function pidAlive(pid) {
3350
- try {
3351
- process.kill(pid, 0);
3352
- return true;
3353
- } catch {
3354
- return false;
3355
- }
3356
- }
3357
- function isDesktopHostAlive() {
3358
- const pid = readDesktopHostPid();
3359
- return pid != null && pidAlive(pid);
3360
- }
3361
- function isThisProcessDesktopHost() {
3362
- return readDesktopHostPid() === process.pid && pidAlive(process.pid);
3363
- }
3364
- function thisProcessShouldDrainAgentQueues() {
3365
- if (isThisProcessDesktopHost()) return true;
3366
- return !isDesktopHostAlive();
3367
- }
3368
- var import_node_fs12, import_node_path13;
3369
- var init_desktop_host = __esm({
3370
- "src/store/desktop-host.ts"() {
3371
- "use strict";
3372
- import_node_fs12 = require("fs");
3373
- import_node_path13 = require("path");
3374
- init_paths();
3375
- }
3376
- });
3377
-
3378
- // src/brightsy/cloud-connect-constants.ts
3379
- function parseForceStopMessage(message) {
3380
- const normalized = message.replace(/\r\n/g, "\n");
3381
- const nl = normalized.indexOf("\n");
3382
- const firstLine = (nl === -1 ? normalized : normalized.slice(0, nl)).trim();
3383
- const remainder = (nl === -1 ? "" : normalized.slice(nl + 1)).trim();
3384
- const forceStop = firstLine.toLowerCase() === SIDEBOARD_FORCE_STOP.toLowerCase();
3385
- return { forceStop, remainder: forceStop ? remainder : message };
3386
- }
3387
- var CLOUD_ORCHESTRATOR_GOAL, SIDEBOARD_FORCE_STOP, CLOUD_COORDINATOR_BUSY_REPLY, CLOUD_COORDINATOR_STOPPED_REPLY, CLOUD_COORDINATOR_TIMEOUT_REPLY;
3388
- var init_cloud_connect_constants = __esm({
3389
- "src/brightsy/cloud-connect-constants.ts"() {
3390
- "use strict";
3391
- CLOUD_ORCHESTRATOR_GOAL = "Cloud-connected Sideboard orchestrator";
3392
- SIDEBOARD_FORCE_STOP = "SIDEBOARD_FORCE_STOP";
3393
- CLOUD_COORDINATOR_BUSY_REPLY = [
3394
- "Sideboard is busy with an in-progress client/desktop tool turn on the global coordinator.",
3395
- "I did not start a new orchestration turn.",
3396
- `To force-stop the in-progress turn, send another desktop request whose first line is exactly ${SIDEBOARD_FORCE_STOP}`,
3397
- "(optional follow-up request on later lines).",
3398
- "What do you want me to do? (retry later, wait until idle, force-stop, rephrase, or cancel)"
3399
- ].join(" ");
3400
- CLOUD_COORDINATOR_STOPPED_REPLY = [
3401
- "Sideboard force-stopped the in-progress global coordinator turn.",
3402
- "No new orchestration request was started."
3403
- ].join(" ");
3404
- CLOUD_COORDINATOR_TIMEOUT_REPLY = [
3405
- "Sideboard global coordinator timed out waiting for the local agent turn to finish.",
3406
- "I did not produce a full orchestration result.",
3407
- `To force-stop the in-progress turn, send another desktop request whose first line is exactly ${SIDEBOARD_FORCE_STOP}`,
3408
- "(optional follow-up request on later lines).",
3409
- "What do you want me to do? (retry later, wait, force-stop, rephrase, or cancel)"
3410
- ].join(" ");
3411
- }
3412
- });
3413
-
3414
- // src/paths/workspace-scratch.ts
3415
- function attachmentsGitignoreBody() {
3416
- return ATTACHMENTS_GITIGNORE;
3417
- }
3418
- function isWorkspaceScratchPath(relativePath) {
3419
- const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
3420
- return p === ATTACHMENTS_DIR || p.startsWith(`${ATTACHMENTS_DIR}/`) || p === CLI_DIR || p.startsWith(`${CLI_DIR}/`) || p === LEGACY_ATTACHMENTS_DIR || p.startsWith(`${LEGACY_ATTACHMENTS_DIR}/`) || p === ".sideboard" || p.startsWith(".sideboard/") || p === ".context" || p.startsWith(".context/");
3421
- }
3422
- var ATTACHMENTS_DIR, CLI_DIR, DETACHED_JOBS_DIR, LEGACY_ATTACHMENTS_DIR, LEGACY_DETACHED_JOBS_DIR, ATTACHMENTS_GITIGNORE;
3423
- var init_workspace_scratch = __esm({
3424
- "src/paths/workspace-scratch.ts"() {
3425
- "use strict";
3426
- ATTACHMENTS_DIR = ".context/attachments";
3427
- CLI_DIR = ".context/cli";
3428
- DETACHED_JOBS_DIR = ".context/.sideboard/detached-jobs";
3429
- LEGACY_ATTACHMENTS_DIR = ".sideboard/attachments";
3430
- LEGACY_DETACHED_JOBS_DIR = ".sideboard/detached-jobs";
3431
- ATTACHMENTS_GITIGNORE = `# Sideboard / workspace attachments (local only)
3432
- *
3433
- !.gitignore
3434
- `;
3435
- }
3436
- });
3437
-
3438
- // src/composer/stage-files.ts
3439
- function fileExtension(filePath) {
3440
- const base = (0, import_node_path14.basename)(filePath).toLowerCase();
3441
- return base.includes(".") ? base.split(".").pop() || "" : "";
3442
- }
3443
- function isImageFilePath(filePath) {
3444
- return IMAGE_EXTENSIONS.has(fileExtension(filePath));
3445
- }
3446
- function imageMimeType(filePath) {
3447
- return IMAGE_MIME_BY_EXT[fileExtension(filePath)] || "image/png";
3448
- }
3449
- function ensureAttachmentsDir(worktreePath) {
3450
- const dir = (0, import_node_path14.join)(worktreePath, ATTACHMENTS_DIR);
3451
- (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
3452
- const gi = (0, import_node_path14.join)(dir, ".gitignore");
3453
- if (!(0, import_node_fs13.existsSync)(gi)) {
3454
- (0, import_node_fs13.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
3455
- }
3456
- return dir;
3457
- }
3458
- function uniqueAttachmentName(dir, originalName) {
3459
- const safe = originalName.replace(/[/\\]/g, "_") || "file";
3460
- if (!(0, import_node_fs13.existsSync)((0, import_node_path14.join)(dir, safe))) return safe;
3461
- const ext = (0, import_node_path14.extname)(safe);
3462
- const stem = ext ? safe.slice(0, -ext.length) : safe;
3463
- for (let i = 1; i < 1e4; i++) {
3464
- const candidate = `${stem}-${i}${ext}`;
3465
- if (!(0, import_node_fs13.existsSync)((0, import_node_path14.join)(dir, candidate))) return candidate;
3466
- }
3467
- return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
3468
- }
3469
- function previewDataUrlFromBuf(filePath, buf) {
3470
- if (!isImageFilePath(filePath)) return void 0;
3471
- if (buf.length > MAX_PREVIEW_BYTES) return void 0;
3472
- return `data:${imageMimeType(filePath)};base64,${buf.toString("base64")}`;
3473
- }
3474
- function attachmentFromBuffer(name, buf, opts) {
3475
- const previewDataUrl = previewDataUrlFromBuf(name, buf);
3476
- if (isImageFilePath(name)) {
3477
- const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
3478
- return {
3479
- id: (0, import_node_crypto5.randomUUID)(),
3480
- name,
3481
- kind: "file",
3482
- path: opts.path,
3483
- previewDataUrl,
3484
- content: [
3485
- `Image attached: ${pathHint}`,
3486
- opts.path ? `Use the Read tool on \`${opts.path}\` to view this image.` : "The image is shown in the composer; copy it into the worktree if you need to inspect pixels."
3487
- ].join("\n")
3488
- };
3489
- }
3490
- if (buf.length > MAX_INLINE_BYTES) {
3491
- return {
3492
- id: (0, import_node_crypto5.randomUUID)(),
3493
- name,
3494
- kind: "file",
3495
- path: opts.path,
3496
- content: opts.path ? `(file too large to attach inline: \`${opts.path}\`, ${buf.length} bytes \u2014 use the Read tool)` : `(file too large to attach inline: ${opts.sourceLabel || name}, ${buf.length} bytes)`
3497
- };
3498
- }
3499
- if (buf.includes(0)) {
3500
- return {
3501
- id: (0, import_node_crypto5.randomUUID)(),
3502
- name,
3503
- kind: "file",
3504
- path: opts.path,
3505
- content: opts.path ? `(binary file at \`${opts.path}\` \u2014 use tools to inspect)` : `(binary file attached by path only: ${opts.sourceLabel || name})`
3506
- };
3507
- }
3508
- return {
3509
- id: (0, import_node_crypto5.randomUUID)(),
3510
- name,
3511
- kind: "file",
3512
- path: opts.path,
3513
- content: buf.toString("utf8")
3514
- };
3515
- }
3516
- function attachmentFromAbsolutePath(absolutePath) {
3517
- const name = (0, import_node_path14.basename)(absolutePath);
3518
- try {
3519
- const st = (0, import_node_fs13.statSync)(absolutePath);
3520
- if (!st.isFile()) {
3521
- return {
3522
- id: (0, import_node_crypto5.randomUUID)(),
3523
- name,
3524
- kind: "file",
3525
- content: `(not a file: ${absolutePath})`
3526
- };
3527
- }
3528
- const buf = (0, import_node_fs13.readFileSync)(absolutePath);
3529
- return attachmentFromBuffer(name, buf, { sourceLabel: absolutePath });
3530
- } catch (err) {
3531
- return {
3532
- id: (0, import_node_crypto5.randomUUID)(),
3533
- name,
3534
- kind: "file",
3535
- content: `(could not read ${absolutePath}: ${err instanceof Error ? err.message : String(err)})`
3536
- };
3537
- }
3538
- }
3539
- function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
3540
- if (absolutePaths.length === 0) return [];
3541
- const dir = ensureAttachmentsDir(worktreePath);
3542
- const out = [];
3543
- for (const abs of absolutePaths) {
3544
- const originalName = (0, import_node_path14.basename)(abs);
3545
- try {
3546
- const st = (0, import_node_fs13.statSync)(abs);
3547
- if (!st.isFile()) continue;
3548
- const name = uniqueAttachmentName(dir, originalName);
3549
- const destAbs = (0, import_node_path14.join)(dir, name);
3550
- (0, import_node_fs13.copyFileSync)(abs, destAbs);
3551
- const rel = `${ATTACHMENTS_DIR}/${name}`;
3552
- const buf = (0, import_node_fs13.readFileSync)(destAbs);
3553
- out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
3554
- } catch (err) {
3555
- out.push({
3556
- id: (0, import_node_crypto5.randomUUID)(),
3557
- name: originalName,
3558
- kind: "file",
3559
- content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
3560
- });
3561
- }
3562
- }
3563
- return out;
3564
- }
3565
- function stageBuffersAsAttachments(worktreePath, buffers2) {
3566
- if (buffers2.length === 0) return [];
3567
- const dir = ensureAttachmentsDir(worktreePath);
3568
- const out = [];
3569
- for (const item of buffers2) {
3570
- const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
3571
- try {
3572
- const buf = Buffer.from(item.dataBase64, "base64");
3573
- const name = uniqueAttachmentName(dir, originalName);
3574
- const destAbs = (0, import_node_path14.join)(dir, name);
3575
- (0, import_node_fs13.writeFileSync)(destAbs, buf);
3576
- const rel = `${ATTACHMENTS_DIR}/${name}`;
3577
- out.push(attachmentFromBuffer(name, buf, { path: rel }));
3578
- } catch (err) {
3579
- out.push({
3580
- id: (0, import_node_crypto5.randomUUID)(),
3581
- name: originalName,
3582
- kind: "file",
3583
- content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
3584
- });
3585
- }
3586
- }
3587
- return out;
3588
- }
3589
- function attachmentsFromBuffers(buffers2) {
3590
- return buffers2.map((item) => {
3591
- const name = (item.name || "file").replace(/[/\\]/g, "_") || "file";
3592
- try {
3593
- const buf = Buffer.from(item.dataBase64, "base64");
3594
- return attachmentFromBuffer(name, buf, { sourceLabel: name });
3595
- } catch (err) {
3596
- return {
3597
- id: (0, import_node_crypto5.randomUUID)(),
3598
- name,
3599
- kind: "file",
3600
- content: `(could not attach ${name}: ${err instanceof Error ? err.message : String(err)})`
3601
- };
3602
- }
3603
- });
3604
- }
3605
- function isWorktreeRelativePath(p) {
3606
- if (!p || p.includes("..")) return false;
3607
- if (p.startsWith("/")) return false;
3608
- if (/^[A-Za-z]:[\\/]/.test(p)) return false;
3609
- return true;
3610
- }
3611
- function dataUrlToBase64(url) {
3612
- if (!url) return null;
3613
- const m = /^data:[^;]+;base64,(.+)$/s.exec(url);
3614
- return m?.[1] ?? null;
3615
- }
3616
- function persistPendingFileAttachments(worktreePath, attachments) {
3617
- if (attachments.length === 0) return attachments;
3618
- const keep = [];
3619
- const buffers2 = [];
3620
- for (const att of attachments) {
3621
- if (att.kind !== "file") {
3622
- keep.push(att);
3623
- continue;
3624
- }
3625
- if (att.path && isWorktreeRelativePath(att.path)) {
3626
- keep.push(att);
3627
- continue;
3628
- }
3629
- const fromPreview = dataUrlToBase64(att.previewDataUrl);
3630
- if (fromPreview) {
3631
- buffers2.push({ name: att.name, dataBase64: fromPreview });
3632
- continue;
3633
- }
3634
- if (att.content && !IMAGE_HINT_RE.test(att.content) && !PLACEHOLDER_CONTENT_RE.test(att.content)) {
3635
- buffers2.push({
3636
- name: att.name,
3637
- dataBase64: Buffer.from(att.content, "utf8").toString("base64")
3638
- });
3639
- continue;
3640
- }
3641
- keep.push(att);
3642
- }
3643
- if (buffers2.length === 0) return attachments;
3644
- return [...keep, ...stageBuffersAsAttachments(worktreePath, buffers2)];
3645
- }
3646
- function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
3647
- const out = [];
3648
- for (const rel of relativePaths) {
3649
- if (!rel || rel.includes("..") || rel.startsWith("/")) {
3650
- out.push({
3651
- id: (0, import_node_crypto5.randomUUID)(),
3652
- name: (0, import_node_path14.basename)(rel) || "file",
3653
- kind: "file",
3654
- content: `(invalid path: ${rel})`
3655
- });
3656
- continue;
3657
- }
3658
- const name = (0, import_node_path14.basename)(rel);
3659
- try {
3660
- const abs = (0, import_node_path14.join)(worktreePath, rel);
3661
- const st = (0, import_node_fs13.statSync)(abs);
3662
- if (!st.isFile()) continue;
3663
- const buf = (0, import_node_fs13.readFileSync)(abs);
3664
- out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
3665
- } catch (err) {
3666
- out.push({
3667
- id: (0, import_node_crypto5.randomUUID)(),
3668
- name,
3669
- kind: "file",
3670
- content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
3671
- });
3672
- }
3673
- }
3674
- return out;
3675
- }
3676
- var import_node_fs13, import_node_path14, import_node_crypto5, IMAGE_EXTENSIONS, IMAGE_MIME_BY_EXT, MAX_INLINE_BYTES, MAX_PREVIEW_BYTES, IMAGE_HINT_RE, PLACEHOLDER_CONTENT_RE;
3677
- var init_stage_files = __esm({
3678
- "src/composer/stage-files.ts"() {
3679
- "use strict";
3680
- import_node_fs13 = require("fs");
3681
- import_node_path14 = require("path");
3682
- import_node_crypto5 = require("crypto");
3683
- init_workspace_scratch();
3684
- IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
3685
- "png",
3686
- "jpg",
3687
- "jpeg",
3688
- "gif",
3689
- "webp",
3690
- "svg",
3691
- "bmp",
3692
- "ico"
3693
- ]);
3694
- IMAGE_MIME_BY_EXT = {
3695
- png: "image/png",
3696
- jpg: "image/jpeg",
3697
- jpeg: "image/jpeg",
3698
- gif: "image/gif",
3699
- webp: "image/webp",
3700
- svg: "image/svg+xml",
3701
- bmp: "image/bmp",
3702
- ico: "image/x-icon"
3703
- };
3704
- MAX_INLINE_BYTES = 4e5;
3705
- MAX_PREVIEW_BYTES = 5e6;
3706
- IMAGE_HINT_RE = /^Image attached:/;
3707
- PLACEHOLDER_CONTENT_RE = /^\((could not |file too large|binary file|not a file|invalid path)/;
3708
- }
3709
- });
3710
-
3711
- // src/git/team-meta.ts
3712
- var SOCCER_TEAM_META;
3713
- var init_team_meta = __esm({
3714
- "src/git/team-meta.ts"() {
2959
+ // src/git/team-meta.ts
2960
+ var SOCCER_TEAM_META;
2961
+ var init_team_meta = __esm({
2962
+ "src/git/team-meta.ts"() {
3715
2963
  "use strict";
3716
2964
  SOCCER_TEAM_META = {
3717
2965
  "aalborg": { location: "Aalborg, Denmark", league: "Danish Superliga" },
@@ -4502,77 +3750,835 @@ var init_teams = __esm({
4502
3750
  }
4503
3751
  });
4504
3752
 
4505
- // src/git/worktree-labels.ts
4506
- function normalizeWorktreePath(worktreePath) {
4507
- const trimmed = (worktreePath ?? "").replace(/\/+$/, "") || "/";
4508
- const absolute = trimmed.startsWith("/");
4509
- const parts = trimmed.split("/").filter((p) => p && p !== ".");
3753
+ // src/git/worktree-labels.ts
3754
+ function normalizeWorktreePath(worktreePath) {
3755
+ const trimmed = (worktreePath ?? "").replace(/\/+$/, "") || "/";
3756
+ const absolute = trimmed.startsWith("/");
3757
+ const parts = trimmed.split("/").filter((p) => p && p !== ".");
3758
+ const out = [];
3759
+ for (const p of parts) {
3760
+ if (p === "..") {
3761
+ out.pop();
3762
+ continue;
3763
+ }
3764
+ out.push(p);
3765
+ }
3766
+ return (absolute ? "/" : "") + out.join("/");
3767
+ }
3768
+ function worktreeNameFromPath(worktreePath) {
3769
+ const normalized = worktreePath.replace(/\/+$/, "");
3770
+ const parts = normalized.split("/");
3771
+ return parts[parts.length - 1] || worktreePath;
3772
+ }
3773
+ function ticketSlugForBranch(sourceRef) {
3774
+ const raw = sourceRef.trim();
3775
+ if (!raw) return null;
3776
+ const linear = raw.match(/\b([A-Za-z]{2,8}-\d{1,6})\b/);
3777
+ if (linear) return linear[1].toLowerCase();
3778
+ const hashed = raw.match(/^#(\d{1,8})$/);
3779
+ if (hashed) return hashed[1];
3780
+ const digits = raw.match(/^(\d{1,8})$/);
3781
+ if (digits) return digits[1];
3782
+ const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
3783
+ return slug || null;
3784
+ }
3785
+ function worktreeSlugForTicket(ticketRef, teamSlug) {
3786
+ const ticket = ticketSlugForBranch(ticketRef);
3787
+ const team = teamSlug.trim().replace(/^thread\//, "");
3788
+ if (!ticket) return team;
3789
+ if (!team) return ticket;
3790
+ return `${ticket}-${team}`;
3791
+ }
3792
+ function isPlaceholderBranch(branchName, worktreePath) {
3793
+ const branch = branchName.trim();
3794
+ if (!branch || branch === "HEAD") return true;
3795
+ if (branch.startsWith("thread/")) return true;
3796
+ const dir = worktreeNameFromPath(worktreePath).toLowerCase();
3797
+ return branch.toLowerCase() === dir;
3798
+ }
3799
+ function branchDisplayLabel(branchName, worktreePath) {
3800
+ const dir = worktreeNameFromPath(worktreePath);
3801
+ if (isPlaceholderBranch(branchName, worktreePath)) {
3802
+ const slug = branchName.replace(/^thread\//, "") || dir;
3803
+ return teamNameFromSlug(slug);
3804
+ }
3805
+ return branchName.trim();
3806
+ }
3807
+ function threadDisplayLabel(thread) {
3808
+ if (thread.userSetTitle && thread.title?.trim()) return thread.title.trim();
3809
+ if (thread.prTitle?.trim()) return thread.prTitle.trim();
3810
+ return branchDisplayLabel(thread.branchName, thread.worktreePath);
3811
+ }
3812
+ function worktreeDisplayLabel(thread) {
3813
+ return threadDisplayLabel(thread);
3814
+ }
3815
+ function worktreeDisplayLabelForGroup(threads) {
3816
+ if (threads.length === 0) return "Worktree";
3817
+ const canonical = [...threads].sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
3818
+ return threadDisplayLabel(canonical);
3819
+ }
3820
+ var init_worktree_labels = __esm({
3821
+ "src/git/worktree-labels.ts"() {
3822
+ "use strict";
3823
+ init_teams();
3824
+ }
3825
+ });
3826
+
3827
+ // src/store/setup-log.ts
3828
+ function setupLogPath(threadId) {
3829
+ return (0, import_node_path11.join)(threadsDir(), `${threadId}.setup.log.json`);
3830
+ }
3831
+ function setupLogKeyForWorktree(worktreePath) {
3832
+ const norm = normalizeWorktreePath(worktreePath);
3833
+ return `wt-${(0, import_node_crypto4.createHash)("sha1").update(norm).digest("hex").slice(0, 16)}`;
3834
+ }
3835
+ function emptySetupLog() {
3836
+ return { ...EMPTY };
3837
+ }
3838
+ function mergeSetupOutput(prev, incoming) {
3839
+ if (!prev) return incoming;
3840
+ if (!incoming) return prev;
3841
+ if (prev === incoming) return prev;
3842
+ if (prev.startsWith(incoming) || prev.endsWith(incoming)) return prev;
3843
+ if (incoming.startsWith(prev) || incoming.endsWith(prev)) return incoming;
3844
+ return incoming.length >= prev.length ? incoming : prev;
3845
+ }
3846
+ function clone(snap) {
3847
+ return { ...snap };
3848
+ }
3849
+ function persistNow(threadId) {
3850
+ const timer = persistTimers.get(threadId);
3851
+ if (timer) {
3852
+ clearTimeout(timer);
3853
+ persistTimers.delete(threadId);
3854
+ }
3855
+ const snap = memory.get(threadId);
3856
+ if (!snap) return;
3857
+ try {
3858
+ (0, import_node_fs10.writeFileSync)(setupLogPath(threadId), `${JSON.stringify(snap)}
3859
+ `, "utf8");
3860
+ } catch {
3861
+ }
3862
+ }
3863
+ function schedulePersist(threadId) {
3864
+ if (persistTimers.has(threadId)) return;
3865
+ persistTimers.set(
3866
+ threadId,
3867
+ setTimeout(() => {
3868
+ persistTimers.delete(threadId);
3869
+ persistNow(threadId);
3870
+ }, 100)
3871
+ );
3872
+ }
3873
+ function loadFromDisk(threadId) {
3874
+ const path2 = setupLogPath(threadId);
3875
+ if (!(0, import_node_fs10.existsSync)(path2)) return null;
3876
+ try {
3877
+ const raw = JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
3878
+ return {
3879
+ output: typeof raw.output === "string" ? raw.output : "",
3880
+ running: Boolean(raw.running),
3881
+ exitCode: typeof raw.exitCode === "number" ? raw.exitCode : null,
3882
+ source: typeof raw.source === "string" ? raw.source : null
3883
+ };
3884
+ } catch {
3885
+ return null;
3886
+ }
3887
+ }
3888
+ function readSetupLog(threadId) {
3889
+ const cached = memory.get(threadId);
3890
+ if (cached) return clone(cached);
3891
+ const disk = loadFromDisk(threadId);
3892
+ if (disk) {
3893
+ memory.set(threadId, disk);
3894
+ return clone(disk);
3895
+ }
3896
+ return emptySetupLog();
3897
+ }
3898
+ function beginSetupLog(threadId) {
3899
+ const snap = {
3900
+ output: "",
3901
+ running: true,
3902
+ exitCode: null,
3903
+ source: null
3904
+ };
3905
+ memory.set(threadId, snap);
3906
+ persistNow(threadId);
3907
+ return clone(snap);
3908
+ }
3909
+ function appendSetupLog(threadId, line) {
3910
+ const current = memory.get(threadId) ?? readSetupLog(threadId);
3911
+ const snap = {
3912
+ ...current,
3913
+ output: current.output ? `${current.output}
3914
+ ${line}` : line,
3915
+ running: true
3916
+ };
3917
+ memory.set(threadId, snap);
3918
+ schedulePersist(threadId);
3919
+ return clone(snap);
3920
+ }
3921
+ function finishSetupLog(threadId, exitCode, source) {
3922
+ const current = memory.get(threadId) ?? readSetupLog(threadId);
3923
+ const snap = {
3924
+ ...current,
3925
+ running: false,
3926
+ exitCode,
3927
+ source: source ?? current.source
3928
+ };
3929
+ memory.set(threadId, snap);
3930
+ persistNow(threadId);
3931
+ return clone(snap);
3932
+ }
3933
+ var import_node_crypto4, import_node_fs10, import_node_path11, EMPTY, memory, persistTimers;
3934
+ var init_setup_log = __esm({
3935
+ "src/store/setup-log.ts"() {
3936
+ "use strict";
3937
+ import_node_crypto4 = require("crypto");
3938
+ import_node_fs10 = require("fs");
3939
+ import_node_path11 = require("path");
3940
+ init_worktree_labels();
3941
+ init_paths();
3942
+ EMPTY = {
3943
+ output: "",
3944
+ running: false,
3945
+ exitCode: null,
3946
+ source: null
3947
+ };
3948
+ memory = /* @__PURE__ */ new Map();
3949
+ persistTimers = /* @__PURE__ */ new Map();
3950
+ }
3951
+ });
3952
+
3953
+ // src/store/schedules.ts
3954
+ function schedulesPath() {
3955
+ return (0, import_node_path12.join)(appDataDir(), "schedules.json");
3956
+ }
3957
+ function parseDurationMs(every) {
3958
+ const m = every.trim().match(DURATION_RE);
3959
+ if (!m) return null;
3960
+ const n = Number.parseInt(m[1], 10);
3961
+ if (!Number.isFinite(n) || n <= 0) return null;
3962
+ const unit = m[2].toLowerCase();
3963
+ return n * UNIT_MS[unit];
3964
+ }
3965
+ function defaultScheduleName(prompt) {
3966
+ const line = prompt.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
3967
+ if (!line) return "Untitled schedule";
3968
+ return line.length > 60 ? `${line.slice(0, 57).trimEnd()}\u2026` : line;
3969
+ }
3970
+ function resolveScheduleThreadId(raw, env = process.env) {
3971
+ const value = raw?.trim() || "";
3972
+ if (!value) return null;
3973
+ if (value.toLowerCase() === "self") {
3974
+ return env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
3975
+ }
3976
+ return value;
3977
+ }
3978
+ function computeNextRunAt(when, from = /* @__PURE__ */ new Date()) {
3979
+ if (when.kind === "once") {
3980
+ const at = new Date(when.at);
3981
+ if (Number.isNaN(at.getTime())) {
3982
+ throw new Error(`Invalid once-at datetime: ${when.at}`);
3983
+ }
3984
+ return at;
3985
+ }
3986
+ if (when.kind === "every") {
3987
+ const ms = parseDurationMs(when.every);
3988
+ if (!ms) {
3989
+ throw new Error(`Invalid interval: ${when.every} (use 15m, 1h, 6h, 1d)`);
3990
+ }
3991
+ return new Date(from.getTime() + ms);
3992
+ }
3993
+ try {
3994
+ const job = new import_croner.Cron(when.expr, {
3995
+ timezone: when.tz?.trim() || void 0,
3996
+ paused: true
3997
+ });
3998
+ const next = job.nextRun(from);
3999
+ if (!next) {
4000
+ throw new Error(`Cron expression has no next run: ${when.expr}`);
4001
+ }
4002
+ return next;
4003
+ } catch (err) {
4004
+ const message = err instanceof Error ? err.message : String(err);
4005
+ throw new Error(`Invalid cron: ${message}`);
4006
+ }
4007
+ }
4008
+ function formatScheduleWhen(when) {
4009
+ if (when.kind === "once") return `once at ${when.at}`;
4010
+ if (when.kind === "every") return `every ${when.every}`;
4011
+ const tz = when.tz?.trim();
4012
+ return tz ? `cron ${when.expr} (${tz})` : `cron ${when.expr}`;
4013
+ }
4014
+ function nowIso2(d = /* @__PURE__ */ new Date()) {
4015
+ return d.toISOString();
4016
+ }
4017
+ function normalizeWhen(raw) {
4018
+ if (!raw || typeof raw !== "object") {
4019
+ throw new Error("Schedule is missing `when`");
4020
+ }
4021
+ const rec = raw;
4022
+ const kind = rec.kind;
4023
+ if (kind === "once" && typeof rec.at === "string") {
4024
+ return { kind: "once", at: rec.at };
4025
+ }
4026
+ if (kind === "every" && typeof rec.every === "string") {
4027
+ return { kind: "every", every: rec.every };
4028
+ }
4029
+ if (kind === "cron" && typeof rec.expr === "string") {
4030
+ return {
4031
+ kind: "cron",
4032
+ expr: rec.expr,
4033
+ tz: typeof rec.tz === "string" && rec.tz.trim() ? rec.tz.trim() : void 0
4034
+ };
4035
+ }
4036
+ throw new Error("Schedule `when` must be once, every, or cron");
4037
+ }
4038
+ function normalizeTask(raw) {
4039
+ const when = normalizeWhen(raw.when);
4040
+ const agent = raw.agent && isOrchestratorCapableAgent(raw.agent) ? raw.agent : null;
4041
+ return {
4042
+ id: raw.id,
4043
+ name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : defaultScheduleName(raw.prompt),
4044
+ prompt: String(raw.prompt ?? ""),
4045
+ enabled: Boolean(raw.enabled),
4046
+ when,
4047
+ threadId: raw.threadId?.trim() || null,
4048
+ agent,
4049
+ model: raw.model?.trim() || null,
4050
+ nextRunAt: raw.nextRunAt,
4051
+ lastRunAt: raw.lastRunAt ?? null,
4052
+ lastThreadId: raw.lastThreadId ?? null,
4053
+ lastError: raw.lastError ?? null,
4054
+ createdBy: raw.createdBy === "mcp" || raw.createdBy === "cli" || raw.createdBy === "ui" ? raw.createdBy : "ui",
4055
+ createdAt: raw.createdAt,
4056
+ updatedAt: raw.updatedAt
4057
+ };
4058
+ }
4059
+ function readAll() {
4060
+ const path2 = schedulesPath();
4061
+ if (!(0, import_node_fs11.existsSync)(path2)) return [];
4062
+ try {
4063
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
4064
+ if (!Array.isArray(parsed)) return [];
4065
+ return parsed.map((row) => {
4066
+ try {
4067
+ return normalizeTask(row);
4068
+ } catch {
4069
+ return null;
4070
+ }
4071
+ }).filter((row) => Boolean(row?.id && row.prompt));
4072
+ } catch {
4073
+ return [];
4074
+ }
4075
+ }
4076
+ function writeAll(rows) {
4077
+ writePrivateFile(schedulesPath(), `${JSON.stringify(rows, null, 2)}
4078
+ `);
4079
+ }
4080
+ function buildTask(input, now) {
4081
+ const prompt = input.prompt.trim();
4082
+ if (!prompt) throw new Error("Schedule prompt is required");
4083
+ const when = normalizeWhen(input.when);
4084
+ const nextRunAt = computeNextRunAt(when, now).toISOString();
4085
+ const ts = nowIso2(now);
4086
+ const agent = input.agent ? coerceOrchestratorAgent(input.agent) : null;
4087
+ return {
4088
+ id: (0, import_node_crypto5.randomUUID)(),
4089
+ name: input.name?.trim() || defaultScheduleName(prompt),
4090
+ prompt,
4091
+ enabled: input.enabled !== false,
4092
+ when,
4093
+ threadId: input.threadId?.trim() || null,
4094
+ agent,
4095
+ model: input.model?.trim() || null,
4096
+ nextRunAt,
4097
+ lastRunAt: null,
4098
+ lastThreadId: null,
4099
+ lastError: null,
4100
+ createdBy: input.createdBy,
4101
+ createdAt: ts,
4102
+ updatedAt: ts
4103
+ };
4104
+ }
4105
+ function listSchedules() {
4106
+ return readAll().sort((a, b) => a.nextRunAt.localeCompare(b.nextRunAt));
4107
+ }
4108
+ function hasEnabledSchedules() {
4109
+ return listSchedules().some((row) => row.enabled);
4110
+ }
4111
+ function getSchedule(id) {
4112
+ const key = id.trim();
4113
+ if (!key) return null;
4114
+ return readAll().find(
4115
+ (row) => row.id === key || row.id.startsWith(key) && key.length >= 8
4116
+ ) ?? null;
4117
+ }
4118
+ function createSchedule(input) {
4119
+ const task = buildTask(input, /* @__PURE__ */ new Date());
4120
+ const rows = readAll();
4121
+ rows.push(task);
4122
+ writeAll(rows);
4123
+ return task;
4124
+ }
4125
+ function updateSchedule(id, patch) {
4126
+ const rows = readAll();
4127
+ const idx = rows.findIndex((row) => row.id === id || id.length >= 8 && row.id.startsWith(id));
4128
+ if (idx < 0) throw new Error(`Schedule not found: ${id}`);
4129
+ const prev = rows[idx];
4130
+ const when = patch.when ? normalizeWhen(patch.when) : prev.when;
4131
+ const prompt = patch.prompt !== void 0 ? patch.prompt.trim() : prev.prompt;
4132
+ if (!prompt) throw new Error("Schedule prompt is required");
4133
+ const whenChanged = patch.when !== void 0;
4134
+ const next = {
4135
+ ...prev,
4136
+ name: patch.name !== void 0 ? patch.name.trim() || defaultScheduleName(prompt) : prev.name,
4137
+ prompt,
4138
+ enabled: patch.enabled ?? prev.enabled,
4139
+ when,
4140
+ threadId: patch.threadId !== void 0 ? patch.threadId?.trim() || null : prev.threadId,
4141
+ agent: patch.agent !== void 0 ? patch.agent ? coerceOrchestratorAgent(patch.agent) : null : prev.agent,
4142
+ model: patch.model !== void 0 ? patch.model?.trim() || null : prev.model,
4143
+ nextRunAt: whenChanged ? computeNextRunAt(when).toISOString() : prev.nextRunAt,
4144
+ lastError: whenChanged ? null : prev.lastError,
4145
+ updatedAt: nowIso2()
4146
+ };
4147
+ rows[idx] = next;
4148
+ writeAll(rows);
4149
+ return next;
4150
+ }
4151
+ function deleteSchedule(id) {
4152
+ const rows = readAll();
4153
+ const next = rows.filter(
4154
+ (row) => row.id !== id && !(id.length >= 8 && row.id.startsWith(id))
4155
+ );
4156
+ if (next.length === rows.length) throw new Error(`Schedule not found: ${id}`);
4157
+ writeAll(next);
4158
+ }
4159
+ function recordScheduleRun(id, result) {
4160
+ const rows = readAll();
4161
+ const idx = rows.findIndex((row) => row.id === id);
4162
+ if (idx < 0) throw new Error(`Schedule not found: ${id}`);
4163
+ const prev = rows[idx];
4164
+ const firedAt = result.firedAt ?? /* @__PURE__ */ new Date();
4165
+ const next = {
4166
+ ...prev,
4167
+ enabled: prev.when.kind === "once" ? false : prev.enabled,
4168
+ nextRunAt: prev.when.kind === "once" ? prev.nextRunAt : computeNextRunAt(prev.when, firedAt).toISOString(),
4169
+ lastRunAt: nowIso2(firedAt),
4170
+ lastThreadId: result.lastThreadId !== void 0 ? result.lastThreadId : prev.lastThreadId,
4171
+ lastError: result.lastError ?? null,
4172
+ updatedAt: nowIso2(firedAt)
4173
+ };
4174
+ rows[idx] = next;
4175
+ writeAll(rows);
4176
+ return next;
4177
+ }
4178
+ var import_node_crypto5, import_node_fs11, import_node_path12, import_croner, DURATION_RE, UNIT_MS;
4179
+ var init_schedules = __esm({
4180
+ "src/store/schedules.ts"() {
4181
+ "use strict";
4182
+ import_node_crypto5 = require("crypto");
4183
+ import_node_fs11 = require("fs");
4184
+ import_node_path12 = require("path");
4185
+ import_croner = require("croner");
4186
+ init_orchestrator_capable();
4187
+ init_paths();
4188
+ init_private_file();
4189
+ DURATION_RE = /^(\d+)(s|m|h|d)$/i;
4190
+ UNIT_MS = {
4191
+ s: 1e3,
4192
+ m: 6e4,
4193
+ h: 36e5,
4194
+ d: 864e5
4195
+ };
4196
+ }
4197
+ });
4198
+
4199
+ // src/store/desktop-host.ts
4200
+ function desktopHostPidPath() {
4201
+ return (0, import_node_path13.join)(appDataDir(), "desktop-host.pid");
4202
+ }
4203
+ function claimDesktopHost(pid = process.pid) {
4204
+ (0, import_node_fs12.writeFileSync)(desktopHostPidPath(), `${pid}
4205
+ `, "utf8");
4206
+ }
4207
+ function releaseDesktopHost(pid = process.pid) {
4208
+ if (readDesktopHostPid() !== pid) return;
4209
+ try {
4210
+ (0, import_node_fs12.unlinkSync)(desktopHostPidPath());
4211
+ } catch {
4212
+ }
4213
+ }
4214
+ function readDesktopHostPid() {
4215
+ try {
4216
+ const pid = Number.parseInt((0, import_node_fs12.readFileSync)(desktopHostPidPath(), "utf8").trim(), 10);
4217
+ if (!Number.isFinite(pid) || pid <= 0) return null;
4218
+ return pid;
4219
+ } catch {
4220
+ return null;
4221
+ }
4222
+ }
4223
+ function pidAlive(pid) {
4224
+ try {
4225
+ process.kill(pid, 0);
4226
+ return true;
4227
+ } catch {
4228
+ return false;
4229
+ }
4230
+ }
4231
+ function isDesktopHostAlive() {
4232
+ const pid = readDesktopHostPid();
4233
+ return pid != null && pidAlive(pid);
4234
+ }
4235
+ function isThisProcessDesktopHost() {
4236
+ return readDesktopHostPid() === process.pid && pidAlive(process.pid);
4237
+ }
4238
+ function thisProcessShouldDrainAgentQueues() {
4239
+ if (isThisProcessDesktopHost()) return true;
4240
+ return !isDesktopHostAlive();
4241
+ }
4242
+ var import_node_fs12, import_node_path13;
4243
+ var init_desktop_host = __esm({
4244
+ "src/store/desktop-host.ts"() {
4245
+ "use strict";
4246
+ import_node_fs12 = require("fs");
4247
+ import_node_path13 = require("path");
4248
+ init_paths();
4249
+ }
4250
+ });
4251
+
4252
+ // src/brightsy/cloud-connect-constants.ts
4253
+ function parseForceStopMessage(message) {
4254
+ const normalized = message.replace(/\r\n/g, "\n");
4255
+ const nl = normalized.indexOf("\n");
4256
+ const firstLine = (nl === -1 ? normalized : normalized.slice(0, nl)).trim();
4257
+ const remainder = (nl === -1 ? "" : normalized.slice(nl + 1)).trim();
4258
+ const forceStop = firstLine.toLowerCase() === SIDEBOARD_FORCE_STOP.toLowerCase();
4259
+ return { forceStop, remainder: forceStop ? remainder : message };
4260
+ }
4261
+ var CLOUD_ORCHESTRATOR_GOAL, SIDEBOARD_FORCE_STOP, CLOUD_COORDINATOR_BUSY_REPLY, CLOUD_COORDINATOR_STOPPED_REPLY, CLOUD_COORDINATOR_TIMEOUT_REPLY;
4262
+ var init_cloud_connect_constants = __esm({
4263
+ "src/brightsy/cloud-connect-constants.ts"() {
4264
+ "use strict";
4265
+ CLOUD_ORCHESTRATOR_GOAL = "Cloud-connected Sideboard orchestrator";
4266
+ SIDEBOARD_FORCE_STOP = "SIDEBOARD_FORCE_STOP";
4267
+ CLOUD_COORDINATOR_BUSY_REPLY = [
4268
+ "Sideboard is busy with an in-progress client/desktop tool turn on the global coordinator.",
4269
+ "I did not start a new orchestration turn.",
4270
+ `To force-stop the in-progress turn, send another desktop request whose first line is exactly ${SIDEBOARD_FORCE_STOP}`,
4271
+ "(optional follow-up request on later lines).",
4272
+ "What do you want me to do? (retry later, wait until idle, force-stop, rephrase, or cancel)"
4273
+ ].join(" ");
4274
+ CLOUD_COORDINATOR_STOPPED_REPLY = [
4275
+ "Sideboard force-stopped the in-progress global coordinator turn.",
4276
+ "No new orchestration request was started."
4277
+ ].join(" ");
4278
+ CLOUD_COORDINATOR_TIMEOUT_REPLY = [
4279
+ "Sideboard global coordinator timed out waiting for the local agent turn to finish.",
4280
+ "I did not produce a full orchestration result.",
4281
+ `To force-stop the in-progress turn, send another desktop request whose first line is exactly ${SIDEBOARD_FORCE_STOP}`,
4282
+ "(optional follow-up request on later lines).",
4283
+ "What do you want me to do? (retry later, wait, force-stop, rephrase, or cancel)"
4284
+ ].join(" ");
4285
+ }
4286
+ });
4287
+
4288
+ // src/paths/workspace-scratch.ts
4289
+ function attachmentsGitignoreBody() {
4290
+ return ATTACHMENTS_GITIGNORE;
4291
+ }
4292
+ function isWorkspaceScratchPath(relativePath) {
4293
+ const p = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
4294
+ return p === ATTACHMENTS_DIR || p.startsWith(`${ATTACHMENTS_DIR}/`) || p === CLI_DIR || p.startsWith(`${CLI_DIR}/`) || p === LEGACY_ATTACHMENTS_DIR || p.startsWith(`${LEGACY_ATTACHMENTS_DIR}/`) || p === ".sideboard" || p.startsWith(".sideboard/") || p === ".context" || p.startsWith(".context/");
4295
+ }
4296
+ var ATTACHMENTS_DIR, CLI_DIR, DETACHED_JOBS_DIR, LEGACY_ATTACHMENTS_DIR, LEGACY_DETACHED_JOBS_DIR, ATTACHMENTS_GITIGNORE;
4297
+ var init_workspace_scratch = __esm({
4298
+ "src/paths/workspace-scratch.ts"() {
4299
+ "use strict";
4300
+ ATTACHMENTS_DIR = ".context/attachments";
4301
+ CLI_DIR = ".context/cli";
4302
+ DETACHED_JOBS_DIR = ".context/.sideboard/detached-jobs";
4303
+ LEGACY_ATTACHMENTS_DIR = ".sideboard/attachments";
4304
+ LEGACY_DETACHED_JOBS_DIR = ".sideboard/detached-jobs";
4305
+ ATTACHMENTS_GITIGNORE = `# Sideboard / workspace attachments (local only)
4306
+ *
4307
+ !.gitignore
4308
+ `;
4309
+ }
4310
+ });
4311
+
4312
+ // src/composer/stage-files.ts
4313
+ function fileExtension(filePath) {
4314
+ const base = (0, import_node_path14.basename)(filePath).toLowerCase();
4315
+ return base.includes(".") ? base.split(".").pop() || "" : "";
4316
+ }
4317
+ function isImageFilePath(filePath) {
4318
+ return IMAGE_EXTENSIONS.has(fileExtension(filePath));
4319
+ }
4320
+ function imageMimeType(filePath) {
4321
+ return IMAGE_MIME_BY_EXT[fileExtension(filePath)] || "image/png";
4322
+ }
4323
+ function ensureAttachmentsDir(worktreePath) {
4324
+ const dir = (0, import_node_path14.join)(worktreePath, ATTACHMENTS_DIR);
4325
+ (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
4326
+ const gi = (0, import_node_path14.join)(dir, ".gitignore");
4327
+ if (!(0, import_node_fs13.existsSync)(gi)) {
4328
+ (0, import_node_fs13.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
4329
+ }
4330
+ return dir;
4331
+ }
4332
+ function uniqueAttachmentName(dir, originalName) {
4333
+ const safe = originalName.replace(/[/\\]/g, "_") || "file";
4334
+ if (!(0, import_node_fs13.existsSync)((0, import_node_path14.join)(dir, safe))) return safe;
4335
+ const ext = (0, import_node_path14.extname)(safe);
4336
+ const stem = ext ? safe.slice(0, -ext.length) : safe;
4337
+ for (let i = 1; i < 1e4; i++) {
4338
+ const candidate = `${stem}-${i}${ext}`;
4339
+ if (!(0, import_node_fs13.existsSync)((0, import_node_path14.join)(dir, candidate))) return candidate;
4340
+ }
4341
+ return `${stem}-${(0, import_node_crypto6.randomUUID)()}${ext}`;
4342
+ }
4343
+ function previewDataUrlFromBuf(filePath, buf) {
4344
+ if (!isImageFilePath(filePath)) return void 0;
4345
+ if (buf.length > MAX_PREVIEW_BYTES) return void 0;
4346
+ return `data:${imageMimeType(filePath)};base64,${buf.toString("base64")}`;
4347
+ }
4348
+ function attachmentFromBuffer(name, buf, opts) {
4349
+ const previewDataUrl = previewDataUrlFromBuf(name, buf);
4350
+ if (isImageFilePath(name)) {
4351
+ const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
4352
+ return {
4353
+ id: (0, import_node_crypto6.randomUUID)(),
4354
+ name,
4355
+ kind: "file",
4356
+ path: opts.path,
4357
+ previewDataUrl,
4358
+ content: [
4359
+ `Image attached: ${pathHint}`,
4360
+ opts.path ? `Use the Read tool on \`${opts.path}\` to view this image.` : "The image is shown in the composer; copy it into the worktree if you need to inspect pixels."
4361
+ ].join("\n")
4362
+ };
4363
+ }
4364
+ if (buf.length > MAX_INLINE_BYTES) {
4365
+ return {
4366
+ id: (0, import_node_crypto6.randomUUID)(),
4367
+ name,
4368
+ kind: "file",
4369
+ path: opts.path,
4370
+ content: opts.path ? `(file too large to attach inline: \`${opts.path}\`, ${buf.length} bytes \u2014 use the Read tool)` : `(file too large to attach inline: ${opts.sourceLabel || name}, ${buf.length} bytes)`
4371
+ };
4372
+ }
4373
+ if (buf.includes(0)) {
4374
+ return {
4375
+ id: (0, import_node_crypto6.randomUUID)(),
4376
+ name,
4377
+ kind: "file",
4378
+ path: opts.path,
4379
+ content: opts.path ? `(binary file at \`${opts.path}\` \u2014 use tools to inspect)` : `(binary file attached by path only: ${opts.sourceLabel || name})`
4380
+ };
4381
+ }
4382
+ return {
4383
+ id: (0, import_node_crypto6.randomUUID)(),
4384
+ name,
4385
+ kind: "file",
4386
+ path: opts.path,
4387
+ content: buf.toString("utf8")
4388
+ };
4389
+ }
4390
+ function attachmentFromAbsolutePath(absolutePath) {
4391
+ const name = (0, import_node_path14.basename)(absolutePath);
4392
+ try {
4393
+ const st = (0, import_node_fs13.statSync)(absolutePath);
4394
+ if (!st.isFile()) {
4395
+ return {
4396
+ id: (0, import_node_crypto6.randomUUID)(),
4397
+ name,
4398
+ kind: "file",
4399
+ content: `(not a file: ${absolutePath})`
4400
+ };
4401
+ }
4402
+ const buf = (0, import_node_fs13.readFileSync)(absolutePath);
4403
+ return attachmentFromBuffer(name, buf, { sourceLabel: absolutePath });
4404
+ } catch (err) {
4405
+ return {
4406
+ id: (0, import_node_crypto6.randomUUID)(),
4407
+ name,
4408
+ kind: "file",
4409
+ content: `(could not read ${absolutePath}: ${err instanceof Error ? err.message : String(err)})`
4410
+ };
4411
+ }
4412
+ }
4413
+ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
4414
+ if (absolutePaths.length === 0) return [];
4415
+ const dir = ensureAttachmentsDir(worktreePath);
4510
4416
  const out = [];
4511
- for (const p of parts) {
4512
- if (p === "..") {
4513
- out.pop();
4514
- continue;
4417
+ for (const abs of absolutePaths) {
4418
+ const originalName = (0, import_node_path14.basename)(abs);
4419
+ try {
4420
+ const st = (0, import_node_fs13.statSync)(abs);
4421
+ if (!st.isFile()) continue;
4422
+ const name = uniqueAttachmentName(dir, originalName);
4423
+ const destAbs = (0, import_node_path14.join)(dir, name);
4424
+ (0, import_node_fs13.copyFileSync)(abs, destAbs);
4425
+ const rel = `${ATTACHMENTS_DIR}/${name}`;
4426
+ const buf = (0, import_node_fs13.readFileSync)(destAbs);
4427
+ out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
4428
+ } catch (err) {
4429
+ out.push({
4430
+ id: (0, import_node_crypto6.randomUUID)(),
4431
+ name: originalName,
4432
+ kind: "file",
4433
+ content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
4434
+ });
4435
+ }
4436
+ }
4437
+ return out;
4438
+ }
4439
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
4440
+ if (buffers2.length === 0) return [];
4441
+ const dir = ensureAttachmentsDir(worktreePath);
4442
+ const out = [];
4443
+ for (const item of buffers2) {
4444
+ const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
4445
+ try {
4446
+ const buf = Buffer.from(item.dataBase64, "base64");
4447
+ const name = uniqueAttachmentName(dir, originalName);
4448
+ const destAbs = (0, import_node_path14.join)(dir, name);
4449
+ (0, import_node_fs13.writeFileSync)(destAbs, buf);
4450
+ const rel = `${ATTACHMENTS_DIR}/${name}`;
4451
+ out.push(attachmentFromBuffer(name, buf, { path: rel }));
4452
+ } catch (err) {
4453
+ out.push({
4454
+ id: (0, import_node_crypto6.randomUUID)(),
4455
+ name: originalName,
4456
+ kind: "file",
4457
+ content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
4458
+ });
4515
4459
  }
4516
- out.push(p);
4517
4460
  }
4518
- return (absolute ? "/" : "") + out.join("/");
4519
- }
4520
- function worktreeNameFromPath(worktreePath) {
4521
- const normalized = worktreePath.replace(/\/+$/, "");
4522
- const parts = normalized.split("/");
4523
- return parts[parts.length - 1] || worktreePath;
4461
+ return out;
4524
4462
  }
4525
- function ticketSlugForBranch(sourceRef) {
4526
- const raw = sourceRef.trim();
4527
- if (!raw) return null;
4528
- const linear = raw.match(/\b([A-Za-z]{2,8}-\d{1,6})\b/);
4529
- if (linear) return linear[1].toLowerCase();
4530
- const hashed = raw.match(/^#(\d{1,8})$/);
4531
- if (hashed) return hashed[1];
4532
- const digits = raw.match(/^(\d{1,8})$/);
4533
- if (digits) return digits[1];
4534
- const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
4535
- return slug || null;
4463
+ function attachmentsFromBuffers(buffers2) {
4464
+ return buffers2.map((item) => {
4465
+ const name = (item.name || "file").replace(/[/\\]/g, "_") || "file";
4466
+ try {
4467
+ const buf = Buffer.from(item.dataBase64, "base64");
4468
+ return attachmentFromBuffer(name, buf, { sourceLabel: name });
4469
+ } catch (err) {
4470
+ return {
4471
+ id: (0, import_node_crypto6.randomUUID)(),
4472
+ name,
4473
+ kind: "file",
4474
+ content: `(could not attach ${name}: ${err instanceof Error ? err.message : String(err)})`
4475
+ };
4476
+ }
4477
+ });
4536
4478
  }
4537
- function worktreeSlugForTicket(ticketRef, teamSlug) {
4538
- const ticket = ticketSlugForBranch(ticketRef);
4539
- const team = teamSlug.trim().replace(/^thread\//, "");
4540
- if (!ticket) return team;
4541
- if (!team) return ticket;
4542
- return `${ticket}-${team}`;
4479
+ function isWorktreeRelativePath(p) {
4480
+ if (!p || p.includes("..")) return false;
4481
+ if (p.startsWith("/")) return false;
4482
+ if (/^[A-Za-z]:[\\/]/.test(p)) return false;
4483
+ return true;
4543
4484
  }
4544
- function isPlaceholderBranch(branchName, worktreePath) {
4545
- const branch = branchName.trim();
4546
- if (!branch || branch === "HEAD") return true;
4547
- if (branch.startsWith("thread/")) return true;
4548
- const dir = worktreeNameFromPath(worktreePath).toLowerCase();
4549
- return branch.toLowerCase() === dir;
4485
+ function dataUrlToBase64(url) {
4486
+ if (!url) return null;
4487
+ const m = /^data:[^;]+;base64,(.+)$/s.exec(url);
4488
+ return m?.[1] ?? null;
4550
4489
  }
4551
- function branchDisplayLabel(branchName, worktreePath) {
4552
- const dir = worktreeNameFromPath(worktreePath);
4553
- if (isPlaceholderBranch(branchName, worktreePath)) {
4554
- const slug = branchName.replace(/^thread\//, "") || dir;
4555
- return teamNameFromSlug(slug);
4490
+ function persistPendingFileAttachments(worktreePath, attachments) {
4491
+ if (attachments.length === 0) return attachments;
4492
+ const keep = [];
4493
+ const buffers2 = [];
4494
+ for (const att of attachments) {
4495
+ if (att.kind !== "file") {
4496
+ keep.push(att);
4497
+ continue;
4498
+ }
4499
+ if (att.path && isWorktreeRelativePath(att.path)) {
4500
+ keep.push(att);
4501
+ continue;
4502
+ }
4503
+ const fromPreview = dataUrlToBase64(att.previewDataUrl);
4504
+ if (fromPreview) {
4505
+ buffers2.push({ name: att.name, dataBase64: fromPreview });
4506
+ continue;
4507
+ }
4508
+ if (att.content && !IMAGE_HINT_RE.test(att.content) && !PLACEHOLDER_CONTENT_RE.test(att.content)) {
4509
+ buffers2.push({
4510
+ name: att.name,
4511
+ dataBase64: Buffer.from(att.content, "utf8").toString("base64")
4512
+ });
4513
+ continue;
4514
+ }
4515
+ keep.push(att);
4556
4516
  }
4557
- return branchName.trim();
4558
- }
4559
- function threadDisplayLabel(thread) {
4560
- if (thread.userSetTitle && thread.title?.trim()) return thread.title.trim();
4561
- if (thread.prTitle?.trim()) return thread.prTitle.trim();
4562
- return branchDisplayLabel(thread.branchName, thread.worktreePath);
4563
- }
4564
- function worktreeDisplayLabel(thread) {
4565
- return threadDisplayLabel(thread);
4517
+ if (buffers2.length === 0) return attachments;
4518
+ return [...keep, ...stageBuffersAsAttachments(worktreePath, buffers2)];
4566
4519
  }
4567
- function worktreeDisplayLabelForGroup(threads) {
4568
- if (threads.length === 0) return "Worktree";
4569
- const canonical = [...threads].sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
4570
- return threadDisplayLabel(canonical);
4520
+ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
4521
+ const out = [];
4522
+ for (const rel of relativePaths) {
4523
+ if (!rel || rel.includes("..") || rel.startsWith("/")) {
4524
+ out.push({
4525
+ id: (0, import_node_crypto6.randomUUID)(),
4526
+ name: (0, import_node_path14.basename)(rel) || "file",
4527
+ kind: "file",
4528
+ content: `(invalid path: ${rel})`
4529
+ });
4530
+ continue;
4531
+ }
4532
+ const name = (0, import_node_path14.basename)(rel);
4533
+ try {
4534
+ const abs = (0, import_node_path14.join)(worktreePath, rel);
4535
+ const st = (0, import_node_fs13.statSync)(abs);
4536
+ if (!st.isFile()) continue;
4537
+ const buf = (0, import_node_fs13.readFileSync)(abs);
4538
+ out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
4539
+ } catch (err) {
4540
+ out.push({
4541
+ id: (0, import_node_crypto6.randomUUID)(),
4542
+ name,
4543
+ kind: "file",
4544
+ content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
4545
+ });
4546
+ }
4547
+ }
4548
+ return out;
4571
4549
  }
4572
- var init_worktree_labels = __esm({
4573
- "src/git/worktree-labels.ts"() {
4550
+ var import_node_fs13, import_node_path14, import_node_crypto6, IMAGE_EXTENSIONS, IMAGE_MIME_BY_EXT, MAX_INLINE_BYTES, MAX_PREVIEW_BYTES, IMAGE_HINT_RE, PLACEHOLDER_CONTENT_RE;
4551
+ var init_stage_files = __esm({
4552
+ "src/composer/stage-files.ts"() {
4574
4553
  "use strict";
4575
- init_teams();
4554
+ import_node_fs13 = require("fs");
4555
+ import_node_path14 = require("path");
4556
+ import_node_crypto6 = require("crypto");
4557
+ init_workspace_scratch();
4558
+ IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
4559
+ "png",
4560
+ "jpg",
4561
+ "jpeg",
4562
+ "gif",
4563
+ "webp",
4564
+ "svg",
4565
+ "bmp",
4566
+ "ico"
4567
+ ]);
4568
+ IMAGE_MIME_BY_EXT = {
4569
+ png: "image/png",
4570
+ jpg: "image/jpeg",
4571
+ jpeg: "image/jpeg",
4572
+ gif: "image/gif",
4573
+ webp: "image/webp",
4574
+ svg: "image/svg+xml",
4575
+ bmp: "image/bmp",
4576
+ ico: "image/x-icon"
4577
+ };
4578
+ MAX_INLINE_BYTES = 4e5;
4579
+ MAX_PREVIEW_BYTES = 5e6;
4580
+ IMAGE_HINT_RE = /^Image attached:/;
4581
+ PLACEHOLDER_CONTENT_RE = /^\((could not |file too large|binary file|not a file|invalid path)/;
4576
4582
  }
4577
4583
  });
4578
4584
 
@@ -7282,7 +7288,7 @@ async function createOrUpdatePr(worktreePath, opts) {
7282
7288
  const bodyText = clampGithubPrBody(opts.body ?? opts.title);
7283
7289
  const bodyFile = (0, import_node_path20.join)(
7284
7290
  (0, import_node_os6.tmpdir)(),
7285
- `sideboard-pr-body-${process.pid}-${(0, import_node_crypto6.randomBytes)(6).toString("hex")}.md`
7291
+ `sideboard-pr-body-${process.pid}-${(0, import_node_crypto7.randomBytes)(6).toString("hex")}.md`
7286
7292
  );
7287
7293
  (0, import_node_fs18.writeFileSync)(bodyFile, bodyText.endsWith("\n") ? bodyText : `${bodyText}
7288
7294
  `, "utf8");
@@ -7423,11 +7429,11 @@ function allocateTeamSlug(repoPath) {
7423
7429
  }
7424
7430
  throw new Error("No available soccer team worktree directories left");
7425
7431
  }
7426
- var import_node_crypto6, import_node_fs18, import_node_os6, import_node_path20, PR_DETAILS_JSON_FIELDS;
7432
+ var import_node_crypto7, import_node_fs18, import_node_os6, import_node_path20, PR_DETAILS_JSON_FIELDS;
7427
7433
  var init_worktree = __esm({
7428
7434
  "src/git/worktree.ts"() {
7429
7435
  "use strict";
7430
- import_node_crypto6 = require("crypto");
7436
+ import_node_crypto7 = require("crypto");
7431
7437
  import_node_fs18 = require("fs");
7432
7438
  import_node_os6 = require("os");
7433
7439
  import_node_path20 = require("path");
@@ -12161,6 +12167,9 @@ function toCodexDisableUserMcpArgs(names) {
12161
12167
  }
12162
12168
  return args;
12163
12169
  }
12170
+ function toCodexUnattendedAppsArgs() {
12171
+ return ["-c", "features.apps=false", "-c", "apps._default.enabled=false"];
12172
+ }
12164
12173
  var import_node_fs32, import_node_os11, import_node_path33, import_smol_toml2;
12165
12174
  var init_orch_mcp_isolation = __esm({
12166
12175
  "src/agents/orch-mcp-isolation.ts"() {
@@ -12356,6 +12365,7 @@ var init_codex = __esm({
12356
12365
  orchestratorThreadId: isOrchestrator ? thread.id : null
12357
12366
  });
12358
12367
  const mcpOverrides = [
12368
+ ...toCodexUnattendedAppsArgs(),
12359
12369
  ...toCodexMcpConfigArgs(injected2),
12360
12370
  ...isOrchestrator ? toCodexDisableUserMcpArgs(
12361
12371
  userMcpNamesToDisable({
@@ -14793,6 +14803,59 @@ var init_context_compact = __esm({
14793
14803
  }
14794
14804
  });
14795
14805
 
14806
+ // src/threads/worktree-runtime.ts
14807
+ function worktreeProcessPrefix(worktreePath) {
14808
+ return `wt:${normalizeWorktreePath(worktreePath)}`;
14809
+ }
14810
+ function worktreeSetupProcessKey(worktreePath) {
14811
+ return `${worktreeProcessPrefix(worktreePath)}:setup`;
14812
+ }
14813
+ function worktreeRunProcessKey(worktreePath, scriptName) {
14814
+ return `${worktreeProcessPrefix(worktreePath)}:run:${scriptName}`;
14815
+ }
14816
+ function worktreeDevProcessKey(worktreePath) {
14817
+ return `${worktreeProcessPrefix(worktreePath)}:dev`;
14818
+ }
14819
+ function isWorktreeRunProcessKey(key, worktreePath) {
14820
+ const prefix = worktreeProcessPrefix(worktreePath);
14821
+ return key.startsWith(`${prefix}:run:`) || key === `${prefix}:dev`;
14822
+ }
14823
+ function pickRichestSetupLog(snaps) {
14824
+ let best;
14825
+ for (const snap of snaps) {
14826
+ if (!snap.output && !snap.running && snap.exitCode == null) continue;
14827
+ if (!best) {
14828
+ best = snap;
14829
+ continue;
14830
+ }
14831
+ if (snap.running && !best.running) {
14832
+ best = snap;
14833
+ continue;
14834
+ }
14835
+ if (snap.running === best.running && snap.output.length > best.output.length) {
14836
+ best = snap;
14837
+ }
14838
+ }
14839
+ return best;
14840
+ }
14841
+ function mergeWorktreeActiveRuns(threads) {
14842
+ const byName = /* @__PURE__ */ new Map();
14843
+ let devPort = null;
14844
+ for (const thread of threads) {
14845
+ if (thread.devPort != null && devPort == null) devPort = thread.devPort;
14846
+ for (const run2 of thread.activeRuns ?? []) {
14847
+ if (!byName.has(run2.scriptName)) byName.set(run2.scriptName, run2);
14848
+ }
14849
+ }
14850
+ return { activeRuns: [...byName.values()], devPort };
14851
+ }
14852
+ var init_worktree_runtime = __esm({
14853
+ "src/threads/worktree-runtime.ts"() {
14854
+ "use strict";
14855
+ init_worktree_labels();
14856
+ }
14857
+ });
14858
+
14796
14859
  // src/threads/chat-tabs.ts
14797
14860
  function sameWorktreePath(a, b) {
14798
14861
  return normalizeWorktreePath(a) === normalizeWorktreePath(b);
@@ -14855,7 +14918,7 @@ function forkMessageSlice(from, throughIndex) {
14855
14918
  function buildForkTranscriptAttachment(baseTitle, messages) {
14856
14919
  const title = baseTitle || "Chat";
14857
14920
  return {
14858
- id: (0, import_node_crypto7.randomUUID)(),
14921
+ id: (0, import_node_crypto8.randomUUID)(),
14859
14922
  name: `Transcript of ${title}.md`,
14860
14923
  kind: "transcript",
14861
14924
  content: formatTranscriptMarkdown(title, messages)
@@ -14889,7 +14952,8 @@ function createChatTab(input) {
14889
14952
  planMode: from.planMode,
14890
14953
  autonomy: input.autonomy ?? from.autonomy,
14891
14954
  attachments: input.attachments ?? [],
14892
- status: "idle"
14955
+ status: "idle",
14956
+ ...mergeWorktreeActiveRuns(threadsSharingWorktree(binding.worktreePath))
14893
14957
  });
14894
14958
  writeThread(thread);
14895
14959
  return thread;
@@ -14912,17 +14976,18 @@ function forkChatTab(input) {
14912
14976
  }
14913
14977
  return tab;
14914
14978
  }
14915
- var import_node_crypto7;
14979
+ var import_node_crypto8;
14916
14980
  var init_chat_tabs = __esm({
14917
14981
  "src/threads/chat-tabs.ts"() {
14918
14982
  "use strict";
14919
- import_node_crypto7 = require("crypto");
14983
+ import_node_crypto8 = require("crypto");
14920
14984
  init_context_compact();
14921
14985
  init_teams();
14922
14986
  init_worktree_labels();
14923
14987
  init_global_workspace();
14924
14988
  init_orchestrator_capable();
14925
14989
  init_thread_store();
14990
+ init_worktree_runtime();
14926
14991
  init_worktree_labels();
14927
14992
  }
14928
14993
  });
@@ -15182,7 +15247,7 @@ function buildReviewRequestAttachment(content, opts) {
15182
15247
  const path2 = opts?.path ?? REVIEW_SKILL_PATH;
15183
15248
  const name = opts?.name ?? (path2 === REVIEW_SKILL_PATH ? REVIEW_SKILL_NAME : path2 === REPO_REVIEW_PATH || path2 === CONTEXT_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
15184
15249
  return {
15185
- id: (0, import_node_crypto8.randomUUID)(),
15250
+ id: (0, import_node_crypto9.randomUUID)(),
15186
15251
  name,
15187
15252
  kind: "file",
15188
15253
  path: path2,
@@ -15217,11 +15282,11 @@ async function requestReview(threadRef, send2) {
15217
15282
  const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
15218
15283
  return { tab: started, from };
15219
15284
  }
15220
- var import_node_crypto8, import_node_fs38, import_node_path38, REPO_REVIEW_PATH, REPO_REVIEW_NAME, CONTEXT_REVIEW_PATH, REVIEW_REQUEST_PATH, LEGACY_REVIEW_REQUEST_PATH, REVIEW_REQUEST_NAME, REVIEW_REQUEST_PREFILL, LEGACY_REVIEW_TEMPLATE_MARKERS;
15285
+ var import_node_crypto9, import_node_fs38, import_node_path38, REPO_REVIEW_PATH, REPO_REVIEW_NAME, CONTEXT_REVIEW_PATH, REVIEW_REQUEST_PATH, LEGACY_REVIEW_REQUEST_PATH, REVIEW_REQUEST_NAME, REVIEW_REQUEST_PREFILL, LEGACY_REVIEW_TEMPLATE_MARKERS;
15221
15286
  var init_request_review = __esm({
15222
15287
  "src/review/request-review.ts"() {
15223
15288
  "use strict";
15224
- import_node_crypto8 = require("crypto");
15289
+ import_node_crypto9 = require("crypto");
15225
15290
  import_node_fs38 = require("fs");
15226
15291
  import_node_path38 = require("path");
15227
15292
  init_global_workspace();
@@ -18092,7 +18157,7 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
18092
18157
  `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
18093
18158
  ].join("\n");
18094
18159
  return {
18095
- id: (0, import_node_crypto9.randomUUID)(),
18160
+ id: (0, import_node_crypto10.randomUUID)(),
18096
18161
  name: "Orchestration quota handoff.md",
18097
18162
  kind: "transcript",
18098
18163
  content: body
@@ -18113,11 +18178,11 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
18113
18178
  sourceType: "orchestration"
18114
18179
  });
18115
18180
  }
18116
- var import_node_crypto9, QUOTA_CONTINUE_PROMPT, QUOTA_RESUME_PROMPT;
18181
+ var import_node_crypto10, QUOTA_CONTINUE_PROMPT, QUOTA_RESUME_PROMPT;
18117
18182
  var init_quota_failover = __esm({
18118
18183
  "src/orchestrator/quota-failover.ts"() {
18119
18184
  "use strict";
18120
- import_node_crypto9 = require("crypto");
18185
+ import_node_crypto10 = require("crypto");
18121
18186
  init_session_quota();
18122
18187
  init_app_settings();
18123
18188
  init_global_workspace();
@@ -21039,6 +21104,7 @@ var init_orchestrator = __esm({
21039
21104
  init_setup_last_error();
21040
21105
  init_request_review();
21041
21106
  init_fork_worktree();
21107
+ init_worktree_runtime();
21042
21108
  init_quota_failover();
21043
21109
  init_adopt();
21044
21110
  init_stack_layers();
@@ -22204,22 +22270,25 @@ var init_orchestrator = __esm({
22204
22270
  "No run script found for this worktree. Add [scripts.run.*] in the worktree\u2019s .sideboard/settings.toml or .conductor/settings.toml (or settings.local.toml on the main repo)."
22205
22271
  );
22206
22272
  }
22207
- const runKey = `${thread.id}:run:${resolvedName}`;
22208
- const existing = this.processes.get(runKey);
22209
- const active = (thread.activeRuns ?? []).find((r) => r.scriptName === resolvedName);
22273
+ const siblings = threadsSharingWorktree(thread.worktreePath);
22274
+ const shared = mergeWorktreeActiveRuns(siblings);
22275
+ const runKey = worktreeRunProcessKey(thread.worktreePath, resolvedName);
22276
+ const existing = this.processes.get(runKey) ?? siblings.map((t) => this.processes.get(`${t.id}:run:${resolvedName}`)).find(Boolean);
22277
+ const active = shared.activeRuns.find((r) => r.scriptName === resolvedName);
22210
22278
  if (existing && active) {
22211
22279
  return { port: active.port, scriptName: resolvedName, ports: active.ports };
22212
22280
  }
22213
22281
  if (!scriptName) {
22214
- const legacy = this.processes.get(`${thread.id}:dev`);
22215
- if (legacy && thread.devPort) {
22216
- return { port: thread.devPort, scriptName: resolvedName, ports: [thread.devPort] };
22282
+ const legacy = this.processes.get(worktreeDevProcessKey(thread.worktreePath)) ?? siblings.map((t) => this.processes.get(`${t.id}:dev`)).find(Boolean);
22283
+ if (legacy && shared.devPort) {
22284
+ return { port: shared.devPort, scriptName: resolvedName, ports: [shared.devPort] };
22217
22285
  }
22218
22286
  }
22219
22287
  const mode = getRunMode(thread.worktreePath, thread.repoPath);
22220
22288
  if (mode === "nonconcurrent") {
22289
+ const here = normalizeWorktreePath(thread.worktreePath);
22221
22290
  for (const t of listThreads()) {
22222
- if (t.id === thread.id) continue;
22291
+ if (normalizeWorktreePath(t.worktreePath) === here) continue;
22223
22292
  const runs = t.activeRuns ?? [];
22224
22293
  if (runs.length > 0 || t.devPort) {
22225
22294
  throw new Error(
@@ -22254,7 +22323,7 @@ var init_orchestrator = __esm({
22254
22323
  });
22255
22324
  const isDefault = scripts.find((s) => s.default)?.name === resolvedName || !scripts.some((s) => s.default) && (resolvedName === "dev" || resolvedName === scripts[0]?.name);
22256
22325
  if (isDefault) {
22257
- this.processes.set(`${thread.id}:dev`, {
22326
+ this.processes.set(worktreeDevProcessKey(thread.worktreePath), {
22258
22327
  kind: "dev",
22259
22328
  pid: handle.pid,
22260
22329
  startedAt,
@@ -22269,13 +22338,14 @@ var init_orchestrator = __esm({
22269
22338
  startedAt
22270
22339
  };
22271
22340
  const nextRuns = [
22272
- ...(thread.activeRuns ?? []).filter((r) => r.scriptName !== resolvedName),
22341
+ ...shared.activeRuns.filter((r) => r.scriptName !== resolvedName),
22273
22342
  run2
22274
22343
  ];
22275
- updateThread(thread.id, {
22276
- activeRuns: nextRuns,
22277
- devPort: isDefault ? handle.port : thread.devPort
22278
- });
22344
+ this.syncWorktreeRuns(
22345
+ thread.worktreePath,
22346
+ nextRuns,
22347
+ isDefault ? handle.port : shared.devPort
22348
+ );
22279
22349
  this.emit({
22280
22350
  type: "dev_server_started",
22281
22351
  threadId: thread.id,
@@ -22284,15 +22354,14 @@ var init_orchestrator = __esm({
22284
22354
  });
22285
22355
  void handle.done.then(() => {
22286
22356
  this.processes.delete(runKey);
22287
- if (isDefault) this.processes.delete(`${thread.id}:dev`);
22288
- const latest = readThread(thread.id);
22289
- const remaining = (latest?.activeRuns ?? []).filter(
22290
- (r) => r.scriptName !== resolvedName
22357
+ if (isDefault) this.processes.delete(worktreeDevProcessKey(thread.worktreePath));
22358
+ const latest = mergeWorktreeActiveRuns(threadsSharingWorktree(thread.worktreePath));
22359
+ const remaining = latest.activeRuns.filter((r) => r.scriptName !== resolvedName);
22360
+ this.syncWorktreeRuns(
22361
+ thread.worktreePath,
22362
+ remaining,
22363
+ isDefault ? null : latest.devPort
22291
22364
  );
22292
- updateThread(thread.id, {
22293
- activeRuns: remaining,
22294
- devPort: isDefault ? null : latest?.devPort ?? null
22295
- });
22296
22365
  this.emit({
22297
22366
  type: "dev_server_stopped",
22298
22367
  threadId: thread.id,
@@ -22303,51 +22372,82 @@ var init_orchestrator = __esm({
22303
22372
  }
22304
22373
  stopDev(threadRef, scriptName) {
22305
22374
  const thread = this.requireThread(threadRef);
22375
+ const siblings = threadsSharingWorktree(thread.worktreePath);
22376
+ const shared = mergeWorktreeActiveRuns(siblings);
22306
22377
  if (scriptName) {
22307
- const runKey = `${thread.id}:run:${scriptName}`;
22308
- const proc = this.processes.get(runKey);
22309
- if (proc) proc.kill();
22310
- this.processes.delete(runKey);
22311
- const remaining = (thread.activeRuns ?? []).filter((r) => r.scriptName !== scriptName);
22312
- const isPrimary = thread.devPort != null && thread.activeRuns?.find((r) => r.scriptName === scriptName)?.port === thread.devPort;
22313
- updateThread(thread.id, {
22314
- activeRuns: remaining,
22315
- devPort: isPrimary ? null : thread.devPort
22316
- });
22378
+ const keys = [
22379
+ worktreeRunProcessKey(thread.worktreePath, scriptName),
22380
+ ...siblings.map((t) => `${t.id}:run:${scriptName}`)
22381
+ ];
22382
+ for (const key of keys) {
22383
+ const proc = this.processes.get(key);
22384
+ if (proc) proc.kill();
22385
+ this.processes.delete(key);
22386
+ }
22387
+ const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
22388
+ const isPrimary = shared.devPort != null && shared.activeRuns.find((r) => r.scriptName === scriptName)?.port === shared.devPort;
22389
+ this.syncWorktreeRuns(
22390
+ thread.worktreePath,
22391
+ remaining,
22392
+ isPrimary ? null : shared.devPort
22393
+ );
22317
22394
  this.emit({ type: "dev_server_stopped", threadId: thread.id, scriptName });
22318
22395
  return;
22319
22396
  }
22320
22397
  for (const [key, proc] of [...this.processes.entries()]) {
22321
- if (key.startsWith(`${thread.id}:run:`) || key === `${thread.id}:dev`) {
22398
+ if (isWorktreeRunProcessKey(key, thread.worktreePath) || siblings.some((t) => key.startsWith(`${t.id}:run:`) || key === `${t.id}:dev`)) {
22322
22399
  proc.kill();
22323
22400
  this.processes.delete(key);
22324
22401
  }
22325
22402
  }
22326
- updateThread(thread.id, { activeRuns: [], devPort: null });
22403
+ this.syncWorktreeRuns(thread.worktreePath, [], null);
22327
22404
  this.emit({ type: "dev_server_stopped", threadId: thread.id });
22328
22405
  }
22406
+ syncWorktreeRuns(worktreePath, activeRuns, devPort) {
22407
+ for (const sibling of threadsSharingWorktree(worktreePath)) {
22408
+ updateThread(sibling.id, { activeRuns, devPort });
22409
+ }
22410
+ }
22329
22411
  listThreadRunScripts(threadRef) {
22330
22412
  const thread = this.requireThread(threadRef);
22331
22413
  return listRunScripts(thread.worktreePath, thread.repoPath);
22332
22414
  }
22333
22415
  getActiveRuns(threadRef) {
22334
- return this.requireThread(threadRef).activeRuns ?? [];
22416
+ const thread = this.requireThread(threadRef);
22417
+ return mergeWorktreeActiveRuns(threadsSharingWorktree(thread.worktreePath)).activeRuns;
22335
22418
  }
22336
22419
  getSetupLog(threadRef) {
22337
22420
  const thread = this.getThread(threadRef);
22338
22421
  if (!thread) return readSetupLog(threadRef);
22339
- const snap = readSetupLog(thread.id);
22340
- if (snap.running && !this.processes.has(`${thread.id}:setup`)) {
22422
+ const siblingIds = [
22423
+ thread.id,
22424
+ ...threadsSharingWorktree(thread.worktreePath).map((t) => t.id)
22425
+ ];
22426
+ const snap = pickRichestSetupLog([
22427
+ readSetupLog(setupLogKeyForWorktree(thread.worktreePath)),
22428
+ ...[...new Set(siblingIds)].map((id) => readSetupLog(id))
22429
+ ]) ?? readSetupLog(thread.id);
22430
+ if (snap.running && !this.hasWorktreeSetupProcess(thread.worktreePath)) {
22341
22431
  return { ...snap, running: false };
22342
22432
  }
22343
22433
  return snap;
22344
22434
  }
22435
+ hasWorktreeSetupProcess(worktreePath) {
22436
+ if (this.processes.has(worktreeSetupProcessKey(worktreePath))) return true;
22437
+ return threadsSharingWorktree(worktreePath).some(
22438
+ (t) => this.processes.has(`${t.id}:setup`)
22439
+ );
22440
+ }
22441
+ writeSetupLog(threadId, worktreePath, fn) {
22442
+ fn(setupLogKeyForWorktree(worktreePath));
22443
+ fn(threadId);
22444
+ }
22345
22445
  async runSetup(threadRef, opts) {
22346
22446
  const thread = this.requireThread(threadRef);
22347
22447
  this.assertNotGlobal(thread, "Setup");
22348
- const key = `${thread.id}:setup`;
22349
- if (this.processes.has(key)) {
22350
- throw new Error("Setup already running for this thread");
22448
+ const key = worktreeSetupProcessKey(thread.worktreePath);
22449
+ if (this.hasWorktreeSetupProcess(thread.worktreePath)) {
22450
+ throw new Error("Setup already running for this worktree");
22351
22451
  }
22352
22452
  const abort = new AbortController();
22353
22453
  this.processes.set(key, {
@@ -22355,13 +22455,17 @@ var init_orchestrator = __esm({
22355
22455
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
22356
22456
  kill: () => abort.abort()
22357
22457
  });
22358
- beginSetupLog(thread.id);
22458
+ this.writeSetupLog(thread.id, thread.worktreePath, beginSetupLog);
22359
22459
  this.emit({ type: "setup_started", threadId: thread.id });
22360
22460
  let finished = false;
22361
22461
  const finish = (exitCode, source) => {
22362
22462
  if (finished) return;
22363
22463
  finished = true;
22364
- finishSetupLog(thread.id, exitCode, source);
22464
+ this.writeSetupLog(
22465
+ thread.id,
22466
+ thread.worktreePath,
22467
+ (logKey) => finishSetupLog(logKey, exitCode, source)
22468
+ );
22365
22469
  this.emit({ type: "setup_finished", threadId: thread.id, exitCode });
22366
22470
  };
22367
22471
  try {
@@ -22369,14 +22473,22 @@ var init_orchestrator = __esm({
22369
22473
  thread.repoPath,
22370
22474
  thread.worktreePath,
22371
22475
  (line) => {
22372
- appendSetupLog(thread.id, line);
22476
+ this.writeSetupLog(
22477
+ thread.id,
22478
+ thread.worktreePath,
22479
+ (logKey) => appendSetupLog(logKey, line)
22480
+ );
22373
22481
  this.emit({ type: "setup_output", threadId: thread.id, line });
22374
22482
  },
22375
22483
  { signal: abort.signal }
22376
22484
  );
22377
22485
  if (!setup.ran) {
22378
22486
  const line = "No setup script in .sideboard/settings.toml, .conductor/settings.toml, .cursor/worktrees.json, or script/setup (bin/setup, scripts/setup)";
22379
- appendSetupLog(thread.id, line);
22487
+ this.writeSetupLog(
22488
+ thread.id,
22489
+ thread.worktreePath,
22490
+ (logKey) => appendSetupLog(logKey, line)
22491
+ );
22380
22492
  this.emit({ type: "setup_output", threadId: thread.id, line });
22381
22493
  finish(null, null);
22382
22494
  throw new Error(line);
@@ -22404,8 +22516,14 @@ var init_orchestrator = __esm({
22404
22516
  }
22405
22517
  cancelSetup(threadRef) {
22406
22518
  const thread = this.requireThread(threadRef);
22407
- const proc = this.processes.get(`${thread.id}:setup`);
22408
- if (proc) proc.kill();
22519
+ const keys = [
22520
+ worktreeSetupProcessKey(thread.worktreePath),
22521
+ ...threadsSharingWorktree(thread.worktreePath).map((t) => `${t.id}:setup`)
22522
+ ];
22523
+ for (const key of [...new Set(keys)]) {
22524
+ const proc = this.processes.get(key);
22525
+ if (proc) proc.kill();
22526
+ }
22409
22527
  }
22410
22528
  async applyIntoMain(threadRef, opts) {
22411
22529
  const thread = this.requireThread(threadRef);
@@ -24205,7 +24323,7 @@ function issueActivityKind(issue, sinceIso) {
24205
24323
  }
24206
24324
 
24207
24325
  // src/integrations/linear-oauth.ts
24208
- var import_node_crypto10 = require("crypto");
24326
+ var import_node_crypto11 = require("crypto");
24209
24327
  var import_node_http = require("http");
24210
24328
  init_app_settings();
24211
24329
  init_fetch();
@@ -24237,8 +24355,8 @@ function linearOAuthCredentials() {
24237
24355
  return { clientId, clientSecret };
24238
24356
  }
24239
24357
  function createLinearPkce() {
24240
- const verifier = (0, import_node_crypto10.randomBytes)(32).toString("base64url");
24241
- const challenge = (0, import_node_crypto10.createHash)("sha256").update(verifier).digest("base64url");
24358
+ const verifier = (0, import_node_crypto11.randomBytes)(32).toString("base64url");
24359
+ const challenge = (0, import_node_crypto11.createHash)("sha256").update(verifier).digest("base64url");
24242
24360
  return { verifier, challenge };
24243
24361
  }
24244
24362
  function linearOAuthAuthorizeUrl(clientId, state, codeChallenge) {
@@ -24369,7 +24487,7 @@ async function startLinearOAuth(opts) {
24369
24487
  throw new LinearOAuthCancelledError();
24370
24488
  }
24371
24489
  const { clientId, clientSecret } = linearOAuthCredentials();
24372
- const state = (0, import_node_crypto10.randomBytes)(16).toString("hex");
24490
+ const state = (0, import_node_crypto11.randomBytes)(16).toString("hex");
24373
24491
  const pkce = createLinearPkce();
24374
24492
  const authorizeUrl = linearOAuthAuthorizeUrl(clientId, state, pkce.challenge);
24375
24493
  const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
@@ -26222,7 +26340,7 @@ function buildPathRefAttachment(input) {
26222
26340
  init_stage_files();
26223
26341
 
26224
26342
  // src/composer/pasted-text.ts
26225
- var import_node_crypto11 = require("crypto");
26343
+ var import_node_crypto12 = require("crypto");
26226
26344
  var PASTE_ATTACH_MIN_CHARS = 1200;
26227
26345
  var PASTE_ATTACH_MIN_LINES = 15;
26228
26346
  var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
@@ -26249,7 +26367,7 @@ function nextPastedTextName(existing) {
26249
26367
  }
26250
26368
  function buildPastedTextAttachment(text7, opts) {
26251
26369
  return {
26252
- id: opts?.id ?? (0, import_node_crypto11.randomUUID)(),
26370
+ id: opts?.id ?? (0, import_node_crypto12.randomUUID)(),
26253
26371
  name: opts?.name ?? "Pasted text #1.txt",
26254
26372
  kind: "file",
26255
26373
  path: opts?.path,
@@ -26403,7 +26521,7 @@ init_orphan_cleanup();
26403
26521
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
26404
26522
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
26405
26523
  var import_zod9 = require("zod");
26406
- var import_node_crypto13 = require("crypto");
26524
+ var import_node_crypto14 = require("crypto");
26407
26525
  var import_node_path52 = require("path");
26408
26526
  init_orchestrator();
26409
26527
  init_worktree();
@@ -28130,7 +28248,7 @@ init_worktree();
28130
28248
  init_paths();
28131
28249
 
28132
28250
  // src/board/board-pins.ts
28133
- var import_node_crypto12 = require("crypto");
28251
+ var import_node_crypto13 = require("crypto");
28134
28252
  var import_node_fs54 = require("fs");
28135
28253
  var import_node_path50 = require("path");
28136
28254
  init_paths();
@@ -28162,7 +28280,7 @@ function addBoardPin(input) {
28162
28280
  const ref = input.ref.trim();
28163
28281
  if (!ref) throw new Error("Board item needs a ref");
28164
28282
  const pin = {
28165
- id: (0, import_node_crypto12.randomUUID)(),
28283
+ id: (0, import_node_crypto13.randomUUID)(),
28166
28284
  kind: input.kind,
28167
28285
  ref,
28168
28286
  repoPath: input.repoPath.trim(),
@@ -28996,7 +29114,7 @@ async function startMcpServer() {
28996
29114
  const ident = issue?.identifier ?? ref.trim();
28997
29115
  const attachments = issue ? [
28998
29116
  {
28999
- id: (0, import_node_crypto13.randomUUID)(),
29117
+ id: (0, import_node_crypto14.randomUUID)(),
29000
29118
  name: issue.identifier,
29001
29119
  kind: "issue",
29002
29120
  content: [
@@ -29951,7 +30069,7 @@ init_orch_mcp_isolation();
29951
30069
  init_workspaces2();
29952
30070
 
29953
30071
  // src/slack/oauth.ts
29954
- var import_node_crypto14 = require("crypto");
30072
+ var import_node_crypto15 = require("crypto");
29955
30073
  init_app_settings();
29956
30074
  init_workspaces2();
29957
30075
 
@@ -30175,7 +30293,7 @@ async function startSlackOAuth(opts) {
30175
30293
  throw new SlackOAuthCancelledError();
30176
30294
  }
30177
30295
  const { clientId } = slackOAuthCredentials();
30178
- const state = (0, import_node_crypto14.randomBytes)(16).toString("hex");
30296
+ const state = (0, import_node_crypto15.randomBytes)(16).toString("hex");
30179
30297
  const authorizeUrl = slackOAuthAuthorizeUrl(clientId, state);
30180
30298
  const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
30181
30299
  const pollIntervalMs = opts?.pollIntervalMs ?? 400;
@@ -30457,7 +30575,7 @@ init_workspaces2();
30457
30575
  init_reply_target();
30458
30576
 
30459
30577
  // src/slack/relay-protocol.ts
30460
- var import_node_crypto15 = require("crypto");
30578
+ var import_node_crypto16 = require("crypto");
30461
30579
  var SLACK_RELAY_PING_INTERVAL_MS = 2e4;
30462
30580
  var SLACK_RELAY_PONG_TIMEOUT_MS = 15e3;
30463
30581
  function slackRelaySessionKey(teamId, userId, deviceId) {
@@ -30467,7 +30585,7 @@ function slackRelayUserKey(teamId, userId) {
30467
30585
  return `${teamId.trim()}:${userId.trim()}`;
30468
30586
  }
30469
30587
  function newSlackRelayEventId() {
30470
- return (0, import_node_crypto15.randomBytes)(12).toString("hex");
30588
+ return (0, import_node_crypto16.randomBytes)(12).toString("hex");
30471
30589
  }
30472
30590
  function parseSlackRelayClientMessage(raw) {
30473
30591
  try {