whipped 0.5.1 → 0.7.0

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/cli.js CHANGED
@@ -3529,7 +3529,7 @@ function isResumableSessionState(state) {
3529
3529
  function normalizeTag(raw2) {
3530
3530
  return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3531
3531
  }
3532
- var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
3532
+ var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeCopyEntrySchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
3533
3533
  var init_api_contract = __esm({
3534
3534
  "src/core/api-contract.ts"() {
3535
3535
  "use strict";
@@ -3847,6 +3847,15 @@ Do NOT include:
3847
3847
  columns: z.array(runtimeBoardColumnSchema),
3848
3848
  cards: z.record(z.string(), runtimeBoardCardSchema)
3849
3849
  });
3850
+ notificationSoundsConfigSchema = z.object({
3851
+ enabled: z.boolean().default(false),
3852
+ readyForReview: z.boolean().default(true),
3853
+ prComment: z.boolean().default(true),
3854
+ done: z.boolean().default(true),
3855
+ reopened: z.boolean().default(true),
3856
+ blocked: z.boolean().default(true),
3857
+ runError: z.boolean().default(true)
3858
+ });
3850
3859
  runtimeGlobalConfigSchema = z.object({
3851
3860
  defaultAgent: runtimeAgentIdSchema.default("claude"),
3852
3861
  maxParallelTasks: z.number().int().positive().default(4),
@@ -3855,6 +3864,7 @@ Do NOT include:
3855
3864
  pollingIntervalSeconds: z.number().int().positive().default(30),
3856
3865
  prPollingIntervalSeconds: z.number().int().positive().default(60),
3857
3866
  terminalApp: z.string().optional(),
3867
+ notificationSounds: notificationSoundsConfigSchema.prefault({}),
3858
3868
  slackEnabled: z.boolean().default(true),
3859
3869
  slackBotToken: z.string().optional(),
3860
3870
  slackSigningSecret: z.string().optional(),
@@ -3880,8 +3890,13 @@ Do NOT include:
3880
3890
  runtimeGithubConfigSchema = z.object({
3881
3891
  token: z.string()
3882
3892
  });
3893
+ runtimeWorktreeCopyEntrySchema = z.object({
3894
+ path: z.string().min(1),
3895
+ symlink: z.boolean().default(false)
3896
+ });
3883
3897
  runtimeWorktreeSetupSchema = z.object({
3884
- filesToCopy: z.array(z.string()).default([]),
3898
+ // Legacy configs stored bare strings; accept and normalize them to copy entries.
3899
+ filesToCopy: z.array(z.union([z.string().transform((path2) => ({ path: path2, symlink: false })), runtimeWorktreeCopyEntrySchema])).default([]),
3885
3900
  installCommand: z.string().default("")
3886
3901
  });
3887
3902
  runtimeProjectSecretSchema = z.object({
@@ -8973,7 +8988,7 @@ var require_tree_kill = __commonJS({
8973
8988
  "node_modules/.pnpm/tree-kill@1.2.2/node_modules/tree-kill/index.js"(exports, module) {
8974
8989
  "use strict";
8975
8990
  var childProcess4 = __require("child_process");
8976
- var spawn8 = childProcess4.spawn;
8991
+ var spawn9 = childProcess4.spawn;
8977
8992
  var exec = childProcess4.exec;
8978
8993
  module.exports = function(pid, signal, callback) {
8979
8994
  if (typeof signal === "function" && callback === void 0) {
@@ -8998,7 +9013,7 @@ var require_tree_kill = __commonJS({
8998
9013
  break;
8999
9014
  case "darwin":
9000
9015
  buildProcessTree(pid, tree, pidsToProcess, function(parentPid) {
9001
- return spawn8("pgrep", ["-P", parentPid]);
9016
+ return spawn9("pgrep", ["-P", parentPid]);
9002
9017
  }, function() {
9003
9018
  killAll(tree, signal, callback);
9004
9019
  });
@@ -9010,7 +9025,7 @@ var require_tree_kill = __commonJS({
9010
9025
  // break;
9011
9026
  default:
9012
9027
  buildProcessTree(pid, tree, pidsToProcess, function(parentPid) {
9013
- return spawn8("ps", ["-o", "pid", "--no-headers", "--ppid", parentPid]);
9028
+ return spawn9("ps", ["-o", "pid", "--no-headers", "--ppid", parentPid]);
9014
9029
  }, function() {
9015
9030
  killAll(tree, signal, callback);
9016
9031
  });
@@ -13547,11 +13562,12 @@ async function runLogs(options) {
13547
13562
  process.exit(1);
13548
13563
  }
13549
13564
  if (options.follow) {
13550
- const child = spawn2("tail", ["-f", "-n", String(options.lines), path2], {
13565
+ const [cmd, args] = process.platform === "win32" ? ["powershell", ["-NoProfile", "-Command", `Get-Content -Path '${path2}' -Tail ${options.lines} -Wait`]] : ["tail", ["-f", "-n", String(options.lines), path2]];
13566
+ const child = spawn2(cmd, args, {
13551
13567
  stdio: "inherit"
13552
13568
  });
13553
13569
  child.on("error", (err) => {
13554
- console.error(`Failed to spawn tail: ${err.message}`);
13570
+ console.error(`Failed to follow log file: ${err.message}`);
13555
13571
  process.exit(1);
13556
13572
  });
13557
13573
  child.on("exit", (code) => process.exit(code ?? 0));
@@ -13606,8 +13622,8 @@ init_logger();
13606
13622
  // src/server/runtime-server.ts
13607
13623
  import { existsSync as existsSync14, readFileSync as readFileSync8 } from "node:fs";
13608
13624
  import { createServer } from "node:http";
13609
- import { join as join21 } from "node:path";
13610
- import { fileURLToPath as fileURLToPath4 } from "node:url";
13625
+ import { join as join22 } from "node:path";
13626
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
13611
13627
  import * as nodePty2 from "node-pty";
13612
13628
 
13613
13629
  // node_modules/.pnpm/ws@8.20.0/node_modules/ws/wrapper.mjs
@@ -14387,8 +14403,55 @@ var slackNotifier = new SlackNotifier();
14387
14403
  // src/server/runtime-server.ts
14388
14404
  init_runtime_config();
14389
14405
  init_logger();
14406
+
14407
+ // src/core/shell.ts
14408
+ function getShellInvocation(command) {
14409
+ if (process.platform === "win32") {
14410
+ return [process.env.ComSpec ?? "cmd.exe", ["/c", command]];
14411
+ }
14412
+ return [process.env.SHELL ?? "/bin/sh", ["-c", command]];
14413
+ }
14414
+
14415
+ // src/server/runtime-server.ts
14390
14416
  init_task_id();
14391
14417
 
14418
+ // src/notifications/sound-player.ts
14419
+ init_runtime_config();
14420
+ init_logger();
14421
+ import { spawn as spawn4 } from "node:child_process";
14422
+ import { dirname as dirname4, join as join11 } from "node:path";
14423
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
14424
+ var SOUNDS_DIR = join11(dirname4(fileURLToPath3(import.meta.url)), "sounds");
14425
+ var SOUND_FILES = {
14426
+ readyForReview: "ready-for-review.wav",
14427
+ prComment: "pr-comment.wav",
14428
+ done: "done.wav",
14429
+ reopened: "reopened.wav",
14430
+ blocked: "blocked.wav",
14431
+ runError: "run-error.wav"
14432
+ };
14433
+ async function playNotificationSound(event) {
14434
+ try {
14435
+ const { notificationSounds } = await loadGlobalConfig();
14436
+ if (!notificationSounds.enabled || !notificationSounds[event]) return;
14437
+ playOnHost(join11(SOUNDS_DIR, SOUND_FILES[event]));
14438
+ } catch (err) {
14439
+ logger.debug({ err }, `[sound] could not play notification sound for ${event}`);
14440
+ }
14441
+ }
14442
+ function playOnHost(file) {
14443
+ const [command, args] = process.platform === "darwin" ? ["afplay", [file]] : process.platform === "win32" ? (
14444
+ // PowerShell's SoundPlayer plays the WAV chimes synchronously in the
14445
+ // detached child (it exits once done); no extra binary needed.
14446
+ ["powershell", ["-NoProfile", "-Command", `(New-Object Media.SoundPlayer '${file}').PlaySync()`]]
14447
+ ) : process.platform === "linux" ? ["paplay", [file]] : [null, null];
14448
+ if (!command) return;
14449
+ const child = spawn4(command, [...args], { stdio: "ignore", detached: true });
14450
+ child.on("error", () => {
14451
+ });
14452
+ child.unref();
14453
+ }
14454
+
14392
14455
  // src/daemon/poller.ts
14393
14456
  init_runtime_config();
14394
14457
  init_logger();
@@ -14400,7 +14463,7 @@ import { existsSync as existsSync8 } from "node:fs";
14400
14463
  import { execFile as execFile7, spawnSync } from "node:child_process";
14401
14464
  import { existsSync as existsSync5, mkdirSync as mkdirSync7, rmSync as rmSync2 } from "node:fs";
14402
14465
  import { homedir as homedir2 } from "node:os";
14403
- import { dirname as dirname4, join as join11, resolve, sep as sep2 } from "node:path";
14466
+ import { dirname as dirname5, join as join12, resolve, sep as sep2 } from "node:path";
14404
14467
  import { promisify as promisify7 } from "node:util";
14405
14468
 
14406
14469
  // node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
@@ -17953,7 +18016,7 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
17953
18016
  // src/git/merge-operations.ts
17954
18017
  init_logger();
17955
18018
  var execFileAsync5 = promisify7(execFile7);
17956
- var WORKTREES_DIR = join11(homedir2(), ".whipped", "worktrees");
18019
+ var WORKTREES_DIR = join12(homedir2(), ".whipped", "worktrees");
17957
18020
  function git(args, cwd) {
17958
18021
  const r = spawnSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
17959
18022
  return { stdout: r.stdout?.trim() ?? "", stderr: r.stderr?.trim() ?? "", ok: r.status === 0 };
@@ -18002,9 +18065,9 @@ async function commitIfDirty(worktreePath, message) {
18002
18065
  return true;
18003
18066
  }
18004
18067
  function attemptMerge(repoPath, effectiveWorktreeId, taskBranch) {
18005
- const worktreePath = join11(WORKTREES_DIR, effectiveWorktreeId);
18068
+ const worktreePath = join12(WORKTREES_DIR, effectiveWorktreeId);
18006
18069
  if (existsSync5(worktreePath)) {
18007
- rmSync2(worktreePath, { recursive: true, force: true });
18070
+ rmSync2(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
18008
18071
  git(["worktree", "prune"], repoPath);
18009
18072
  }
18010
18073
  const statusResult = git(["status", "--porcelain"], repoPath);
@@ -18040,10 +18103,10 @@ async function pushBranch(worktreePath, branch) {
18040
18103
  throw new Error(`Failed to push: ${detail}`);
18041
18104
  });
18042
18105
  }
18043
- var YOLO_DIR = join11(WORKTREES_DIR, ".yolo");
18106
+ var YOLO_DIR = join12(WORKTREES_DIR, ".yolo");
18044
18107
  function createYoloWorktree(repoPath, workspaceId, cardId, baseRef) {
18045
- const tmpPath = join11(YOLO_DIR, workspaceId, `${cardId}-${Date.now()}`);
18046
- mkdirSync7(dirname4(tmpPath), { recursive: true });
18108
+ const tmpPath = join12(YOLO_DIR, workspaceId, `${cardId}-${Date.now()}`);
18109
+ mkdirSync7(dirname5(tmpPath), { recursive: true });
18047
18110
  git(["worktree", "prune"], repoPath);
18048
18111
  const res = git(["worktree", "add", "--detach", tmpPath, baseRef], repoPath);
18049
18112
  if (!res.ok) throw new Error(`Failed to create YOLO worktree at ${tmpPath}: ${res.stderr}`);
@@ -18268,10 +18331,10 @@ init_logger();
18268
18331
  import { execFile as execFile8, spawnSync as spawnSync2 } from "node:child_process";
18269
18332
  import { existsSync as existsSync6, mkdirSync as mkdirSync8 } from "node:fs";
18270
18333
  import { rm as rm2 } from "node:fs/promises";
18271
- import { join as join12 } from "node:path";
18334
+ import { join as join13 } from "node:path";
18272
18335
  import { promisify as promisify8 } from "node:util";
18273
18336
  var execFileAsync6 = promisify8(execFile8);
18274
- var WORKTREES_DIR2 = join12(WHIPPED_HOME_DIR, "worktrees");
18337
+ var WORKTREES_DIR2 = join13(WHIPPED_HOME_DIR, "worktrees");
18275
18338
  function git2(args, cwd) {
18276
18339
  const result = spawnSync2("git", args, {
18277
18340
  cwd,
@@ -18289,7 +18352,7 @@ function getCardBranch(card) {
18289
18352
  function createWorktree(taskId, repoPath, baseRef, branchName) {
18290
18353
  mkdirSync8(WORKTREES_DIR2, { recursive: true });
18291
18354
  let branch = branchName ?? `task/${taskId}`;
18292
- const worktreePath = join12(WORKTREES_DIR2, taskId);
18355
+ const worktreePath = join13(WORKTREES_DIR2, taskId);
18293
18356
  logger.info(`[worktree:create] taskId=${taskId} branch=${branch} baseRef=${baseRef} worktreePath=${worktreePath}`);
18294
18357
  if (existsSync6(worktreePath)) {
18295
18358
  const actualBranch = git2(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
@@ -18367,12 +18430,12 @@ function createWorktree(taskId, repoPath, baseRef, branchName) {
18367
18430
  return { taskId, path: worktreePath, branch, isNew: true, conflictedFiles: [] };
18368
18431
  }
18369
18432
  async function removeWorktreeAsync(taskId, repoPath, branchName) {
18370
- const worktreePath = join12(WORKTREES_DIR2, taskId);
18433
+ const worktreePath = join13(WORKTREES_DIR2, taskId);
18371
18434
  const branch = branchName ?? `task/${taskId}`;
18372
18435
  const t0 = Date.now();
18373
18436
  logger.info(`[cleanup:${taskId}] starting worktree removal`);
18374
18437
  try {
18375
- await rm2(worktreePath, { recursive: true, force: true });
18438
+ await rm2(worktreePath, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
18376
18439
  logger.info(`[cleanup:${taskId}] rm worktree dir done (${Date.now() - t0}ms)`);
18377
18440
  } catch (err) {
18378
18441
  logger.error({ err }, `[cleanup:${taskId}] rm worktree dir failed:`);
@@ -18389,7 +18452,7 @@ async function removeWorktreeAsync(taskId, repoPath, branchName) {
18389
18452
  logger.info(`[cleanup:${taskId}] done in ${Date.now() - t0}ms`);
18390
18453
  }
18391
18454
  function getWorktreePath(taskId) {
18392
- return join12(WORKTREES_DIR2, taskId);
18455
+ return join13(WORKTREES_DIR2, taskId);
18393
18456
  }
18394
18457
  function resolveWorktreeOwnerId(cardId, cards) {
18395
18458
  const story = Object.values(cards).find((c) => c.type === "story" && (c.subtaskIds ?? []).includes(cardId));
@@ -18849,6 +18912,7 @@ var BoardPoller = class {
18849
18912
  `[poller] ${readyEntries.length} new comment(s) from GitHub PR for "${card.description?.split("\n")[0]?.slice(0, 60) ?? card.id}"`
18850
18913
  );
18851
18914
  await appendActivityLog(workspaceId, taskId, `${readyEntries.length} new comment(s) imported from GitHub PR`);
18915
+ void playNotificationSound("prComment");
18852
18916
  }
18853
18917
  updated = true;
18854
18918
  }
@@ -18859,6 +18923,7 @@ var BoardPoller = class {
18859
18923
  await moveCard(workspaceId, taskId, "done");
18860
18924
  await clearCardSession(workspaceId, taskId);
18861
18925
  await appendActivityLog(workspaceId, taskId, "PR merged on GitHub \u2192 Done");
18926
+ void playNotificationSound("done");
18862
18927
  const boardAfterDone = await loadBoard(workspaceId);
18863
18928
  const ownerId = resolveWorktreeOwnerId(taskId, boardAfterDone.cards);
18864
18929
  const groupCards = Object.values(boardAfterDone.cards).filter(
@@ -18876,6 +18941,7 @@ var BoardPoller = class {
18876
18941
  await moveCard(workspaceId, taskId, "blocked");
18877
18942
  await clearCardSession(workspaceId, taskId);
18878
18943
  await appendActivityLog(workspaceId, taskId, "PR closed without merging \u2192 Blocked");
18944
+ void playNotificationSound("blocked");
18879
18945
  updated = true;
18880
18946
  } else if (info2.mergeable === "CONFLICTING") {
18881
18947
  if (!card.terminalSessions?.some((ts) => !ts.endedAt)) {
@@ -18892,6 +18958,7 @@ var BoardPoller = class {
18892
18958
  await moveCard(workspaceId, taskId, "reopened");
18893
18959
  await appendActivityLog(workspaceId, taskId, `${reason} \u2192 Reopened`);
18894
18960
  await clearCardSession(workspaceId, taskId);
18961
+ void playNotificationSound("reopened");
18895
18962
  updated = true;
18896
18963
  }
18897
18964
  if (updated) stateHub.broadcastWorkspaceUpdate(workspaceId);
@@ -19002,10 +19069,10 @@ function buildCodexEffortOverride(effort) {
19002
19069
 
19003
19070
  // src/agents/playwright-mcp.ts
19004
19071
  init_runtime_config();
19005
- import { join as join13 } from "node:path";
19072
+ import { join as join14 } from "node:path";
19006
19073
  var PLAYWRIGHT_MCP_SERVER_NAME = "playwright";
19007
19074
  function buildBrowserMcpServer(cardId) {
19008
- const outputDir = join13(ATTACHMENTS_DIR, cardId);
19075
+ const outputDir = join14(ATTACHMENTS_DIR, cardId);
19009
19076
  return {
19010
19077
  command: "npx",
19011
19078
  args: ["-y", "@playwright/mcp", "--headless", "--isolated", "--output-dir", outputDir],
@@ -19102,12 +19169,34 @@ function getAvailableAgents() {
19102
19169
  }
19103
19170
  });
19104
19171
  }
19172
+ var resolvedCommandCache = /* @__PURE__ */ new Map();
19173
+ function resolveCommandPath(command) {
19174
+ if (process.platform !== "win32") return command;
19175
+ const cached2 = resolvedCommandCache.get(command);
19176
+ if (cached2) return cached2;
19177
+ try {
19178
+ const result = spawnSync4("where.exe", [command], {
19179
+ stdio: ["ignore", "pipe", "ignore"],
19180
+ timeout: 5e3,
19181
+ encoding: "utf-8"
19182
+ });
19183
+ if (result.status === 0 && result.stdout) {
19184
+ const first = result.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)[0];
19185
+ if (first) {
19186
+ resolvedCommandCache.set(command, first);
19187
+ return first;
19188
+ }
19189
+ }
19190
+ } catch {
19191
+ }
19192
+ return command;
19193
+ }
19105
19194
  function getAgentCommand(agentId) {
19106
19195
  const agent = AGENT_DEFINITIONS.find((a) => a.id === agentId);
19107
19196
  if (!agent) {
19108
19197
  throw new Error(`Unknown agent: ${agentId}`);
19109
19198
  }
19110
- return agent.command;
19199
+ return resolveCommandPath(agent.command);
19111
19200
  }
19112
19201
  var READONLY_DISALLOWED_TOOLS = ["Edit", "Write", "NotebookEdit", "Bash"];
19113
19202
  function buildAgentArgs(agentId, prompt, ctx = {}) {
@@ -20389,22 +20478,22 @@ init_workspace_state();
20389
20478
  init_workspace_state();
20390
20479
 
20391
20480
  // src/daemon/scheduler.ts
20392
- import { spawn as spawn5 } from "node:child_process";
20393
- import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
20394
- import { unlink as unlink2 } from "node:fs/promises";
20395
- import { dirname as dirname5, join as join16, resolve as resolve2 } from "node:path";
20396
- import { fileURLToPath as fileURLToPath3 } from "node:url";
20481
+ import { spawn as spawn6 } from "node:child_process";
20482
+ import { existsSync as existsSync10 } from "node:fs";
20483
+ import { cp, link, mkdir as mkdir3, stat as stat2, symlink, unlink as unlink2 } from "node:fs/promises";
20484
+ import { dirname as dirname6, join as join17, resolve as resolve2 } from "node:path";
20485
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
20397
20486
  init_api_contract();
20398
20487
  init_logger();
20399
20488
 
20400
20489
  // src/core/prompt-resolver.ts
20401
20490
  init_logger();
20402
20491
  import { readFileSync as readFileSync5 } from "node:fs";
20403
- import { isAbsolute, join as join14 } from "node:path";
20492
+ import { isAbsolute, join as join15 } from "node:path";
20404
20493
  function resolvePromptText(prompt, repoPath) {
20405
20494
  if (!prompt) return "";
20406
20495
  if (prompt.source === "inline") return prompt.text;
20407
- const path2 = isAbsolute(prompt.path) ? prompt.path : join14(repoPath, prompt.path);
20496
+ const path2 = isAbsolute(prompt.path) ? prompt.path : join15(repoPath, prompt.path);
20408
20497
  try {
20409
20498
  return readFileSync5(path2, "utf-8");
20410
20499
  } catch (err) {
@@ -20421,7 +20510,7 @@ init_workspace_state();
20421
20510
  import { spawnSync as spawnSync5 } from "node:child_process";
20422
20511
  import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
20423
20512
  import { readdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
20424
- import { join as join15 } from "node:path";
20513
+ import { join as join16 } from "node:path";
20425
20514
  init_runtime_config();
20426
20515
  init_api_contract();
20427
20516
  init_logger();
@@ -20579,7 +20668,7 @@ async function runReviewSlot(slot, card, streamId, options, customPrompt) {
20579
20668
  useIncremental,
20580
20669
  diffRef: priorReviewedSha ?? card.baseRef
20581
20670
  };
20582
- const stat2 = getGitStat(worktreePath, card.baseRef);
20671
+ const stat3 = getGitStat(worktreePath, card.baseRef);
20583
20672
  const fullDiff = getGitFullDiff(worktreePath, useIncremental ? priorReviewedSha : card.baseRef);
20584
20673
  const context = formatPriorComments(card);
20585
20674
  let subtaskCards = [];
@@ -20589,7 +20678,7 @@ async function runReviewSlot(slot, card, streamId, options, customPrompt) {
20589
20678
  const rawSystemPrompt = buildReviewSlotSystemPrompt(
20590
20679
  slot,
20591
20680
  card,
20592
- stat2,
20681
+ stat3,
20593
20682
  fullDiff,
20594
20683
  customPrompt,
20595
20684
  context.text,
@@ -20706,7 +20795,7 @@ function getSlotTriggerWord(type) {
20706
20795
  }
20707
20796
  var SCREENSHOT_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp"]);
20708
20797
  async function attachBrowserArtifacts(workspaceId, card, result, since) {
20709
- const dir = join15(ATTACHMENTS_DIR, card.id);
20798
+ const dir = join16(ATTACHMENTS_DIR, card.id);
20710
20799
  let entries;
20711
20800
  try {
20712
20801
  entries = await readdir(dir);
@@ -20719,7 +20808,7 @@ async function attachBrowserArtifacts(workspaceId, card, result, since) {
20719
20808
  for (const name of entries) {
20720
20809
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
20721
20810
  if (!SCREENSHOT_EXTENSIONS.has(ext)) continue;
20722
- const filePath = join15(dir, name);
20811
+ const filePath = join16(dir, name);
20723
20812
  try {
20724
20813
  const info2 = await stat(filePath);
20725
20814
  if (!info2.isFile() || info2.mtimeMs < since) continue;
@@ -20862,6 +20951,7 @@ async function handleReviewSuccess(card, options) {
20862
20951
  await moveCard(workspaceId, card.id, "ready_for_review");
20863
20952
  await appendActivityLog(workspaceId, card.id, "All reviews passed \u2192 moved to Ready for Review");
20864
20953
  stateHub.broadcastWorkspaceUpdate(workspaceId);
20954
+ void playNotificationSound("readyForReview");
20865
20955
  if (deliveryMode === "off") return;
20866
20956
  if (card.type === "subtask") return;
20867
20957
  if (deliveryMode === "yolo") {
@@ -21022,7 +21112,7 @@ ${diffParts.join("\n")}
21022
21112
  if (newUntracked.length > 0) {
21023
21113
  const newFileContents = [];
21024
21114
  for (const file of newUntracked) {
21025
- const content = readFileSafe(join15(worktreePath, file));
21115
+ const content = readFileSafe(join16(worktreePath, file));
21026
21116
  const ext = file.split(".").pop() ?? "";
21027
21117
  newFileContents.push(content ? `### ${file}
21028
21118
  \`\`\`${ext}
@@ -21170,16 +21260,16 @@ ${fullDiff}`;
21170
21260
  return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
21171
21261
  }
21172
21262
  var FOLLOWUP_REVIEW_FOCUS = `**This is a follow-up review.** An earlier version of this work already passed review in a prior round \u2014 only the \`## New Feedback\` / \`## Current Iteration\` items triggered this run. Re-review ONLY: (1) whether those items were addressed in the changes below, and (2) whether the new changes introduced a regression or broke a caller (grep callers of anything touched to confirm). Do NOT re-litigate previously-approved code or raise issues unrelated to this round's feedback.`;
21173
- function renderReviewDiff(stat2, fullDiff, baseRef, scope) {
21263
+ function renderReviewDiff(stat3, fullDiff, baseRef, scope) {
21174
21264
  if (!scope.useIncremental) {
21175
21265
  return `## Changed files
21176
- ${stat2}
21266
+ ${stat3}
21177
21267
 
21178
21268
  ## Diff
21179
21269
  ${formatDiffBlock(fullDiff, baseRef)}`;
21180
21270
  }
21181
21271
  return `## Changed files (full changeset vs \`${baseRef}\`, for context)
21182
- ${stat2}
21272
+ ${stat3}
21183
21273
 
21184
21274
  ## Changes since your last review (review THIS)
21185
21275
  _The diff below is only what changed since you last reviewed this card. The full changeset is summarised above; run \`git diff ${baseRef}...HEAD\` only if you need it to chase a regression._
@@ -21219,11 +21309,11 @@ Revise these values rather than rewriting from scratch, unless they no longer re
21219
21309
  let statSection = "";
21220
21310
  if (worktreePath) {
21221
21311
  if (fullDiff) {
21222
- const stat2 = getGitStat(worktreePath, statBase);
21312
+ const stat3 = getGitStat(worktreePath, statBase);
21223
21313
  statSection = `
21224
21314
 
21225
21315
  ## Current worktree state (vs ${statBase})
21226
- ${stat2}
21316
+ ${stat3}
21227
21317
 
21228
21318
  ## Diff (vs ${statBase})
21229
21319
  ${formatDiffBlock(fullDiff, statBase, "Git diff")}`;
@@ -21234,11 +21324,11 @@ ${formatDiffBlock(fullDiff, statBase, "Git diff")}`;
21234
21324
 
21235
21325
  This is the initial dev run on this card. The worktree is clean and branched from \`${statBase}\` \u2014 there is no prior diff to inspect. Skip \`git diff\` and start implementing.`;
21236
21326
  } else {
21237
- const stat2 = getGitStat(worktreePath, statBase);
21327
+ const stat3 = getGitStat(worktreePath, statBase);
21238
21328
  statSection = `
21239
21329
 
21240
21330
  ## Current worktree state (vs ${statBase})
21241
- ${stat2}`;
21331
+ ${stat3}`;
21242
21332
  }
21243
21333
  }
21244
21334
  const descAttachNote = (card.descriptionAttachments?.length ?? 0) > 0 ? `
@@ -21347,12 +21437,12 @@ ${systemPrompt.trim()}`);
21347
21437
  ${effectiveGitInstructions}`);
21348
21438
  return { text: parts.join("\n\n"), files: context.files };
21349
21439
  }
21350
- function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets = [], systemPrompt, autoCommit = true, subtaskCards = [], browserEnabled = false) {
21440
+ function buildReviewSlotSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets = [], systemPrompt, autoCommit = true, subtaskCards = [], browserEnabled = false) {
21351
21441
  if (slot.type === "orch") {
21352
21442
  return buildOrchSystemPrompt(
21353
21443
  slot,
21354
21444
  card,
21355
- stat2,
21445
+ stat3,
21356
21446
  fullDiff,
21357
21447
  customPrompt,
21358
21448
  priorContext,
@@ -21366,7 +21456,7 @@ function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt,
21366
21456
  return buildMergedReviewSystemPrompt(
21367
21457
  slot,
21368
21458
  card,
21369
- stat2,
21459
+ stat3,
21370
21460
  fullDiff,
21371
21461
  customPrompt,
21372
21462
  priorContext,
@@ -21376,7 +21466,7 @@ function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt,
21376
21466
  browserEnabled
21377
21467
  );
21378
21468
  }
21379
- function buildMergedReviewSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, browserEnabled = false) {
21469
+ function buildMergedReviewSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, browserEnabled = false) {
21380
21470
  const custom = customPrompt.trim() ? `
21381
21471
 
21382
21472
  ## Project-specific instructions
@@ -21412,7 +21502,7 @@ When you set status "fail" (reopening for rework), you can right-size the **tier
21412
21502
  ## Task to review
21413
21503
  ${card.description ?? ""}${descAttachSection}${priorContext}
21414
21504
 
21415
- ${renderReviewDiff(stat2, fullDiff, card.baseRef, scope)}
21505
+ ${renderReviewDiff(stat3, fullDiff, card.baseRef, scope)}
21416
21506
 
21417
21507
  ${ITERATION_SCOPING_NOTE}
21418
21508
 
@@ -21440,7 +21530,7 @@ ${secretsSection}` : ""}${projectContext}${runningAppSection}${levelAdjustSectio
21440
21530
 
21441
21531
  This MCP call is required \u2014 without it the pipeline has no record of your verdict.`;
21442
21532
  }
21443
- function buildOrchSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, autoCommit = true, subtaskCards = []) {
21533
+ function buildOrchSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, autoCommit = true, subtaskCards = []) {
21444
21534
  const commentType = slotCommentType(slot);
21445
21535
  const custom = customPrompt.trim() ? `
21446
21536
 
@@ -21486,7 +21576,7 @@ ${card.description}
21486
21576
 
21487
21577
  ${subtasksSection}${priorContext}
21488
21578
 
21489
- ${renderReviewDiff(stat2, fullDiff, card.baseRef, scope)}
21579
+ ${renderReviewDiff(stat3, fullDiff, card.baseRef, scope)}
21490
21580
 
21491
21581
  ${ITERATION_SCOPING_NOTE}
21492
21582
 
@@ -22018,19 +22108,33 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
22018
22108
  const { filesToCopy, installCommand } = projectConfig.worktreeSetup;
22019
22109
  if (filesToCopy.length > 0) {
22020
22110
  const copied = [];
22021
- for (const relPath of filesToCopy) {
22022
- const src = join16(repoPath, relPath);
22111
+ const linked = [];
22112
+ for (const entry of filesToCopy) {
22113
+ const src = join17(repoPath, entry.path);
22023
22114
  if (!existsSync10(src)) continue;
22024
- const dst = join16(worktree.path, relPath);
22025
- mkdirSync9(dirname5(dst), { recursive: true });
22115
+ const dst = join17(worktree.path, entry.path);
22116
+ await mkdir3(dirname6(dst), { recursive: true });
22026
22117
  try {
22027
- cpSync(src, dst, { recursive: true });
22028
- copied.push(relPath);
22029
- } catch {
22118
+ if (entry.symlink) {
22119
+ await shareIntoWorktree(src, dst);
22120
+ linked.push(entry.path);
22121
+ } else {
22122
+ await cp(src, dst, { recursive: true, dereference: true });
22123
+ copied.push(entry.path);
22124
+ }
22125
+ } catch (err) {
22126
+ logger.warn(
22127
+ { err },
22128
+ `[scheduler] worktree setup: failed to ${entry.symlink ? "link" : "copy"} ${entry.path}`
22129
+ );
22030
22130
  }
22031
22131
  }
22032
- if (copied.length > 0) {
22033
- await appendActivityLog(workspaceId, taskId, `Copied to worktree: ${copied.join(", ")}`);
22132
+ const summary = [
22133
+ copied.length ? `Copied: ${copied.join(", ")}` : "",
22134
+ linked.length ? `Linked: ${linked.join(", ")}` : ""
22135
+ ].filter(Boolean).join(" \xB7 ");
22136
+ if (summary) {
22137
+ await appendActivityLog(workspaceId, taskId, `Worktree setup \u2014 ${summary}`);
22034
22138
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22035
22139
  }
22036
22140
  }
@@ -22038,7 +22142,8 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
22038
22142
  await appendActivityLog(workspaceId, taskId, `Running: ${installCommand.trim()}`);
22039
22143
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22040
22144
  await new Promise((resolve5) => {
22041
- const proc = spawn5("sh", ["-c", installCommand.trim()], {
22145
+ const [shell, shellArgs] = getShellInvocation(installCommand.trim());
22146
+ const proc = spawn6(shell, shellArgs, {
22042
22147
  cwd: worktree.path,
22043
22148
  stdio: "ignore",
22044
22149
  env: { ...process.env, REPO_PATH: repoPath }
@@ -22242,6 +22347,7 @@ ${devSystemPromptResult.text}`;
22242
22347
  if (!hasReviewSlots) {
22243
22348
  await moveCard(workspaceId, taskId, "ready_for_review");
22244
22349
  await appendActivityLog(workspaceId, taskId, "Agent finished \u2192 moved to Ready for Review");
22350
+ void playNotificationSound("readyForReview");
22245
22351
  } else {
22246
22352
  await appendActivityLog(workspaceId, taskId, "Agent finished \u2192 AI review starting");
22247
22353
  }
@@ -22268,6 +22374,7 @@ ${devSystemPromptResult.text}`;
22268
22374
  );
22269
22375
  }
22270
22376
  await moveCard(workspaceId, taskId, destination);
22377
+ void playNotificationSound(destination === "blocked" ? "blocked" : "reopened");
22271
22378
  }
22272
22379
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22273
22380
  this.options.onTaskCompleted(taskId);
@@ -22469,6 +22576,7 @@ ${devSystemPromptResult.text}`;
22469
22576
  if (!hookHasReview) {
22470
22577
  await moveCard(workspaceId, taskId, "ready_for_review");
22471
22578
  await appendActivityLog(workspaceId, taskId, "Agent finished \u2192 moved to Ready for Review");
22579
+ void playNotificationSound("readyForReview");
22472
22580
  } else {
22473
22581
  await appendActivityLog(workspaceId, taskId, "Agent finished \u2192 AI review starting");
22474
22582
  }
@@ -22567,9 +22675,25 @@ ${devSystemPromptResult.text}`;
22567
22675
  this.isShuttingDown = true;
22568
22676
  }
22569
22677
  };
22678
+ async function shareIntoWorktree(src, dst) {
22679
+ const isDir = (await stat2(src)).isDirectory();
22680
+ if (process.platform !== "win32") {
22681
+ await symlink(src, dst, isDir ? "dir" : "file");
22682
+ return;
22683
+ }
22684
+ if (isDir) {
22685
+ await symlink(src, dst, "junction");
22686
+ return;
22687
+ }
22688
+ try {
22689
+ await link(src, dst);
22690
+ } catch {
22691
+ await cp(src, dst, { recursive: true, dereference: true });
22692
+ }
22693
+ }
22570
22694
  function getMcpServerPath() {
22571
- const thisFile = fileURLToPath3(import.meta.url);
22572
- const thisDir = dirname5(thisFile);
22695
+ const thisFile = fileURLToPath4(import.meta.url);
22696
+ const thisDir = dirname6(thisFile);
22573
22697
  const isDev = thisFile.endsWith(".ts");
22574
22698
  if (isDev) {
22575
22699
  const projectRoot = resolve2(thisDir, "../..");
@@ -26406,13 +26530,13 @@ init_runtime_config();
26406
26530
  import { spawnSync as spawnSync8 } from "node:child_process";
26407
26531
  import { existsSync as existsSync12, readdirSync as readdirSync2, statSync } from "node:fs";
26408
26532
  import { homedir as homedir4 } from "node:os";
26409
- import { dirname as dirname6, join as join18, resolve as resolve3 } from "node:path";
26533
+ import { dirname as dirname7, join as join19, resolve as resolve3 } from "node:path";
26410
26534
 
26411
26535
  // src/core/terminal-apps.ts
26412
26536
  import { spawnSync as spawnSync7 } from "node:child_process";
26413
26537
  import { existsSync as existsSync11 } from "node:fs";
26414
26538
  import { homedir as homedir3 } from "node:os";
26415
- import { join as join17 } from "node:path";
26539
+ import { join as join18 } from "node:path";
26416
26540
  var MACOS_TERMINALS = [
26417
26541
  { bundle: "Terminal", label: "Terminal" },
26418
26542
  { bundle: "iTerm", label: "iTerm" },
@@ -26444,12 +26568,13 @@ function appExists(bundle) {
26444
26568
  `/Applications/${bundle}.app`,
26445
26569
  `/System/Applications/${bundle}.app`,
26446
26570
  `/System/Applications/Utilities/${bundle}.app`,
26447
- join17(homedir3(), "Applications", `${bundle}.app`)
26571
+ join18(homedir3(), "Applications", `${bundle}.app`)
26448
26572
  ];
26449
26573
  return paths.some((p2) => existsSync11(p2));
26450
26574
  }
26451
26575
  function binaryExists(bin) {
26452
- const r = spawnSync7("which", [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26576
+ const finder = process.platform === "win32" ? "where" : "which";
26577
+ const r = spawnSync7(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26453
26578
  return r.status === 0 && r.stdout.trim().length > 0;
26454
26579
  }
26455
26580
  function listTerminalApps() {
@@ -26520,15 +26645,15 @@ var openTerminal = async (path2) => {
26520
26645
  };
26521
26646
  var listDir = async (path2, includeFiles, showHidden) => {
26522
26647
  let target = resolve3(path2 || homedir4());
26523
- while (target !== dirname6(target) && !(existsSync12(target) && statSync(target).isDirectory())) {
26524
- target = dirname6(target);
26648
+ while (target !== dirname7(target) && !(existsSync12(target) && statSync(target).isDirectory())) {
26649
+ target = dirname7(target);
26525
26650
  }
26526
- const parent = dirname6(target);
26651
+ const parent = dirname7(target);
26527
26652
  const visible = (name) => showHidden || !name.startsWith(".");
26528
26653
  try {
26529
26654
  const entries = readdirSync2(target, { withFileTypes: true });
26530
- const dirs = entries.filter((e) => e.isDirectory() && visible(e.name)).map((e) => ({ name: e.name, path: join18(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name));
26531
- const files = includeFiles ? entries.filter((e) => e.isFile() && visible(e.name)).map((e) => ({ name: e.name, path: join18(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name)) : [];
26655
+ const dirs = entries.filter((e) => e.isDirectory() && visible(e.name)).map((e) => ({ name: e.name, path: join19(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name));
26656
+ const files = includeFiles ? entries.filter((e) => e.isFile() && visible(e.name)).map((e) => ({ name: e.name, path: join19(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name)) : [];
26532
26657
  return { current: target, parent: parent !== target ? parent : null, dirs, files };
26533
26658
  } catch {
26534
26659
  return { current: target, parent: parent !== target ? parent : null, dirs: [], files: [] };
@@ -26890,8 +27015,8 @@ var checkProjectPath = async (repoPath) => {
26890
27015
  return { valid: false, isGitRepo: false, error: null, name: null, branch: null, remote: null, branches: [] };
26891
27016
  const { statSync: statSync2 } = await import("node:fs");
26892
27017
  try {
26893
- const stat2 = statSync2(repoPath);
26894
- if (!stat2.isDirectory())
27018
+ const stat3 = statSync2(repoPath);
27019
+ if (!stat3.isDirectory())
26895
27020
  return {
26896
27021
  valid: false,
26897
27022
  isGitRepo: false,
@@ -26947,8 +27072,8 @@ var checkProjectPath = async (repoPath) => {
26947
27072
  var addProject = async (repoPath, initialConfig) => {
26948
27073
  const { statSync: statSync2 } = await import("node:fs");
26949
27074
  try {
26950
- const stat2 = statSync2(repoPath);
26951
- if (!stat2.isDirectory()) throw new Error("Not a directory");
27075
+ const stat3 = statSync2(repoPath);
27076
+ if (!stat3.isDirectory()) throw new Error("Not a directory");
26952
27077
  } catch {
26953
27078
  throw BadRequestError(`Path does not exist: ${repoPath}`);
26954
27079
  }
@@ -26984,7 +27109,7 @@ var removeProject = async (workspaceId) => {
26984
27109
  const cards = boardCards;
26985
27110
  enqueueCleanup2(async () => {
26986
27111
  const { rm: rm3 } = await import("node:fs/promises");
26987
- const { join: join22 } = await import("node:path");
27112
+ const { join: join23 } = await import("node:path");
26988
27113
  for (const [cardId, card] of Object.entries(cards)) {
26989
27114
  if (resolveWorktreeOwnerId(cardId, cards) === cardId) {
26990
27115
  await removeWorktreeAsync(cardId, repoPath, card.branchName).catch((err) => {
@@ -26992,11 +27117,11 @@ var removeProject = async (workspaceId) => {
26992
27117
  });
26993
27118
  }
26994
27119
  }
26995
- await rm3(join22(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
27120
+ await rm3(join23(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
26996
27121
  logger.warn(`[cleanup:project:${workspaceId}] workspace dir failed: ${String(err)}`);
26997
27122
  });
26998
27123
  for (const cardId of Object.keys(cards)) {
26999
- await rm3(join22(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
27124
+ await rm3(join23(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
27000
27125
  });
27001
27126
  }
27002
27127
  logger.info(`[cleanup:project:${workspaceId}] done`);
@@ -27285,15 +27410,15 @@ import { z as z14 } from "zod";
27285
27410
  init_runtime_config();
27286
27411
 
27287
27412
  // src/tunnel/cloudflare-setup.ts
27288
- init_runtime_config();
27289
- init_logger();
27290
- import { execFile as execFile9, spawn as spawn6 } from "node:child_process";
27291
- import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27413
+ import { execFile as execFile9, spawn as spawn7 } from "node:child_process";
27414
+ import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27292
27415
  import { homedir as homedir5 } from "node:os";
27293
- import { join as join19 } from "node:path";
27416
+ import { join as join20 } from "node:path";
27294
27417
  import { promisify as promisify9 } from "node:util";
27418
+ init_runtime_config();
27419
+ init_logger();
27295
27420
  var execFileAsync7 = promisify9(execFile9);
27296
- var CLOUDFLARED_DIR = join19(homedir5(), ".cloudflared");
27421
+ var CLOUDFLARED_DIR = join20(homedir5(), ".cloudflared");
27297
27422
  async function checkCloudflaredInstalled() {
27298
27423
  try {
27299
27424
  const { stdout } = await execFileAsync7("cloudflared", ["--version"]);
@@ -27305,7 +27430,7 @@ async function checkCloudflaredInstalled() {
27305
27430
  }
27306
27431
  async function checkCloudflaredAuth() {
27307
27432
  try {
27308
- await access(join19(CLOUDFLARED_DIR, "cert.pem"));
27433
+ await access(join20(CLOUDFLARED_DIR, "cert.pem"));
27309
27434
  return true;
27310
27435
  } catch {
27311
27436
  return false;
@@ -27316,12 +27441,12 @@ async function openCloudflaredLogin(force = false) {
27316
27441
  if (alreadyLoggedIn && !force) return { alreadyLoggedIn: true };
27317
27442
  if (force) {
27318
27443
  try {
27319
- await unlink4(join19(CLOUDFLARED_DIR, "cert.pem"));
27444
+ await unlink4(join20(CLOUDFLARED_DIR, "cert.pem"));
27320
27445
  } catch {
27321
27446
  }
27322
27447
  }
27323
27448
  return new Promise((resolve5) => {
27324
- const proc = spawn6("cloudflared", ["tunnel", "login"], {
27449
+ const proc = spawn7("cloudflared", ["tunnel", "login"], {
27325
27450
  stdio: ["ignore", "pipe", "pipe"]
27326
27451
  });
27327
27452
  let buffer = "";
@@ -27334,7 +27459,8 @@ async function openCloudflaredLogin(force = false) {
27334
27459
  resolved = true;
27335
27460
  const loginUrl = match2[1].trim();
27336
27461
  logger.info(`[cloudflared-login] Auth URL: ${loginUrl}`);
27337
- spawn6("open", [loginUrl], { stdio: "ignore" });
27462
+ open_default(loginUrl).catch(() => {
27463
+ });
27338
27464
  proc.unref();
27339
27465
  resolve5({ alreadyLoggedIn: false, loginUrl });
27340
27466
  }
@@ -27384,7 +27510,7 @@ async function createTunnel(tunnelName) {
27384
27510
  }
27385
27511
  }
27386
27512
  async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27387
- const credentialsFile = join19(CLOUDFLARED_DIR, `${tunnelId}.json`);
27513
+ const credentialsFile = join20(CLOUDFLARED_DIR, `${tunnelId}.json`);
27388
27514
  const config = [
27389
27515
  `tunnel: ${tunnelId}`,
27390
27516
  `credentials-file: ${credentialsFile}`,
@@ -27394,13 +27520,13 @@ async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27394
27520
  ` service: http://127.0.0.1:${DEFAULT_PORT}`,
27395
27521
  ` - service: http_status:404`
27396
27522
  ].join("\n");
27397
- await mkdir3(CLOUDFLARED_DIR, { recursive: true });
27398
- await writeFile3(join19(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
27523
+ await mkdir4(CLOUDFLARED_DIR, { recursive: true });
27524
+ await writeFile3(join20(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
27399
27525
  logger.info(`[tunnel-setup] Wrote ~/.cloudflared/config.yml for tunnel ${tunnelName}`);
27400
27526
  }
27401
27527
  async function readTunnelConfig() {
27402
27528
  try {
27403
- const raw2 = await readFile3(join19(CLOUDFLARED_DIR, "config.yml"), "utf-8");
27529
+ const raw2 = await readFile3(join20(CLOUDFLARED_DIR, "config.yml"), "utf-8");
27404
27530
  const tunnelMatch = raw2.match(/^tunnel:\s*(.+)$/m);
27405
27531
  const hostnameMatch = raw2.match(/hostname:\s*(.+)$/m);
27406
27532
  return {
@@ -27449,9 +27575,9 @@ var resetTunnel = async () => {
27449
27575
  await updateGlobalConfig({ tunnelId: void 0, tunnelDomain: void 0, autoStartTunnel: false });
27450
27576
  const { unlink: unlink5 } = await import("node:fs/promises");
27451
27577
  const { homedir: homedir6 } = await import("node:os");
27452
- const { join: join22 } = await import("node:path");
27578
+ const { join: join23 } = await import("node:path");
27453
27579
  try {
27454
- await unlink5(join22(homedir6(), ".cloudflared", "config.yml"));
27580
+ await unlink5(join23(homedir6(), ".cloudflared", "config.yml"));
27455
27581
  } catch {
27456
27582
  }
27457
27583
  };
@@ -27483,8 +27609,8 @@ import { z as z15 } from "zod";
27483
27609
  // src/api/services/workflows-service.ts
27484
27610
  init_workspace_state();
27485
27611
  import { existsSync as existsSync13 } from "node:fs";
27486
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27487
- import { dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
27612
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27613
+ import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
27488
27614
  var resolvePromptPath = async (workspaceId, requestedPath) => {
27489
27615
  const workspaces = await listWorkspaces();
27490
27616
  const ws = workspaces.find((w2) => w2.workspaceId === workspaceId);
@@ -27521,7 +27647,7 @@ var deleteWorkflow = async (workspaceId, workflowId) => {
27521
27647
  };
27522
27648
  var writePromptFile = async (workspaceId, path2, content) => {
27523
27649
  const targetPath = await resolvePromptPath(workspaceId, path2);
27524
- await mkdir4(dirname7(targetPath), { recursive: true });
27650
+ await mkdir5(dirname8(targetPath), { recursive: true });
27525
27651
  await writeFile4(targetPath, content, "utf-8");
27526
27652
  return { path: path2 };
27527
27653
  };
@@ -27745,8 +27871,8 @@ var RuntimeStateHub = class {
27745
27871
  // src/core/update-check.ts
27746
27872
  init_paths();
27747
27873
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
27748
- import { join as join20 } from "node:path";
27749
- var CACHE_FILE = join20(WHIPPED_HOME_DIR, "update-check.json");
27874
+ import { join as join21 } from "node:path";
27875
+ var CACHE_FILE = join21(WHIPPED_HOME_DIR, "update-check.json");
27750
27876
  var REGISTRY_URL = "https://registry.npmjs.org/whipped/latest";
27751
27877
  var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
27752
27878
  function readCache() {
@@ -27797,7 +27923,7 @@ function scheduleUpdateChecks(currentVersion, onUpdate) {
27797
27923
  }
27798
27924
 
27799
27925
  // src/server/runtime-server.ts
27800
- var __dirname2 = fileURLToPath4(new URL(".", import.meta.url));
27926
+ var __dirname2 = fileURLToPath5(new URL(".", import.meta.url));
27801
27927
  async function cleanupStaleTasks(workspaceId, hub) {
27802
27928
  const board = await loadBoard(workspaceId);
27803
27929
  const now = Date.now();
@@ -27851,8 +27977,8 @@ async function createRuntimeServer(options) {
27851
27977
  const runTerminalListeners = /* @__PURE__ */ new Map();
27852
27978
  function startRun(workspaceId, cardId, command, cwd) {
27853
27979
  stopRun(workspaceId);
27854
- const shell = process.env.SHELL ?? "/bin/bash";
27855
- const pty = nodePty2.spawn(shell, ["-c", command], {
27980
+ const [shell, shellArgs] = getShellInvocation(command);
27981
+ const pty = nodePty2.spawn(shell, shellArgs, {
27856
27982
  name: "xterm-256color",
27857
27983
  cols: 120,
27858
27984
  rows: 40,
@@ -27894,6 +28020,7 @@ async function createRuntimeServer(options) {
27894
28020
  session.status = "error";
27895
28021
  session.errorMessage = `Process exited with code ${exitCode}`;
27896
28022
  stateHub.broadcastRunSessionChange(workspaceId, cardId, "error", session.errorMessage);
28023
+ void playNotificationSound("runError");
27897
28024
  }
27898
28025
  });
27899
28026
  }
@@ -28032,8 +28159,8 @@ async function createRuntimeServer(options) {
28032
28159
  };
28033
28160
  }
28034
28161
  const apiApp = createApiApp(createContext());
28035
- const webUiDistPath = join21(__dirname2, "web-ui");
28036
- const webUiIndexPath = join21(webUiDistPath, "index.html");
28162
+ const webUiDistPath = join22(__dirname2, "web-ui");
28163
+ const webUiIndexPath = join22(webUiDistPath, "index.html");
28037
28164
  const hasWebUi = existsSync14(webUiIndexPath);
28038
28165
  const httpServer = createServer(async (req, res) => {
28039
28166
  const url = new URL(req.url ?? "/", `http://${host}`);
@@ -28267,7 +28394,7 @@ async function createRuntimeServer(options) {
28267
28394
  res.end("Bad request");
28268
28395
  return;
28269
28396
  }
28270
- const filePath = join21(ATTACHMENTS_DIR, cardId, filename);
28397
+ const filePath = join22(ATTACHMENTS_DIR, cardId, filename);
28271
28398
  const { readFile: readFile5 } = await import("node:fs/promises");
28272
28399
  try {
28273
28400
  const data = await readFile5(filePath);
@@ -28342,7 +28469,7 @@ async function createRuntimeServer(options) {
28342
28469
  return;
28343
28470
  }
28344
28471
  if (hasWebUi) {
28345
- const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join21(webUiDistPath, url.pathname);
28472
+ const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join22(webUiDistPath, url.pathname);
28346
28473
  if (existsSync14(filePath)) {
28347
28474
  const content = readFileSync8(filePath);
28348
28475
  res.writeHead(200, { "Content-Type": getContentType(filePath) });
@@ -28561,7 +28688,7 @@ process.on("uncaughtException", (err) => {
28561
28688
  if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
28562
28689
  throw err;
28563
28690
  });
28564
- var VERSION9 = true ? "0.5.1" : "0.0.0-dev";
28691
+ var VERSION9 = true ? "0.7.0" : "0.0.0-dev";
28565
28692
  async function isPortAvailable(port, host) {
28566
28693
  return new Promise((resolve5) => {
28567
28694
  const probe = createServer2();