@sideboard-ai/core 0.1.194 → 0.1.195

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);
4416
+ const out = [];
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);
4510
4442
  const out = [];
4511
- for (const p of parts) {
4512
- if (p === "..") {
4513
- out.pop();
4514
- continue;
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");
@@ -14793,6 +14799,59 @@ var init_context_compact = __esm({
14793
14799
  }
14794
14800
  });
14795
14801
 
14802
+ // src/threads/worktree-runtime.ts
14803
+ function worktreeProcessPrefix(worktreePath) {
14804
+ return `wt:${normalizeWorktreePath(worktreePath)}`;
14805
+ }
14806
+ function worktreeSetupProcessKey(worktreePath) {
14807
+ return `${worktreeProcessPrefix(worktreePath)}:setup`;
14808
+ }
14809
+ function worktreeRunProcessKey(worktreePath, scriptName) {
14810
+ return `${worktreeProcessPrefix(worktreePath)}:run:${scriptName}`;
14811
+ }
14812
+ function worktreeDevProcessKey(worktreePath) {
14813
+ return `${worktreeProcessPrefix(worktreePath)}:dev`;
14814
+ }
14815
+ function isWorktreeRunProcessKey(key, worktreePath) {
14816
+ const prefix = worktreeProcessPrefix(worktreePath);
14817
+ return key.startsWith(`${prefix}:run:`) || key === `${prefix}:dev`;
14818
+ }
14819
+ function pickRichestSetupLog(snaps) {
14820
+ let best;
14821
+ for (const snap of snaps) {
14822
+ if (!snap.output && !snap.running && snap.exitCode == null) continue;
14823
+ if (!best) {
14824
+ best = snap;
14825
+ continue;
14826
+ }
14827
+ if (snap.running && !best.running) {
14828
+ best = snap;
14829
+ continue;
14830
+ }
14831
+ if (snap.running === best.running && snap.output.length > best.output.length) {
14832
+ best = snap;
14833
+ }
14834
+ }
14835
+ return best;
14836
+ }
14837
+ function mergeWorktreeActiveRuns(threads) {
14838
+ const byName = /* @__PURE__ */ new Map();
14839
+ let devPort = null;
14840
+ for (const thread of threads) {
14841
+ if (thread.devPort != null && devPort == null) devPort = thread.devPort;
14842
+ for (const run2 of thread.activeRuns ?? []) {
14843
+ if (!byName.has(run2.scriptName)) byName.set(run2.scriptName, run2);
14844
+ }
14845
+ }
14846
+ return { activeRuns: [...byName.values()], devPort };
14847
+ }
14848
+ var init_worktree_runtime = __esm({
14849
+ "src/threads/worktree-runtime.ts"() {
14850
+ "use strict";
14851
+ init_worktree_labels();
14852
+ }
14853
+ });
14854
+
14796
14855
  // src/threads/chat-tabs.ts
14797
14856
  function sameWorktreePath(a, b) {
14798
14857
  return normalizeWorktreePath(a) === normalizeWorktreePath(b);
@@ -14855,7 +14914,7 @@ function forkMessageSlice(from, throughIndex) {
14855
14914
  function buildForkTranscriptAttachment(baseTitle, messages) {
14856
14915
  const title = baseTitle || "Chat";
14857
14916
  return {
14858
- id: (0, import_node_crypto7.randomUUID)(),
14917
+ id: (0, import_node_crypto8.randomUUID)(),
14859
14918
  name: `Transcript of ${title}.md`,
14860
14919
  kind: "transcript",
14861
14920
  content: formatTranscriptMarkdown(title, messages)
@@ -14889,7 +14948,8 @@ function createChatTab(input) {
14889
14948
  planMode: from.planMode,
14890
14949
  autonomy: input.autonomy ?? from.autonomy,
14891
14950
  attachments: input.attachments ?? [],
14892
- status: "idle"
14951
+ status: "idle",
14952
+ ...mergeWorktreeActiveRuns(threadsSharingWorktree(binding.worktreePath))
14893
14953
  });
14894
14954
  writeThread(thread);
14895
14955
  return thread;
@@ -14912,17 +14972,18 @@ function forkChatTab(input) {
14912
14972
  }
14913
14973
  return tab;
14914
14974
  }
14915
- var import_node_crypto7;
14975
+ var import_node_crypto8;
14916
14976
  var init_chat_tabs = __esm({
14917
14977
  "src/threads/chat-tabs.ts"() {
14918
14978
  "use strict";
14919
- import_node_crypto7 = require("crypto");
14979
+ import_node_crypto8 = require("crypto");
14920
14980
  init_context_compact();
14921
14981
  init_teams();
14922
14982
  init_worktree_labels();
14923
14983
  init_global_workspace();
14924
14984
  init_orchestrator_capable();
14925
14985
  init_thread_store();
14986
+ init_worktree_runtime();
14926
14987
  init_worktree_labels();
14927
14988
  }
14928
14989
  });
@@ -15182,7 +15243,7 @@ function buildReviewRequestAttachment(content, opts) {
15182
15243
  const path2 = opts?.path ?? REVIEW_SKILL_PATH;
15183
15244
  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
15245
  return {
15185
- id: (0, import_node_crypto8.randomUUID)(),
15246
+ id: (0, import_node_crypto9.randomUUID)(),
15186
15247
  name,
15187
15248
  kind: "file",
15188
15249
  path: path2,
@@ -15217,11 +15278,11 @@ async function requestReview(threadRef, send2) {
15217
15278
  const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
15218
15279
  return { tab: started, from };
15219
15280
  }
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;
15281
+ 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
15282
  var init_request_review = __esm({
15222
15283
  "src/review/request-review.ts"() {
15223
15284
  "use strict";
15224
- import_node_crypto8 = require("crypto");
15285
+ import_node_crypto9 = require("crypto");
15225
15286
  import_node_fs38 = require("fs");
15226
15287
  import_node_path38 = require("path");
15227
15288
  init_global_workspace();
@@ -18092,7 +18153,7 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
18092
18153
  `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
18093
18154
  ].join("\n");
18094
18155
  return {
18095
- id: (0, import_node_crypto9.randomUUID)(),
18156
+ id: (0, import_node_crypto10.randomUUID)(),
18096
18157
  name: "Orchestration quota handoff.md",
18097
18158
  kind: "transcript",
18098
18159
  content: body
@@ -18113,11 +18174,11 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
18113
18174
  sourceType: "orchestration"
18114
18175
  });
18115
18176
  }
18116
- var import_node_crypto9, QUOTA_CONTINUE_PROMPT, QUOTA_RESUME_PROMPT;
18177
+ var import_node_crypto10, QUOTA_CONTINUE_PROMPT, QUOTA_RESUME_PROMPT;
18117
18178
  var init_quota_failover = __esm({
18118
18179
  "src/orchestrator/quota-failover.ts"() {
18119
18180
  "use strict";
18120
- import_node_crypto9 = require("crypto");
18181
+ import_node_crypto10 = require("crypto");
18121
18182
  init_session_quota();
18122
18183
  init_app_settings();
18123
18184
  init_global_workspace();
@@ -21039,6 +21100,7 @@ var init_orchestrator = __esm({
21039
21100
  init_setup_last_error();
21040
21101
  init_request_review();
21041
21102
  init_fork_worktree();
21103
+ init_worktree_runtime();
21042
21104
  init_quota_failover();
21043
21105
  init_adopt();
21044
21106
  init_stack_layers();
@@ -22204,22 +22266,25 @@ var init_orchestrator = __esm({
22204
22266
  "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
22267
  );
22206
22268
  }
22207
- const runKey = `${thread.id}:run:${resolvedName}`;
22208
- const existing = this.processes.get(runKey);
22209
- const active = (thread.activeRuns ?? []).find((r) => r.scriptName === resolvedName);
22269
+ const siblings = threadsSharingWorktree(thread.worktreePath);
22270
+ const shared = mergeWorktreeActiveRuns(siblings);
22271
+ const runKey = worktreeRunProcessKey(thread.worktreePath, resolvedName);
22272
+ const existing = this.processes.get(runKey) ?? siblings.map((t) => this.processes.get(`${t.id}:run:${resolvedName}`)).find(Boolean);
22273
+ const active = shared.activeRuns.find((r) => r.scriptName === resolvedName);
22210
22274
  if (existing && active) {
22211
22275
  return { port: active.port, scriptName: resolvedName, ports: active.ports };
22212
22276
  }
22213
22277
  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] };
22278
+ const legacy = this.processes.get(worktreeDevProcessKey(thread.worktreePath)) ?? siblings.map((t) => this.processes.get(`${t.id}:dev`)).find(Boolean);
22279
+ if (legacy && shared.devPort) {
22280
+ return { port: shared.devPort, scriptName: resolvedName, ports: [shared.devPort] };
22217
22281
  }
22218
22282
  }
22219
22283
  const mode = getRunMode(thread.worktreePath, thread.repoPath);
22220
22284
  if (mode === "nonconcurrent") {
22285
+ const here = normalizeWorktreePath(thread.worktreePath);
22221
22286
  for (const t of listThreads()) {
22222
- if (t.id === thread.id) continue;
22287
+ if (normalizeWorktreePath(t.worktreePath) === here) continue;
22223
22288
  const runs = t.activeRuns ?? [];
22224
22289
  if (runs.length > 0 || t.devPort) {
22225
22290
  throw new Error(
@@ -22254,7 +22319,7 @@ var init_orchestrator = __esm({
22254
22319
  });
22255
22320
  const isDefault = scripts.find((s) => s.default)?.name === resolvedName || !scripts.some((s) => s.default) && (resolvedName === "dev" || resolvedName === scripts[0]?.name);
22256
22321
  if (isDefault) {
22257
- this.processes.set(`${thread.id}:dev`, {
22322
+ this.processes.set(worktreeDevProcessKey(thread.worktreePath), {
22258
22323
  kind: "dev",
22259
22324
  pid: handle.pid,
22260
22325
  startedAt,
@@ -22269,13 +22334,14 @@ var init_orchestrator = __esm({
22269
22334
  startedAt
22270
22335
  };
22271
22336
  const nextRuns = [
22272
- ...(thread.activeRuns ?? []).filter((r) => r.scriptName !== resolvedName),
22337
+ ...shared.activeRuns.filter((r) => r.scriptName !== resolvedName),
22273
22338
  run2
22274
22339
  ];
22275
- updateThread(thread.id, {
22276
- activeRuns: nextRuns,
22277
- devPort: isDefault ? handle.port : thread.devPort
22278
- });
22340
+ this.syncWorktreeRuns(
22341
+ thread.worktreePath,
22342
+ nextRuns,
22343
+ isDefault ? handle.port : shared.devPort
22344
+ );
22279
22345
  this.emit({
22280
22346
  type: "dev_server_started",
22281
22347
  threadId: thread.id,
@@ -22284,15 +22350,14 @@ var init_orchestrator = __esm({
22284
22350
  });
22285
22351
  void handle.done.then(() => {
22286
22352
  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
22353
+ if (isDefault) this.processes.delete(worktreeDevProcessKey(thread.worktreePath));
22354
+ const latest = mergeWorktreeActiveRuns(threadsSharingWorktree(thread.worktreePath));
22355
+ const remaining = latest.activeRuns.filter((r) => r.scriptName !== resolvedName);
22356
+ this.syncWorktreeRuns(
22357
+ thread.worktreePath,
22358
+ remaining,
22359
+ isDefault ? null : latest.devPort
22291
22360
  );
22292
- updateThread(thread.id, {
22293
- activeRuns: remaining,
22294
- devPort: isDefault ? null : latest?.devPort ?? null
22295
- });
22296
22361
  this.emit({
22297
22362
  type: "dev_server_stopped",
22298
22363
  threadId: thread.id,
@@ -22303,51 +22368,82 @@ var init_orchestrator = __esm({
22303
22368
  }
22304
22369
  stopDev(threadRef, scriptName) {
22305
22370
  const thread = this.requireThread(threadRef);
22371
+ const siblings = threadsSharingWorktree(thread.worktreePath);
22372
+ const shared = mergeWorktreeActiveRuns(siblings);
22306
22373
  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
- });
22374
+ const keys = [
22375
+ worktreeRunProcessKey(thread.worktreePath, scriptName),
22376
+ ...siblings.map((t) => `${t.id}:run:${scriptName}`)
22377
+ ];
22378
+ for (const key of keys) {
22379
+ const proc = this.processes.get(key);
22380
+ if (proc) proc.kill();
22381
+ this.processes.delete(key);
22382
+ }
22383
+ const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
22384
+ const isPrimary = shared.devPort != null && shared.activeRuns.find((r) => r.scriptName === scriptName)?.port === shared.devPort;
22385
+ this.syncWorktreeRuns(
22386
+ thread.worktreePath,
22387
+ remaining,
22388
+ isPrimary ? null : shared.devPort
22389
+ );
22317
22390
  this.emit({ type: "dev_server_stopped", threadId: thread.id, scriptName });
22318
22391
  return;
22319
22392
  }
22320
22393
  for (const [key, proc] of [...this.processes.entries()]) {
22321
- if (key.startsWith(`${thread.id}:run:`) || key === `${thread.id}:dev`) {
22394
+ if (isWorktreeRunProcessKey(key, thread.worktreePath) || siblings.some((t) => key.startsWith(`${t.id}:run:`) || key === `${t.id}:dev`)) {
22322
22395
  proc.kill();
22323
22396
  this.processes.delete(key);
22324
22397
  }
22325
22398
  }
22326
- updateThread(thread.id, { activeRuns: [], devPort: null });
22399
+ this.syncWorktreeRuns(thread.worktreePath, [], null);
22327
22400
  this.emit({ type: "dev_server_stopped", threadId: thread.id });
22328
22401
  }
22402
+ syncWorktreeRuns(worktreePath, activeRuns, devPort) {
22403
+ for (const sibling of threadsSharingWorktree(worktreePath)) {
22404
+ updateThread(sibling.id, { activeRuns, devPort });
22405
+ }
22406
+ }
22329
22407
  listThreadRunScripts(threadRef) {
22330
22408
  const thread = this.requireThread(threadRef);
22331
22409
  return listRunScripts(thread.worktreePath, thread.repoPath);
22332
22410
  }
22333
22411
  getActiveRuns(threadRef) {
22334
- return this.requireThread(threadRef).activeRuns ?? [];
22412
+ const thread = this.requireThread(threadRef);
22413
+ return mergeWorktreeActiveRuns(threadsSharingWorktree(thread.worktreePath)).activeRuns;
22335
22414
  }
22336
22415
  getSetupLog(threadRef) {
22337
22416
  const thread = this.getThread(threadRef);
22338
22417
  if (!thread) return readSetupLog(threadRef);
22339
- const snap = readSetupLog(thread.id);
22340
- if (snap.running && !this.processes.has(`${thread.id}:setup`)) {
22418
+ const siblingIds = [
22419
+ thread.id,
22420
+ ...threadsSharingWorktree(thread.worktreePath).map((t) => t.id)
22421
+ ];
22422
+ const snap = pickRichestSetupLog([
22423
+ readSetupLog(setupLogKeyForWorktree(thread.worktreePath)),
22424
+ ...[...new Set(siblingIds)].map((id) => readSetupLog(id))
22425
+ ]) ?? readSetupLog(thread.id);
22426
+ if (snap.running && !this.hasWorktreeSetupProcess(thread.worktreePath)) {
22341
22427
  return { ...snap, running: false };
22342
22428
  }
22343
22429
  return snap;
22344
22430
  }
22431
+ hasWorktreeSetupProcess(worktreePath) {
22432
+ if (this.processes.has(worktreeSetupProcessKey(worktreePath))) return true;
22433
+ return threadsSharingWorktree(worktreePath).some(
22434
+ (t) => this.processes.has(`${t.id}:setup`)
22435
+ );
22436
+ }
22437
+ writeSetupLog(threadId, worktreePath, fn) {
22438
+ fn(setupLogKeyForWorktree(worktreePath));
22439
+ fn(threadId);
22440
+ }
22345
22441
  async runSetup(threadRef, opts) {
22346
22442
  const thread = this.requireThread(threadRef);
22347
22443
  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");
22444
+ const key = worktreeSetupProcessKey(thread.worktreePath);
22445
+ if (this.hasWorktreeSetupProcess(thread.worktreePath)) {
22446
+ throw new Error("Setup already running for this worktree");
22351
22447
  }
22352
22448
  const abort = new AbortController();
22353
22449
  this.processes.set(key, {
@@ -22355,13 +22451,17 @@ var init_orchestrator = __esm({
22355
22451
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
22356
22452
  kill: () => abort.abort()
22357
22453
  });
22358
- beginSetupLog(thread.id);
22454
+ this.writeSetupLog(thread.id, thread.worktreePath, beginSetupLog);
22359
22455
  this.emit({ type: "setup_started", threadId: thread.id });
22360
22456
  let finished = false;
22361
22457
  const finish = (exitCode, source) => {
22362
22458
  if (finished) return;
22363
22459
  finished = true;
22364
- finishSetupLog(thread.id, exitCode, source);
22460
+ this.writeSetupLog(
22461
+ thread.id,
22462
+ thread.worktreePath,
22463
+ (logKey) => finishSetupLog(logKey, exitCode, source)
22464
+ );
22365
22465
  this.emit({ type: "setup_finished", threadId: thread.id, exitCode });
22366
22466
  };
22367
22467
  try {
@@ -22369,14 +22469,22 @@ var init_orchestrator = __esm({
22369
22469
  thread.repoPath,
22370
22470
  thread.worktreePath,
22371
22471
  (line) => {
22372
- appendSetupLog(thread.id, line);
22472
+ this.writeSetupLog(
22473
+ thread.id,
22474
+ thread.worktreePath,
22475
+ (logKey) => appendSetupLog(logKey, line)
22476
+ );
22373
22477
  this.emit({ type: "setup_output", threadId: thread.id, line });
22374
22478
  },
22375
22479
  { signal: abort.signal }
22376
22480
  );
22377
22481
  if (!setup.ran) {
22378
22482
  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);
22483
+ this.writeSetupLog(
22484
+ thread.id,
22485
+ thread.worktreePath,
22486
+ (logKey) => appendSetupLog(logKey, line)
22487
+ );
22380
22488
  this.emit({ type: "setup_output", threadId: thread.id, line });
22381
22489
  finish(null, null);
22382
22490
  throw new Error(line);
@@ -22404,8 +22512,14 @@ var init_orchestrator = __esm({
22404
22512
  }
22405
22513
  cancelSetup(threadRef) {
22406
22514
  const thread = this.requireThread(threadRef);
22407
- const proc = this.processes.get(`${thread.id}:setup`);
22408
- if (proc) proc.kill();
22515
+ const keys = [
22516
+ worktreeSetupProcessKey(thread.worktreePath),
22517
+ ...threadsSharingWorktree(thread.worktreePath).map((t) => `${t.id}:setup`)
22518
+ ];
22519
+ for (const key of [...new Set(keys)]) {
22520
+ const proc = this.processes.get(key);
22521
+ if (proc) proc.kill();
22522
+ }
22409
22523
  }
22410
22524
  async applyIntoMain(threadRef, opts) {
22411
22525
  const thread = this.requireThread(threadRef);
@@ -24205,7 +24319,7 @@ function issueActivityKind(issue, sinceIso) {
24205
24319
  }
24206
24320
 
24207
24321
  // src/integrations/linear-oauth.ts
24208
- var import_node_crypto10 = require("crypto");
24322
+ var import_node_crypto11 = require("crypto");
24209
24323
  var import_node_http = require("http");
24210
24324
  init_app_settings();
24211
24325
  init_fetch();
@@ -24237,8 +24351,8 @@ function linearOAuthCredentials() {
24237
24351
  return { clientId, clientSecret };
24238
24352
  }
24239
24353
  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");
24354
+ const verifier = (0, import_node_crypto11.randomBytes)(32).toString("base64url");
24355
+ const challenge = (0, import_node_crypto11.createHash)("sha256").update(verifier).digest("base64url");
24242
24356
  return { verifier, challenge };
24243
24357
  }
24244
24358
  function linearOAuthAuthorizeUrl(clientId, state, codeChallenge) {
@@ -24369,7 +24483,7 @@ async function startLinearOAuth(opts) {
24369
24483
  throw new LinearOAuthCancelledError();
24370
24484
  }
24371
24485
  const { clientId, clientSecret } = linearOAuthCredentials();
24372
- const state = (0, import_node_crypto10.randomBytes)(16).toString("hex");
24486
+ const state = (0, import_node_crypto11.randomBytes)(16).toString("hex");
24373
24487
  const pkce = createLinearPkce();
24374
24488
  const authorizeUrl = linearOAuthAuthorizeUrl(clientId, state, pkce.challenge);
24375
24489
  const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
@@ -26222,7 +26336,7 @@ function buildPathRefAttachment(input) {
26222
26336
  init_stage_files();
26223
26337
 
26224
26338
  // src/composer/pasted-text.ts
26225
- var import_node_crypto11 = require("crypto");
26339
+ var import_node_crypto12 = require("crypto");
26226
26340
  var PASTE_ATTACH_MIN_CHARS = 1200;
26227
26341
  var PASTE_ATTACH_MIN_LINES = 15;
26228
26342
  var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
@@ -26249,7 +26363,7 @@ function nextPastedTextName(existing) {
26249
26363
  }
26250
26364
  function buildPastedTextAttachment(text7, opts) {
26251
26365
  return {
26252
- id: opts?.id ?? (0, import_node_crypto11.randomUUID)(),
26366
+ id: opts?.id ?? (0, import_node_crypto12.randomUUID)(),
26253
26367
  name: opts?.name ?? "Pasted text #1.txt",
26254
26368
  kind: "file",
26255
26369
  path: opts?.path,
@@ -26403,7 +26517,7 @@ init_orphan_cleanup();
26403
26517
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
26404
26518
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
26405
26519
  var import_zod9 = require("zod");
26406
- var import_node_crypto13 = require("crypto");
26520
+ var import_node_crypto14 = require("crypto");
26407
26521
  var import_node_path52 = require("path");
26408
26522
  init_orchestrator();
26409
26523
  init_worktree();
@@ -28130,7 +28244,7 @@ init_worktree();
28130
28244
  init_paths();
28131
28245
 
28132
28246
  // src/board/board-pins.ts
28133
- var import_node_crypto12 = require("crypto");
28247
+ var import_node_crypto13 = require("crypto");
28134
28248
  var import_node_fs54 = require("fs");
28135
28249
  var import_node_path50 = require("path");
28136
28250
  init_paths();
@@ -28162,7 +28276,7 @@ function addBoardPin(input) {
28162
28276
  const ref = input.ref.trim();
28163
28277
  if (!ref) throw new Error("Board item needs a ref");
28164
28278
  const pin = {
28165
- id: (0, import_node_crypto12.randomUUID)(),
28279
+ id: (0, import_node_crypto13.randomUUID)(),
28166
28280
  kind: input.kind,
28167
28281
  ref,
28168
28282
  repoPath: input.repoPath.trim(),
@@ -28996,7 +29110,7 @@ async function startMcpServer() {
28996
29110
  const ident = issue?.identifier ?? ref.trim();
28997
29111
  const attachments = issue ? [
28998
29112
  {
28999
- id: (0, import_node_crypto13.randomUUID)(),
29113
+ id: (0, import_node_crypto14.randomUUID)(),
29000
29114
  name: issue.identifier,
29001
29115
  kind: "issue",
29002
29116
  content: [
@@ -29951,7 +30065,7 @@ init_orch_mcp_isolation();
29951
30065
  init_workspaces2();
29952
30066
 
29953
30067
  // src/slack/oauth.ts
29954
- var import_node_crypto14 = require("crypto");
30068
+ var import_node_crypto15 = require("crypto");
29955
30069
  init_app_settings();
29956
30070
  init_workspaces2();
29957
30071
 
@@ -30175,7 +30289,7 @@ async function startSlackOAuth(opts) {
30175
30289
  throw new SlackOAuthCancelledError();
30176
30290
  }
30177
30291
  const { clientId } = slackOAuthCredentials();
30178
- const state = (0, import_node_crypto14.randomBytes)(16).toString("hex");
30292
+ const state = (0, import_node_crypto15.randomBytes)(16).toString("hex");
30179
30293
  const authorizeUrl = slackOAuthAuthorizeUrl(clientId, state);
30180
30294
  const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
30181
30295
  const pollIntervalMs = opts?.pollIntervalMs ?? 400;
@@ -30457,7 +30571,7 @@ init_workspaces2();
30457
30571
  init_reply_target();
30458
30572
 
30459
30573
  // src/slack/relay-protocol.ts
30460
- var import_node_crypto15 = require("crypto");
30574
+ var import_node_crypto16 = require("crypto");
30461
30575
  var SLACK_RELAY_PING_INTERVAL_MS = 2e4;
30462
30576
  var SLACK_RELAY_PONG_TIMEOUT_MS = 15e3;
30463
30577
  function slackRelaySessionKey(teamId, userId, deviceId) {
@@ -30467,7 +30581,7 @@ function slackRelayUserKey(teamId, userId) {
30467
30581
  return `${teamId.trim()}:${userId.trim()}`;
30468
30582
  }
30469
30583
  function newSlackRelayEventId() {
30470
- return (0, import_node_crypto15.randomBytes)(12).toString("hex");
30584
+ return (0, import_node_crypto16.randomBytes)(12).toString("hex");
30471
30585
  }
30472
30586
  function parseSlackRelayClientMessage(raw) {
30473
30587
  try {