@schoolai/shipyard 1.0.0 → 1.1.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/index.js CHANGED
@@ -6,27 +6,31 @@ import {
6
6
  BRANCH_NAME_PATTERN,
7
7
  CollabCreateRequestSchema,
8
8
  CollabCreateResponseSchema,
9
+ CollabRoomConnection,
9
10
  ErrorResponseSchema,
10
11
  HealthResponseSchema,
11
12
  PermissionModeSchema,
12
13
  PersonalRoomConnection,
13
14
  ROUTES,
14
15
  ValidationErrorResponseSchema
15
- } from "./chunk-FNYWUFGS.js";
16
+ } from "./chunk-4IVLSSKL.js";
16
17
  import {
17
18
  createChildLogger,
18
19
  logger
19
- } from "./chunk-ERDY7OVK.js";
20
+ } from "./chunk-FY3DRRGT.js";
20
21
  import {
21
22
  __export,
23
+ external_exports,
22
24
  getShipyardHome,
23
25
  validateEnv
24
26
  } from "./chunk-HS57GMAL.js";
25
27
 
26
28
  // src/index.ts
29
+ import { readFileSync as readFileSync2 } from "fs";
27
30
  import { mkdir as mkdir4 } from "fs/promises";
28
31
  import { homedir as homedir3 } from "os";
29
32
  import { resolve as resolve3 } from "path";
33
+ import { fileURLToPath } from "url";
30
34
  import { parseArgs } from "util";
31
35
 
32
36
  // ../../node_modules/.pnpm/@loro-extended+change@6.0.0-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/change/dist/index.js
@@ -11759,8 +11763,15 @@ function nanoid(size2 = 21) {
11759
11763
  }
11760
11764
 
11761
11765
  // ../../packages/loro-schema/dist/index.js
11762
- var DEFAULT_EPOCH = 4;
11763
- var DOCUMENT_PREFIXES = ["task-meta", "task-conv", "task-review", "room", "epoch"];
11766
+ var DEFAULT_EPOCH = 5;
11767
+ var DOCUMENT_PREFIXES = [
11768
+ "task-meta",
11769
+ "task-conv",
11770
+ "task-review",
11771
+ "room",
11772
+ "epoch",
11773
+ "user-prefs"
11774
+ ];
11764
11775
  function buildDocumentId(prefix, key, epoch) {
11765
11776
  if (!prefix || !key) {
11766
11777
  throw new Error(`Document ID prefix and key must be non-empty: prefix="${prefix}", key="${key}"`);
@@ -11782,6 +11793,9 @@ function buildTaskConvDocId(taskId, epoch) {
11782
11793
  function buildTaskReviewDocId(taskId, epoch) {
11783
11794
  return buildDocumentId("task-review", taskId, epoch);
11784
11795
  }
11796
+ function buildRoomDocId(userId, epoch) {
11797
+ return buildDocumentId("room", userId, epoch);
11798
+ }
11785
11799
  var DOCUMENT_PREFIX_SET = new Set(DOCUMENT_PREFIXES);
11786
11800
  function isDocumentPrefix(value) {
11787
11801
  return DOCUMENT_PREFIX_SET.has(value);
@@ -11802,7 +11816,6 @@ function parseDocumentId(id) {
11802
11816
  return null;
11803
11817
  return { prefix, key, epoch };
11804
11818
  }
11805
- var LOCAL_USER_ID = "local-user";
11806
11819
  function generateTaskId() {
11807
11820
  return nanoid();
11808
11821
  }
@@ -11821,6 +11834,8 @@ function prefixMutability(prefix, role) {
11821
11834
  return false;
11822
11835
  case "epoch":
11823
11836
  return false;
11837
+ case "user-prefs":
11838
+ return false;
11824
11839
  default:
11825
11840
  return assertNever(prefix);
11826
11841
  }
@@ -11956,6 +11971,66 @@ var DiffFileShape = Shape.plain.struct({
11956
11971
  path: Shape.plain.string(),
11957
11972
  status: Shape.plain.string()
11958
11973
  });
11974
+ var PR_STATES = ["open", "closed", "merged", "draft"];
11975
+ var PR_REVIEW_STATES = [
11976
+ "pending",
11977
+ "approved",
11978
+ "changes_requested",
11979
+ "commented",
11980
+ "dismissed"
11981
+ ];
11982
+ var CI_CHECK_STATUSES = [
11983
+ "success",
11984
+ "failure",
11985
+ "pending",
11986
+ "neutral",
11987
+ "skipped",
11988
+ "cancelled"
11989
+ ];
11990
+ var CICheckShape = Shape.plain.struct({
11991
+ name: Shape.plain.string(),
11992
+ status: Shape.plain.string(...CI_CHECK_STATUSES),
11993
+ conclusion: Shape.plain.string().nullable(),
11994
+ url: Shape.plain.string().nullable(),
11995
+ startedAt: Shape.plain.number().nullable(),
11996
+ completedAt: Shape.plain.number().nullable()
11997
+ });
11998
+ var PRReviewerShape = Shape.plain.struct({
11999
+ login: Shape.plain.string(),
12000
+ state: Shape.plain.string(...PR_REVIEW_STATES),
12001
+ avatarUrl: Shape.plain.string().nullable(),
12002
+ submittedAt: Shape.plain.number().nullable()
12003
+ });
12004
+ var PRCommentShape = Shape.plain.struct({
12005
+ id: Shape.plain.string(),
12006
+ author: Shape.plain.string(),
12007
+ body: Shape.plain.string(),
12008
+ createdAt: Shape.plain.number(),
12009
+ updatedAt: Shape.plain.number().nullable(),
12010
+ path: Shape.plain.string().nullable(),
12011
+ line: Shape.plain.number().nullable(),
12012
+ side: Shape.plain.string().nullable(),
12013
+ isReviewComment: Shape.plain.boolean()
12014
+ });
12015
+ var PRDataShape = Shape.plain.struct({
12016
+ number: Shape.plain.number(),
12017
+ title: Shape.plain.string(),
12018
+ state: Shape.plain.string(...PR_STATES),
12019
+ author: Shape.plain.string(),
12020
+ url: Shape.plain.string(),
12021
+ baseRef: Shape.plain.string(),
12022
+ headRef: Shape.plain.string(),
12023
+ body: Shape.plain.string(),
12024
+ isDraft: Shape.plain.boolean(),
12025
+ additions: Shape.plain.number(),
12026
+ deletions: Shape.plain.number(),
12027
+ changedFiles: Shape.plain.number(),
12028
+ createdAt: Shape.plain.number(),
12029
+ updatedAt: Shape.plain.number(),
12030
+ checks: Shape.plain.array(CICheckShape),
12031
+ reviewers: Shape.plain.array(PRReviewerShape),
12032
+ comments: Shape.plain.array(PRCommentShape)
12033
+ });
11959
12034
  var DiffStateShape = Shape.struct({
11960
12035
  unstaged: Shape.plain.string(),
11961
12036
  staged: Shape.plain.string(),
@@ -11967,7 +12042,13 @@ var DiffStateShape = Shape.struct({
11967
12042
  branchUpdatedAt: Shape.plain.number(),
11968
12043
  lastTurnDiff: Shape.plain.string(),
11969
12044
  lastTurnFiles: Shape.list(DiffFileShape),
11970
- lastTurnUpdatedAt: Shape.plain.number()
12045
+ lastTurnUpdatedAt: Shape.plain.number(),
12046
+ currentBranchPRNumber: Shape.plain.number(),
12047
+ prData: Shape.plain.array(PRDataShape),
12048
+ prDiff: Shape.plain.string(),
12049
+ prFiles: Shape.list(DiffFileShape),
12050
+ prUpdatedAt: Shape.plain.number(),
12051
+ prAvailable: Shape.plain.boolean()
11971
12052
  });
11972
12053
  var SessionEntryShape = Shape.plain.struct({
11973
12054
  sessionId: Shape.plain.string(),
@@ -12004,7 +12085,7 @@ var PlanVersionShape = Shape.plain.struct({
12004
12085
  ...ATTRIBUTION_FIELDS
12005
12086
  });
12006
12087
  var DIFF_COMMENT_SIDES = ["old", "new"];
12007
- var DIFF_COMMENT_SCOPES = ["working-tree", "last-turn"];
12088
+ var DIFF_COMMENT_SCOPES = ["working-tree", "last-turn", "pr"];
12008
12089
  var DiffCommentShape = Shape.plain.struct({
12009
12090
  commentId: Shape.plain.string(),
12010
12091
  filePath: Shape.plain.string(),
@@ -12072,6 +12153,16 @@ var TaskReviewDocumentSchema = Shape.doc({
12072
12153
  deliveredCommentIds: Shape.list(Shape.plain.string()),
12073
12154
  todoItems: Shape.list(TodoItemShape)
12074
12155
  }, { mergeable: true });
12156
+ var PinnedEntryShape = Shape.plain.struct({
12157
+ pinnedAt: Shape.plain.number()
12158
+ });
12159
+ var AcknowledgedEntryShape = Shape.plain.struct({
12160
+ at: Shape.plain.number()
12161
+ });
12162
+ var UserPreferencesDocumentSchema = Shape.doc({
12163
+ pinnedTaskIds: Shape.record(PinnedEntryShape),
12164
+ acknowledgedTasks: Shape.record(AcknowledgedEntryShape)
12165
+ }, { mergeable: true });
12075
12166
  var TERMINAL_TASK_STATES = ["completed", "failed", "canceled"];
12076
12167
  var TOOL_RISK_LEVELS = ["low", "medium", "high"];
12077
12168
  var PERMISSION_DECISIONS = ["approved", "denied"];
@@ -12117,6 +12208,7 @@ var TaskIndexEntryShape = Shape.struct({
12117
12208
  todoTotal: Shape.plain.number(),
12118
12209
  currentActivity: Shape.plain.string().nullable()
12119
12210
  });
12211
+ var KEEP_AWAKE_GRACE_PERIOD_MS = 15 * 60 * 1e3;
12120
12212
  var WORKTREE_SETUP_STATUSES = ["running", "done", "failed"];
12121
12213
  var WorktreeSetupStatusShape = Shape.plain.struct({
12122
12214
  status: Shape.plain.string(...WORKTREE_SETUP_STATUSES),
@@ -12212,6 +12304,18 @@ var AnthropicLoginResponseEphemeral = Shape.plain.struct({
12212
12304
  loginUrl: Shape.plain.string().nullable(),
12213
12305
  error: Shape.plain.string().nullable()
12214
12306
  });
12307
+ var TERMINAL_SESSION_STATUSES = ["running", "exited"];
12308
+ var TERMINAL_CONTROL_PREFIX = "\0\0";
12309
+ var FILE_IO_CONTROL_PREFIX = "\0\0";
12310
+ var TerminalSessionEphemeral = Shape.plain.struct({
12311
+ terminalId: Shape.plain.string(),
12312
+ taskId: Shape.plain.string(),
12313
+ machineId: Shape.plain.string(),
12314
+ cwd: Shape.plain.string(),
12315
+ status: Shape.plain.string(...TERMINAL_SESSION_STATUSES),
12316
+ exitCode: Shape.plain.number().nullable(),
12317
+ createdAt: Shape.plain.number()
12318
+ });
12215
12319
  var ROOM_EPHEMERAL_DECLARATIONS = {
12216
12320
  capabilities: MachineCapabilitiesEphemeral,
12217
12321
  enhancePromptReqs: EnhancePromptRequestEphemeral,
@@ -12220,7 +12324,8 @@ var ROOM_EPHEMERAL_DECLARATIONS = {
12220
12324
  worktreeCreateResps: WorktreeCreateResponseEphemeral,
12221
12325
  worktreeSetupResps: WorktreeSetupResultEphemeral,
12222
12326
  anthropicLoginReqs: AnthropicLoginRequestEphemeral,
12223
- anthropicLoginResps: AnthropicLoginResponseEphemeral
12327
+ anthropicLoginResps: AnthropicLoginResponseEphemeral,
12328
+ terminalSessions: TerminalSessionEphemeral
12224
12329
  };
12225
12330
  var TASK_CONV_EPHEMERAL_DECLARATIONS = {
12226
12331
  permReqs: PermissionRequestEphemeral,
@@ -12297,8 +12402,8 @@ var FileStorageAdapter = class extends StorageAdapter {
12297
12402
  return join(this.#dataDir, ...sanitized);
12298
12403
  }
12299
12404
  #pathToKey(filePath) {
12300
- const relative = filePath.slice(this.#dataDir.length + 1);
12301
- return relative.split(sep).map((part) => decodeURIComponent(part));
12405
+ const relative2 = filePath.slice(this.#dataDir.length + 1);
12406
+ return relative2.split(sep).map((part) => decodeURIComponent(part));
12302
12407
  }
12303
12408
  #isPrefix(prefix, key) {
12304
12409
  if (prefix.length > key.length) return false;
@@ -12358,24 +12463,11 @@ var LifecycleManager = class {
12358
12463
  { signal: "SIGINT", handler: intHandler }
12359
12464
  ];
12360
12465
  const exceptionHandler = (error2) => {
12361
- try {
12362
- logger.error({ error: error2 }, "Uncaught exception \u2014 initiating shutdown");
12363
- } catch {
12364
- }
12466
+ logger.error({ err: error2 }, "Uncaught exception \u2014 initiating shutdown");
12365
12467
  void this.#shutdown("uncaughtException");
12366
12468
  };
12367
12469
  const rejectionHandler = (reason) => {
12368
- try {
12369
- let jsonStr;
12370
- try {
12371
- jsonStr = JSON.stringify(reason);
12372
- } catch {
12373
- jsonStr = "(not serializable)";
12374
- }
12375
- const detail = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { inspected: `${String(reason)} ${jsonStr}` };
12376
- logger.error(detail, "Unhandled rejection (non-fatal)");
12377
- } catch {
12378
- }
12470
+ logger.error({ err: reason }, "Unhandled rejection (non-fatal)");
12379
12471
  };
12380
12472
  process.on("uncaughtException", exceptionHandler);
12381
12473
  process.on("unhandledRejection", rejectionHandler);
@@ -12453,7 +12545,7 @@ var LifecycleManager = class {
12453
12545
  async #shutdown(signal) {
12454
12546
  if (this.#isShuttingDown) return;
12455
12547
  this.#isShuttingDown = true;
12456
- logger.info({ signal }, "Shutdown signal received");
12548
+ logger.info({ signal, pid: process.pid, ppid: process.ppid }, "Shutdown signal received");
12457
12549
  const HARD_KILL_MS = 15e3;
12458
12550
  const forceExit = setTimeout(() => {
12459
12551
  logger.error("Graceful shutdown timed out, forcing exit");
@@ -12478,7 +12570,7 @@ var LifecycleManager = class {
12478
12570
  })
12479
12571
  ]).finally(() => clearTimeout(timeoutId));
12480
12572
  } catch (error2) {
12481
- logger.error({ error: error2 }, "Error during shutdown callback");
12573
+ logger.error({ err: error2 }, "Error during shutdown callback");
12482
12574
  }
12483
12575
  }
12484
12576
  await this.#removePidFile();
@@ -12491,9 +12583,10 @@ var LifecycleManager = class {
12491
12583
 
12492
12584
  // src/serve.ts
12493
12585
  import { spawn as spawn4 } from "child_process";
12494
- import { mkdir as mkdir3 } from "fs/promises";
12586
+ import { existsSync, statSync } from "fs";
12587
+ import { mkdir as mkdir3, readFile as readFile5, stat as stat2 } from "fs/promises";
12495
12588
  import { homedir as homedir2, hostname as hostname2 } from "os";
12496
- import { resolve as resolve2 } from "path";
12589
+ import { isAbsolute as isAbsolute3, relative, resolve as resolve2 } from "path";
12497
12590
 
12498
12591
  // ../../node_modules/.pnpm/@levischuck+tiny-cbor@0.3.2/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor_internal.js
12499
12592
  function decodeLength(data, argument, index) {
@@ -14328,7 +14421,7 @@ function runWithTimeout(command2, args, cwd, timeoutMs) {
14328
14421
  execFile(
14329
14422
  command2,
14330
14423
  args,
14331
- { timeout: timeoutMs, cwd, maxBuffer: 2 * 1024 * 1024 },
14424
+ { timeout: timeoutMs, cwd, maxBuffer: 10 * 1024 * 1024 },
14332
14425
  (error2, stdout) => {
14333
14426
  if (error2) {
14334
14427
  reject(error2);
@@ -14341,6 +14434,19 @@ function runWithTimeout(command2, args, cwd, timeoutMs) {
14341
14434
  }
14342
14435
  var DIFF_TIMEOUT_MS = 15e3;
14343
14436
  var MAX_DIFF_SIZE = 2e5;
14437
+ var gitRepoCache = /* @__PURE__ */ new Map();
14438
+ async function isGitRepo(cwd) {
14439
+ const cached = gitRepoCache.get(cwd);
14440
+ if (cached !== void 0) return cached;
14441
+ try {
14442
+ await runWithTimeout("git", ["rev-parse", "--git-dir"], cwd, TIMEOUT_MS);
14443
+ gitRepoCache.set(cwd, true);
14444
+ return true;
14445
+ } catch {
14446
+ gitRepoCache.set(cwd, false);
14447
+ return false;
14448
+ }
14449
+ }
14344
14450
  async function withIntentToAdd(cwd, fn) {
14345
14451
  let added = false;
14346
14452
  try {
@@ -14362,29 +14468,46 @@ async function withIntentToAdd(cwd, fn) {
14362
14468
  }
14363
14469
  }
14364
14470
  }
14365
- async function getUnstagedDiff(cwd) {
14366
- const result = await withIntentToAdd(
14367
- cwd,
14368
- () => runWithTimeout("git", ["diff", "--no-color"], cwd, DIFF_TIMEOUT_MS)
14369
- );
14471
+ function isMaxBufferError(err) {
14472
+ return err instanceof Error && err.message.includes("maxBuffer");
14473
+ }
14474
+ var BUFFER_OVERFLOW_MSG = "... diff too large (exceeded buffer limit) ...\n";
14475
+ function truncateDiff(result) {
14370
14476
  return result.length > MAX_DIFF_SIZE ? `${result.slice(0, MAX_DIFF_SIZE)}
14371
14477
 
14372
- ... diff truncated (exceeds 1MB) ...
14478
+ ... diff truncated (exceeds 200KB) ...
14373
14479
  ` : result;
14374
14480
  }
14481
+ async function getUnstagedDiff(cwd) {
14482
+ if (!await isGitRepo(cwd)) return "";
14483
+ try {
14484
+ const result = await withIntentToAdd(
14485
+ cwd,
14486
+ () => runWithTimeout("git", ["diff", "--no-color"], cwd, DIFF_TIMEOUT_MS)
14487
+ );
14488
+ return truncateDiff(result);
14489
+ } catch (err) {
14490
+ if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
14491
+ throw err;
14492
+ }
14493
+ }
14375
14494
  async function getStagedDiff(cwd) {
14376
- const result = await runWithTimeout(
14377
- "git",
14378
- ["diff", "--cached", "--no-color"],
14379
- cwd,
14380
- DIFF_TIMEOUT_MS
14381
- );
14382
- return result.length > MAX_DIFF_SIZE ? `${result.slice(0, MAX_DIFF_SIZE)}
14383
-
14384
- ... diff truncated (exceeds 1MB) ...
14385
- ` : result;
14495
+ if (!await isGitRepo(cwd)) return "";
14496
+ try {
14497
+ const result = await runWithTimeout(
14498
+ "git",
14499
+ ["diff", "--cached", "--no-color"],
14500
+ cwd,
14501
+ DIFF_TIMEOUT_MS
14502
+ );
14503
+ return truncateDiff(result);
14504
+ } catch (err) {
14505
+ if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
14506
+ throw err;
14507
+ }
14386
14508
  }
14387
14509
  async function getChangedFiles(cwd) {
14510
+ if (!await isGitRepo(cwd)) return [];
14388
14511
  const out = await runWithTimeout("git", ["status", "--porcelain"], cwd, DIFF_TIMEOUT_MS);
14389
14512
  if (!out) return [];
14390
14513
  return out.split("\n").filter(Boolean).map((line) => ({
@@ -14430,11 +14553,9 @@ async function getBranchDiff(cwd, baseBranch) {
14430
14553
  cwd,
14431
14554
  BRANCH_DIFF_TIMEOUT_MS
14432
14555
  );
14433
- return result.length > MAX_DIFF_SIZE ? `${result.slice(0, MAX_DIFF_SIZE)}
14434
-
14435
- ... diff truncated (exceeds 1MB) ...
14436
- ` : result;
14437
- } catch {
14556
+ return truncateDiff(result);
14557
+ } catch (err) {
14558
+ if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
14438
14559
  return "";
14439
14560
  }
14440
14561
  }
@@ -14477,11 +14598,9 @@ async function getSnapshotDiff(cwd, fromRef, toRef) {
14477
14598
  cwd,
14478
14599
  DIFF_TIMEOUT_MS
14479
14600
  );
14480
- return result.length > MAX_DIFF_SIZE ? `${result.slice(0, MAX_DIFF_SIZE)}
14481
-
14482
- ... diff truncated (exceeds 1MB) ...
14483
- ` : result;
14484
- } catch {
14601
+ return truncateDiff(result);
14602
+ } catch (err) {
14603
+ if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
14485
14604
  return "";
14486
14605
  }
14487
14606
  }
@@ -14625,6 +14744,265 @@ async function getRepoMetadata(repoPath) {
14625
14744
  return null;
14626
14745
  }
14627
14746
  }
14747
+ var GH_PR_JSON_FIELDS = "number,title,state,author,url,baseRefName,headRefName,body,isDraft,additions,deletions,changedFiles,createdAt,updatedAt,statusCheckRollup,reviews,reviewRequests,comments";
14748
+ var GH_STATE_MAP = {
14749
+ OPEN: "open",
14750
+ CLOSED: "closed",
14751
+ MERGED: "merged"
14752
+ };
14753
+ var GhPRResponseSchema = external_exports.object({
14754
+ number: external_exports.number().optional(),
14755
+ title: external_exports.string().optional(),
14756
+ state: external_exports.string().optional(),
14757
+ author: external_exports.object({ login: external_exports.string() }).passthrough().optional(),
14758
+ url: external_exports.string().optional(),
14759
+ baseRefName: external_exports.string().optional(),
14760
+ headRefName: external_exports.string().optional(),
14761
+ body: external_exports.string().optional(),
14762
+ isDraft: external_exports.boolean().optional(),
14763
+ additions: external_exports.number().optional(),
14764
+ deletions: external_exports.number().optional(),
14765
+ changedFiles: external_exports.number().optional(),
14766
+ createdAt: external_exports.string().optional(),
14767
+ updatedAt: external_exports.string().optional(),
14768
+ statusCheckRollup: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).optional(),
14769
+ reviews: external_exports.union([
14770
+ external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())),
14771
+ external_exports.object({ nodes: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }).passthrough()
14772
+ ]).optional(),
14773
+ comments: external_exports.union([
14774
+ external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())),
14775
+ external_exports.object({ nodes: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }).passthrough()
14776
+ ]).optional()
14777
+ }).passthrough();
14778
+ var GhPRListResponseSchema = external_exports.array(GhPRResponseSchema);
14779
+ function parseTimestamp(value) {
14780
+ if (typeof value === "string") {
14781
+ const parsed = new Date(value).getTime();
14782
+ if (!Number.isNaN(parsed)) return parsed;
14783
+ }
14784
+ if (typeof value === "number") return value;
14785
+ return 0;
14786
+ }
14787
+ function field(obj, key) {
14788
+ if (typeof obj === "object" && obj !== null && key in obj) {
14789
+ return obj[key];
14790
+ }
14791
+ return void 0;
14792
+ }
14793
+ function toRecord(value) {
14794
+ if (typeof value === "object" && value !== null) return value;
14795
+ return {};
14796
+ }
14797
+ function extractLogin(obj) {
14798
+ const login = field(obj, "login");
14799
+ return typeof login === "string" ? login : "";
14800
+ }
14801
+ function extractStringField(obj, f) {
14802
+ const val = field(obj, f);
14803
+ return typeof val === "string" ? val : null;
14804
+ }
14805
+ function extractArrayOrNodes(value) {
14806
+ if (Array.isArray(value)) return value;
14807
+ const nodes = field(value, "nodes");
14808
+ if (Array.isArray(nodes)) return nodes;
14809
+ return [];
14810
+ }
14811
+ function mapCheckStatus(check) {
14812
+ const state = typeof check.state === "string" ? check.state.toLowerCase() : "";
14813
+ const conclusion = typeof check.conclusion === "string" ? check.conclusion.toLowerCase() : "";
14814
+ if (conclusion === "success") return "success";
14815
+ if (conclusion === "failure" || conclusion === "timed_out" || conclusion === "action_required")
14816
+ return "failure";
14817
+ if (conclusion === "neutral") return "neutral";
14818
+ if (conclusion === "skipped") return "skipped";
14819
+ if (conclusion === "cancelled") return "cancelled";
14820
+ if (state === "success") return "success";
14821
+ if (state === "failure" || state === "error") return "failure";
14822
+ if (state === "pending" || state === "queued" || state === "in_progress") return "pending";
14823
+ return "pending";
14824
+ }
14825
+ function mapReviewState(value) {
14826
+ if (typeof value !== "string") return "pending";
14827
+ const upper = value.toUpperCase();
14828
+ if (upper === "APPROVED") return "approved";
14829
+ if (upper === "CHANGES_REQUESTED") return "changes_requested";
14830
+ if (upper === "COMMENTED") return "commented";
14831
+ if (upper === "DISMISSED") return "dismissed";
14832
+ return "pending";
14833
+ }
14834
+ function mapGhCheck(c) {
14835
+ const name = typeof c.name === "string" ? c.name : typeof c.context === "string" ? c.context : "";
14836
+ const url = typeof c.targetUrl === "string" ? c.targetUrl : typeof c.detailsUrl === "string" ? c.detailsUrl : null;
14837
+ return {
14838
+ name,
14839
+ status: mapCheckStatus(c),
14840
+ conclusion: typeof c.conclusion === "string" ? c.conclusion.toLowerCase() : null,
14841
+ url,
14842
+ startedAt: parseTimestamp(c.startedAt),
14843
+ completedAt: parseTimestamp(c.completedAt)
14844
+ };
14845
+ }
14846
+ function mapGhReviewer(r) {
14847
+ return {
14848
+ login: extractLogin(r.author),
14849
+ state: mapReviewState(r.state),
14850
+ avatarUrl: extractStringField(r.author, "avatarUrl"),
14851
+ submittedAt: parseTimestamp(r.submittedAt)
14852
+ };
14853
+ }
14854
+ function mapGhComment(c) {
14855
+ return {
14856
+ id: typeof c.id === "string" ? c.id : String(c.id ?? ""),
14857
+ author: extractLogin(c.author),
14858
+ body: typeof c.body === "string" ? c.body : "",
14859
+ createdAt: parseTimestamp(c.createdAt),
14860
+ updatedAt: typeof c.updatedAt === "string" ? parseTimestamp(c.updatedAt) : null,
14861
+ path: typeof c.path === "string" ? c.path : null,
14862
+ line: typeof c.line === "number" ? c.line : null,
14863
+ side: typeof c.side === "string" ? c.side : null,
14864
+ isReviewComment: c.pullRequestReview !== void 0 && c.pullRequestReview !== null
14865
+ };
14866
+ }
14867
+ function mapGhPRToPRData(raw) {
14868
+ const isDraft2 = raw.isDraft === true;
14869
+ const ghState = typeof raw.state === "string" ? raw.state.toUpperCase() : "";
14870
+ const state = isDraft2 ? "draft" : GH_STATE_MAP[ghState] ?? "open";
14871
+ const rawChecks = Array.isArray(raw.statusCheckRollup) ? raw.statusCheckRollup : [];
14872
+ const checks = rawChecks.map(mapGhCheck);
14873
+ const reviewers = extractArrayOrNodes(raw.reviews).map(
14874
+ (r) => mapGhReviewer(toRecord(r))
14875
+ );
14876
+ const comments = extractArrayOrNodes(raw.comments).map(
14877
+ (c) => mapGhComment(toRecord(c))
14878
+ );
14879
+ return {
14880
+ number: typeof raw.number === "number" ? raw.number : 0,
14881
+ title: typeof raw.title === "string" ? raw.title : "",
14882
+ state,
14883
+ author: extractLogin(raw.author),
14884
+ url: typeof raw.url === "string" ? raw.url : "",
14885
+ baseRef: typeof raw.baseRefName === "string" ? raw.baseRefName : "",
14886
+ headRef: typeof raw.headRefName === "string" ? raw.headRefName : "",
14887
+ body: typeof raw.body === "string" ? raw.body : "",
14888
+ isDraft: isDraft2,
14889
+ additions: typeof raw.additions === "number" ? raw.additions : 0,
14890
+ deletions: typeof raw.deletions === "number" ? raw.deletions : 0,
14891
+ changedFiles: typeof raw.changedFiles === "number" ? raw.changedFiles : 0,
14892
+ createdAt: parseTimestamp(raw.createdAt),
14893
+ updatedAt: parseTimestamp(raw.updatedAt),
14894
+ checks,
14895
+ reviewers,
14896
+ comments
14897
+ };
14898
+ }
14899
+ async function isGhAvailable() {
14900
+ try {
14901
+ await run("which", ["gh"]);
14902
+ return true;
14903
+ } catch {
14904
+ return false;
14905
+ }
14906
+ }
14907
+ async function getPRForCurrentBranch(cwd) {
14908
+ try {
14909
+ const stdout = await runWithTimeout(
14910
+ "gh",
14911
+ ["pr", "view", "--json", GH_PR_JSON_FIELDS],
14912
+ cwd,
14913
+ 15e3
14914
+ );
14915
+ const parseResult = GhPRResponseSchema.safeParse(JSON.parse(stdout));
14916
+ if (!parseResult.success) {
14917
+ return null;
14918
+ }
14919
+ return mapGhPRToPRData(parseResult.data);
14920
+ } catch {
14921
+ return null;
14922
+ }
14923
+ }
14924
+ async function getPRDiff(cwd, prNumber) {
14925
+ try {
14926
+ const result = await runWithTimeout(
14927
+ "gh",
14928
+ ["pr", "diff", String(prNumber), "--color=never"],
14929
+ cwd,
14930
+ 15e3
14931
+ );
14932
+ return truncateDiff(result);
14933
+ } catch (err) {
14934
+ if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
14935
+ return "";
14936
+ }
14937
+ }
14938
+ async function getPRFiles(cwd, prNumber) {
14939
+ try {
14940
+ const stdout = await runWithTimeout(
14941
+ "gh",
14942
+ ["pr", "view", String(prNumber), "--json", "files"],
14943
+ cwd,
14944
+ 15e3
14945
+ );
14946
+ const parsed = JSON.parse(stdout);
14947
+ if (!Array.isArray(parsed.files)) return [];
14948
+ return parsed.files.map((f) => ({
14949
+ path: typeof f.path === "string" ? f.path : "",
14950
+ status: f.additions !== void 0 || f.deletions !== void 0 ? "M" : "U"
14951
+ }));
14952
+ } catch {
14953
+ return [];
14954
+ }
14955
+ }
14956
+ async function getAssignedReviews(cwd) {
14957
+ try {
14958
+ const stdout = await runWithTimeout(
14959
+ "gh",
14960
+ [
14961
+ "pr",
14962
+ "list",
14963
+ "--search",
14964
+ "is:open review-requested:@me",
14965
+ "--json",
14966
+ GH_PR_JSON_FIELDS,
14967
+ "--limit",
14968
+ "10"
14969
+ ],
14970
+ cwd,
14971
+ 15e3
14972
+ );
14973
+ const parseResult = GhPRListResponseSchema.safeParse(JSON.parse(stdout));
14974
+ if (!parseResult.success) return [];
14975
+ return parseResult.data.map(mapGhPRToPRData);
14976
+ } catch {
14977
+ return [];
14978
+ }
14979
+ }
14980
+ async function getUserPRs(cwd) {
14981
+ try {
14982
+ const stdout = await runWithTimeout(
14983
+ "gh",
14984
+ [
14985
+ "pr",
14986
+ "list",
14987
+ "--author",
14988
+ "@me",
14989
+ "--state",
14990
+ "open",
14991
+ "--json",
14992
+ GH_PR_JSON_FIELDS,
14993
+ "--limit",
14994
+ "10"
14995
+ ],
14996
+ cwd,
14997
+ 15e3
14998
+ );
14999
+ const parseResult = GhPRListResponseSchema.safeParse(JSON.parse(stdout));
15000
+ if (!parseResult.success) return [];
15001
+ return parseResult.data.map(mapGhPRToPRData);
15002
+ } catch {
15003
+ return [];
15004
+ }
15005
+ }
14628
15006
  async function detectEnvironments() {
14629
15007
  const repoPaths = await findGitRepos(homedir());
14630
15008
  const repoInfos = await Promise.all(repoPaths.map(getRepoMetadata));
@@ -14783,6 +15161,664 @@ function createBranchWatcher(options) {
14783
15161
  };
14784
15162
  }
14785
15163
 
15164
+ // src/backpressure-send.ts
15165
+ var DEFAULT_BACKPRESSURE_THRESHOLD = 64 * 1024;
15166
+ var DEFAULT_MAX_QUEUE_BYTES = 1024 * 1024;
15167
+ var BUFFERED_AMOUNT_LOW_EVENT = "bufferedamountlow";
15168
+ function byteLength(data) {
15169
+ if (typeof data === "string") {
15170
+ return Buffer.byteLength(data);
15171
+ }
15172
+ return data.byteLength;
15173
+ }
15174
+ function createBackpressureSender(channel, options = {}) {
15175
+ const threshold = options.threshold ?? DEFAULT_BACKPRESSURE_THRESHOLD;
15176
+ const maxQueueBytes = options.maxQueueBytes ?? DEFAULT_MAX_QUEUE_BYTES;
15177
+ const dropOldest = options.dropOldest ?? false;
15178
+ const queue = [];
15179
+ let totalQueuedBytes = 0;
15180
+ let isPaused = false;
15181
+ function trySend(data) {
15182
+ try {
15183
+ channel.send(data);
15184
+ } catch {
15185
+ }
15186
+ }
15187
+ function drain() {
15188
+ while (queue.length > 0) {
15189
+ const buffered = channel.bufferedAmount;
15190
+ if (buffered !== void 0 && buffered > threshold) {
15191
+ return;
15192
+ }
15193
+ const entry = queue.shift();
15194
+ if (!entry) break;
15195
+ totalQueuedBytes -= entry.bytes;
15196
+ trySend(entry.data);
15197
+ }
15198
+ if (queue.length === 0) {
15199
+ isPaused = false;
15200
+ channel.removeEventListener?.(BUFFERED_AMOUNT_LOW_EVENT, drain);
15201
+ }
15202
+ }
15203
+ function evictOldest(requiredBytes) {
15204
+ let droppedBytes = 0;
15205
+ while (queue.length > 0 && totalQueuedBytes + requiredBytes > maxQueueBytes) {
15206
+ const oldest = queue.shift();
15207
+ if (!oldest) break;
15208
+ totalQueuedBytes -= oldest.bytes;
15209
+ droppedBytes += oldest.bytes;
15210
+ }
15211
+ if (droppedBytes > 0) {
15212
+ options.onDrop?.(droppedBytes);
15213
+ }
15214
+ }
15215
+ function enqueue(data) {
15216
+ const bytes = byteLength(data);
15217
+ if (totalQueuedBytes + bytes > maxQueueBytes) {
15218
+ if (dropOldest) {
15219
+ evictOldest(bytes);
15220
+ } else {
15221
+ options.onOverflow?.();
15222
+ return;
15223
+ }
15224
+ }
15225
+ queue.push({ data, bytes });
15226
+ totalQueuedBytes += bytes;
15227
+ }
15228
+ function startPause() {
15229
+ if (isPaused) return;
15230
+ isPaused = true;
15231
+ if (channel.addEventListener) {
15232
+ if (channel.bufferedAmountLowThreshold !== void 0) {
15233
+ channel.bufferedAmountLowThreshold = threshold;
15234
+ }
15235
+ channel.addEventListener(BUFFERED_AMOUNT_LOW_EVENT, drain);
15236
+ }
15237
+ }
15238
+ return {
15239
+ send(data) {
15240
+ if (channel.bufferedAmount === void 0) {
15241
+ trySend(data);
15242
+ return true;
15243
+ }
15244
+ if (!isPaused && channel.bufferedAmount <= threshold) {
15245
+ trySend(data);
15246
+ return true;
15247
+ }
15248
+ startPause();
15249
+ enqueue(data);
15250
+ return !isPaused || queue.length > 0;
15251
+ },
15252
+ dispose() {
15253
+ channel.removeEventListener?.(BUFFERED_AMOUNT_LOW_EVENT, drain);
15254
+ queue.length = 0;
15255
+ totalQueuedBytes = 0;
15256
+ isPaused = false;
15257
+ },
15258
+ get paused() {
15259
+ return isPaused;
15260
+ },
15261
+ get queuedBytes() {
15262
+ return totalQueuedBytes;
15263
+ }
15264
+ };
15265
+ }
15266
+
15267
+ // src/channel-buffer.ts
15268
+ function createChannelBuffer(channel, options) {
15269
+ const log = createChildLogger({ mode: options.logPrefix ?? "channel-buffer" });
15270
+ let open = channel.readyState === "open";
15271
+ const pending = [];
15272
+ let pendingBytes = 0;
15273
+ let sender = null;
15274
+ function getSender() {
15275
+ if (!sender) {
15276
+ sender = createBackpressureSender(channel, {
15277
+ threshold: options.backpressureThreshold,
15278
+ maxQueueBytes: options.maxBytes,
15279
+ dropOldest: options.dropOldest,
15280
+ onOverflow: options.onOverflow,
15281
+ onDrop: (bytes) => {
15282
+ log.debug({ droppedBytes: bytes }, "Backpressure dropped oldest data");
15283
+ }
15284
+ });
15285
+ }
15286
+ return sender;
15287
+ }
15288
+ return {
15289
+ get isOpen() {
15290
+ return open;
15291
+ },
15292
+ markOpen() {
15293
+ open = true;
15294
+ },
15295
+ sendOrBuffer(data) {
15296
+ if (open) {
15297
+ return getSender().send(data);
15298
+ }
15299
+ const byteLen = Buffer.byteLength(data);
15300
+ if (pendingBytes + byteLen > options.maxBytes) {
15301
+ log.warn({ pendingBytes, newBytes: byteLen, max: options.maxBytes }, "Buffer overflow");
15302
+ options.onOverflow?.();
15303
+ return false;
15304
+ }
15305
+ pending.push(data);
15306
+ pendingBytes += byteLen;
15307
+ return true;
15308
+ },
15309
+ flush() {
15310
+ const bp = getSender();
15311
+ for (const chunk of pending) {
15312
+ bp.send(chunk);
15313
+ }
15314
+ pending.length = 0;
15315
+ pendingBytes = 0;
15316
+ },
15317
+ reset() {
15318
+ pending.length = 0;
15319
+ pendingBytes = 0;
15320
+ open = false;
15321
+ sender?.dispose();
15322
+ sender = null;
15323
+ },
15324
+ dispose() {
15325
+ sender?.dispose();
15326
+ sender = null;
15327
+ }
15328
+ };
15329
+ }
15330
+
15331
+ // src/peer-manager.ts
15332
+ var ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
15333
+ function machineIdToPeerId(machineId) {
15334
+ return machineId;
15335
+ }
15336
+ async function loadDefaultFactory() {
15337
+ const { RTCPeerConnection } = await import("node-datachannel/polyfill");
15338
+ return () => {
15339
+ const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
15340
+ return pc;
15341
+ };
15342
+ }
15343
+ function parseTerminalChannelLabel(label) {
15344
+ if (!label.startsWith("terminal-io:")) return null;
15345
+ const suffix = label.slice("terminal-io:".length);
15346
+ const colonIdx = suffix.indexOf(":");
15347
+ const taskId = colonIdx === -1 ? suffix : suffix.slice(0, colonIdx);
15348
+ if (!taskId) return null;
15349
+ const terminalId = colonIdx === -1 || suffix.slice(colonIdx + 1) === "" ? crypto.randomUUID() : suffix.slice(colonIdx + 1);
15350
+ return { taskId, terminalId };
15351
+ }
15352
+ function guardLoroChannelSend(dc) {
15353
+ if (!dc.send) return;
15354
+ const originalSend = dc.send.bind(dc);
15355
+ dc.send = (data) => {
15356
+ try {
15357
+ originalSend(data);
15358
+ } catch {
15359
+ }
15360
+ };
15361
+ }
15362
+ function parseFileChannelLabel(label) {
15363
+ if (!label.startsWith("file-io:")) return null;
15364
+ const channelId = label.slice("file-io:".length);
15365
+ return channelId || null;
15366
+ }
15367
+ function createPeerManager(config2) {
15368
+ const peers = /* @__PURE__ */ new Map();
15369
+ const pendingCreates = /* @__PURE__ */ new Map();
15370
+ let factoryPromise = null;
15371
+ async function getFactory() {
15372
+ if (config2.createPeerConnection) {
15373
+ return config2.createPeerConnection;
15374
+ }
15375
+ if (!factoryPromise) {
15376
+ factoryPromise = loadDefaultFactory();
15377
+ }
15378
+ return factoryPromise;
15379
+ }
15380
+ function setupPeerHandlers(machineId, pc) {
15381
+ pc.onicecandidate = (event) => {
15382
+ if (event.candidate) {
15383
+ config2.onIceCandidate(machineId, {
15384
+ candidate: event.candidate.candidate,
15385
+ sdpMid: event.candidate.sdpMid,
15386
+ sdpMLineIndex: event.candidate.sdpMLineIndex
15387
+ });
15388
+ }
15389
+ };
15390
+ pc.ondatachannel = (event) => {
15391
+ const channel = event.channel;
15392
+ const label = channel.label ?? "";
15393
+ const terminalParsed = parseTerminalChannelLabel(label);
15394
+ if (terminalParsed) {
15395
+ logger.debug(
15396
+ { machineId, taskId: terminalParsed.taskId, terminalId: terminalParsed.terminalId },
15397
+ "Terminal data channel received"
15398
+ );
15399
+ config2.onTerminalChannel?.(
15400
+ machineId,
15401
+ event.channel,
15402
+ terminalParsed.taskId,
15403
+ terminalParsed.terminalId
15404
+ );
15405
+ return;
15406
+ }
15407
+ if (label.startsWith("terminal-io:")) {
15408
+ logger.warn({ machineId, label }, "Terminal channel with empty taskId");
15409
+ return;
15410
+ }
15411
+ const fileChannelId = parseFileChannelLabel(label);
15412
+ if (fileChannelId) {
15413
+ logger.info({ machineId, channelId: fileChannelId }, "File I/O data channel received");
15414
+ config2.onFileChannel?.(machineId, event.channel, fileChannelId);
15415
+ return;
15416
+ }
15417
+ if (label.startsWith("file-io:")) {
15418
+ logger.warn({ machineId }, "File channel with empty id, ignoring");
15419
+ return;
15420
+ }
15421
+ logger.info({ machineId }, "Data channel received from browser");
15422
+ const rawChannel = event.channel;
15423
+ guardLoroChannelSend(rawChannel);
15424
+ config2.webrtcAdapter.attachDataChannel(machineIdToPeerId(machineId), event.channel);
15425
+ logger.info({ machineId }, "Data channel attached to Loro adapter");
15426
+ };
15427
+ pc.onconnectionstatechange = () => {
15428
+ const state = pc.connectionState;
15429
+ logger.info({ machineId, state }, "Peer connection state changed");
15430
+ if (state === "failed" || state === "closed") {
15431
+ config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
15432
+ peers.delete(machineId);
15433
+ pc.close();
15434
+ }
15435
+ };
15436
+ pc.onsignalingstatechange = () => {
15437
+ const sigState = pc.signalingState;
15438
+ logger.info({ machineId, signalingState: sigState }, "Signaling state changed");
15439
+ };
15440
+ pc.onicegatheringstatechange = () => {
15441
+ const iceState = pc.iceGatheringState;
15442
+ logger.info({ machineId, iceGatheringState: iceState }, "ICE gathering state changed");
15443
+ };
15444
+ }
15445
+ return {
15446
+ async handleOffer(fromMachineId, offer) {
15447
+ logger.info({ fromMachineId }, "Handling WebRTC offer");
15448
+ const existing = peers.get(fromMachineId);
15449
+ if (existing) {
15450
+ logger.debug({ fromMachineId }, "Closing existing peer connection");
15451
+ existing.close();
15452
+ peers.delete(fromMachineId);
15453
+ }
15454
+ const promise = (async () => {
15455
+ const factory = await getFactory();
15456
+ const pc = factory();
15457
+ logger.debug({ fromMachineId }, "Created peer connection");
15458
+ setupPeerHandlers(fromMachineId, pc);
15459
+ logger.debug({ fromMachineId }, "Setting remote description (offer)");
15460
+ await pc.setRemoteDescription(offer);
15461
+ logger.debug({ fromMachineId }, "Creating answer");
15462
+ const answer = await pc.createAnswer();
15463
+ logger.debug(
15464
+ { fromMachineId, hasAnswerSdp: !!answer.sdp },
15465
+ "Setting local description (answer)"
15466
+ );
15467
+ await pc.setLocalDescription(answer);
15468
+ peers.set(fromMachineId, pc);
15469
+ pendingCreates.delete(fromMachineId);
15470
+ logger.info({ fromMachineId }, "Sending WebRTC answer");
15471
+ config2.onAnswer(fromMachineId, { type: "answer", sdp: answer.sdp });
15472
+ return pc;
15473
+ })();
15474
+ pendingCreates.set(fromMachineId, promise);
15475
+ const HANDSHAKE_TIMEOUT_MS = 3e4;
15476
+ setTimeout(() => {
15477
+ if (pendingCreates.get(fromMachineId) === promise) {
15478
+ pendingCreates.delete(fromMachineId);
15479
+ logger.warn({ fromMachineId }, "WebRTC handshake timed out");
15480
+ }
15481
+ }, HANDSHAKE_TIMEOUT_MS);
15482
+ await promise;
15483
+ },
15484
+ async initiateOffer(targetMachineId) {
15485
+ logger.info({ targetMachineId }, "Initiating WebRTC offer");
15486
+ const existing = peers.get(targetMachineId);
15487
+ if (existing) {
15488
+ logger.debug({ targetMachineId }, "Closing existing peer connection");
15489
+ existing.close();
15490
+ peers.delete(targetMachineId);
15491
+ }
15492
+ const promise = (async () => {
15493
+ const factory = await getFactory();
15494
+ const pc = factory();
15495
+ logger.debug({ targetMachineId }, "Created peer connection");
15496
+ setupPeerHandlers(targetMachineId, pc);
15497
+ if (!pc.createDataChannel) {
15498
+ throw new Error("PeerConnection does not support createDataChannel");
15499
+ }
15500
+ if (!pc.createOffer) {
15501
+ throw new Error("PeerConnection does not support createOffer");
15502
+ }
15503
+ logger.debug({ targetMachineId }, "Creating loro-sync data channel");
15504
+ const channel = pc.createDataChannel("loro-sync", { ordered: true });
15505
+ const rawChannel = channel;
15506
+ guardLoroChannelSend(rawChannel);
15507
+ rawChannel.onopen = () => {
15508
+ config2.webrtcAdapter.attachDataChannel(
15509
+ machineIdToPeerId(targetMachineId),
15510
+ channel
15511
+ );
15512
+ logger.debug({ targetMachineId }, "Data channel attached to Loro adapter");
15513
+ };
15514
+ logger.debug({ targetMachineId }, "Creating offer");
15515
+ const offer = await pc.createOffer();
15516
+ logger.debug(
15517
+ { targetMachineId, hasOfferSdp: !!offer.sdp },
15518
+ "Setting local description (offer)"
15519
+ );
15520
+ await pc.setLocalDescription(offer);
15521
+ peers.set(targetMachineId, pc);
15522
+ pendingCreates.delete(targetMachineId);
15523
+ logger.info({ targetMachineId }, "Sending WebRTC offer");
15524
+ config2.onOffer?.(targetMachineId, { type: "offer", sdp: offer.sdp });
15525
+ return pc;
15526
+ })();
15527
+ pendingCreates.set(targetMachineId, promise);
15528
+ const HANDSHAKE_TIMEOUT_MS = 3e4;
15529
+ setTimeout(() => {
15530
+ if (pendingCreates.get(targetMachineId) === promise) {
15531
+ pendingCreates.delete(targetMachineId);
15532
+ logger.warn({ targetMachineId }, "WebRTC handshake timed out (initiator)");
15533
+ }
15534
+ }, HANDSHAKE_TIMEOUT_MS);
15535
+ await promise;
15536
+ },
15537
+ async handleAnswer(fromMachineId, answer) {
15538
+ logger.debug({ fromMachineId }, "Handling WebRTC answer");
15539
+ let pc = peers.get(fromMachineId);
15540
+ if (!pc) {
15541
+ const pending = pendingCreates.get(fromMachineId);
15542
+ if (pending) {
15543
+ pc = await pending;
15544
+ } else {
15545
+ logger.warn({ fromMachineId }, "Received answer for unknown peer");
15546
+ return;
15547
+ }
15548
+ }
15549
+ await pc.setRemoteDescription(answer);
15550
+ logger.debug({ fromMachineId }, "Remote description (answer) set");
15551
+ },
15552
+ async handleIce(fromMachineId, candidate) {
15553
+ logger.debug({ fromMachineId }, "Handling WebRTC ICE candidate");
15554
+ let pc = peers.get(fromMachineId);
15555
+ if (!pc) {
15556
+ const pending = pendingCreates.get(fromMachineId);
15557
+ if (pending) {
15558
+ pc = await pending;
15559
+ } else {
15560
+ logger.debug({ fromMachineId }, "Received ICE candidate for unknown peer");
15561
+ return;
15562
+ }
15563
+ }
15564
+ await pc.addIceCandidate(candidate);
15565
+ logger.debug({ fromMachineId }, "ICE candidate added");
15566
+ },
15567
+ closePeer(targetId) {
15568
+ const pc = peers.get(targetId);
15569
+ if (pc) {
15570
+ config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(targetId));
15571
+ pc.close();
15572
+ peers.delete(targetId);
15573
+ }
15574
+ },
15575
+ destroy() {
15576
+ for (const [machineId, pc] of peers) {
15577
+ config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
15578
+ pc.close();
15579
+ }
15580
+ peers.clear();
15581
+ pendingCreates.clear();
15582
+ }
15583
+ };
15584
+ }
15585
+
15586
+ // src/collab-room-manager.ts
15587
+ function assertNever2(x) {
15588
+ throw new Error(`Unhandled message type: ${JSON.stringify(x)}`);
15589
+ }
15590
+ function namespacePeerId(roomId, remoteUserId) {
15591
+ return `collab:${roomId}:${remoteUserId}`;
15592
+ }
15593
+ function stripPeerIdNamespace(roomId, namespacedId) {
15594
+ const prefix = `collab:${roomId}:`;
15595
+ if (!namespacedId.startsWith(prefix)) {
15596
+ throw new Error(`Expected namespaced peer ID with prefix "${prefix}", got "${namespacedId}"`);
15597
+ }
15598
+ return namespacedId.slice(prefix.length);
15599
+ }
15600
+ function handleAuthenticated(room, msg) {
15601
+ room.myUserId = msg.userId;
15602
+ room.log.info({ roomId: room.roomId, userId: msg.userId }, "Authenticated in collab room");
15603
+ }
15604
+ function maybeInitiateOffer(room, remoteUserId, context) {
15605
+ if (!room.myUserId) return;
15606
+ room.knownPeers.add(remoteUserId);
15607
+ const shouldInitiate = room.myUserId < remoteUserId;
15608
+ if (!shouldInitiate) return;
15609
+ room.log.info({ roomId: room.roomId, targetUserId: remoteUserId }, context);
15610
+ const namespacedPeerId = namespacePeerId(room.roomId, remoteUserId);
15611
+ room.peerManager.initiateOffer(namespacedPeerId).catch((err) => {
15612
+ room.log.error(
15613
+ { roomId: room.roomId, targetUserId: remoteUserId, err },
15614
+ "Failed to initiate offer"
15615
+ );
15616
+ });
15617
+ }
15618
+ function handleParticipantsList(room, msg) {
15619
+ if (!room.myUserId) {
15620
+ room.log.warn({ roomId: room.roomId }, "Received participants-list before authentication");
15621
+ return;
15622
+ }
15623
+ for (const participant of msg.participants) {
15624
+ if (participant.userId === room.myUserId) continue;
15625
+ if (room.knownPeers.has(participant.userId)) continue;
15626
+ maybeInitiateOffer(room, participant.userId, "Initiating WebRTC offer to existing participant");
15627
+ }
15628
+ }
15629
+ function handleParticipantJoined(room, msg) {
15630
+ if (!room.myUserId) return;
15631
+ if (msg.participant.userId === room.myUserId) return;
15632
+ maybeInitiateOffer(room, msg.participant.userId, "Initiating WebRTC offer to new participant");
15633
+ }
15634
+ function handleWebrtcOffer(room, msg) {
15635
+ room.log.info({ roomId: room.roomId, fromUserId: msg.targetUserId }, "Received WebRTC offer");
15636
+ const offer = msg.offer;
15637
+ const namespacedPeerId = namespacePeerId(room.roomId, msg.targetUserId);
15638
+ room.peerManager.handleOffer(namespacedPeerId, offer).catch((err) => {
15639
+ room.log.error(
15640
+ { roomId: room.roomId, fromUserId: msg.targetUserId, err },
15641
+ "Failed to handle WebRTC offer"
15642
+ );
15643
+ });
15644
+ }
15645
+ function handleWebrtcAnswer(room, msg) {
15646
+ room.log.info({ roomId: room.roomId, fromUserId: msg.targetUserId }, "Received WebRTC answer");
15647
+ const answer = msg.answer;
15648
+ const namespacedPeerId = namespacePeerId(room.roomId, msg.targetUserId);
15649
+ room.peerManager.handleAnswer(namespacedPeerId, answer).catch((err) => {
15650
+ room.log.error(
15651
+ { roomId: room.roomId, fromUserId: msg.targetUserId, err },
15652
+ "Failed to handle WebRTC answer"
15653
+ );
15654
+ });
15655
+ }
15656
+ function handleWebrtcIce(room, msg) {
15657
+ room.log.debug({ roomId: room.roomId, fromUserId: msg.targetUserId }, "Received ICE candidate");
15658
+ const candidate = msg.candidate;
15659
+ const namespacedPeerId = namespacePeerId(room.roomId, msg.targetUserId);
15660
+ room.peerManager.handleIce(namespacedPeerId, candidate).catch((err) => {
15661
+ room.log.error(
15662
+ { roomId: room.roomId, fromUserId: msg.targetUserId, err },
15663
+ "Failed to handle ICE candidate"
15664
+ );
15665
+ });
15666
+ }
15667
+ function handleCollabRoomMessage(room, msg) {
15668
+ switch (msg.type) {
15669
+ case "authenticated":
15670
+ handleAuthenticated(room, msg);
15671
+ break;
15672
+ case "participants-list":
15673
+ handleParticipantsList(room, msg);
15674
+ break;
15675
+ case "participant-joined":
15676
+ handleParticipantJoined(room, msg);
15677
+ break;
15678
+ case "participant-left":
15679
+ room.knownPeers.delete(msg.userId);
15680
+ room.peerManager.closePeer(namespacePeerId(room.roomId, msg.userId));
15681
+ room.log.info({ roomId: room.roomId, userId: msg.userId }, "Participant left collab room");
15682
+ break;
15683
+ case "webrtc-offer":
15684
+ handleWebrtcOffer(room, msg);
15685
+ break;
15686
+ case "webrtc-answer":
15687
+ handleWebrtcAnswer(room, msg);
15688
+ break;
15689
+ case "webrtc-ice":
15690
+ handleWebrtcIce(room, msg);
15691
+ break;
15692
+ case "error":
15693
+ room.log.error({ roomId: room.roomId, message: msg.message }, "Collab room error");
15694
+ break;
15695
+ default:
15696
+ assertNever2(msg);
15697
+ }
15698
+ }
15699
+ function createRoomPeerManager(roomId, connection, webrtcAdapter) {
15700
+ return createPeerManager({
15701
+ webrtcAdapter,
15702
+ onAnswer(namespacedId, answer) {
15703
+ const targetUserId = stripPeerIdNamespace(roomId, namespacedId);
15704
+ connection.send({
15705
+ type: "webrtc-answer",
15706
+ targetUserId,
15707
+ // eslint-disable-next-line no-restricted-syntax -- SDP is opaque over signaling
15708
+ answer
15709
+ });
15710
+ },
15711
+ onOffer(namespacedId, offer) {
15712
+ const targetUserId = stripPeerIdNamespace(roomId, namespacedId);
15713
+ connection.send({
15714
+ type: "webrtc-offer",
15715
+ targetUserId,
15716
+ // eslint-disable-next-line no-restricted-syntax -- SDP is opaque over signaling
15717
+ offer
15718
+ });
15719
+ },
15720
+ onIceCandidate(namespacedId, candidate) {
15721
+ const targetUserId = stripPeerIdNamespace(roomId, namespacedId);
15722
+ connection.send({
15723
+ type: "webrtc-ice",
15724
+ targetUserId,
15725
+ // eslint-disable-next-line no-restricted-syntax -- ICE candidate is opaque over signaling
15726
+ candidate
15727
+ });
15728
+ }
15729
+ });
15730
+ }
15731
+ function createCollabRoomManager() {
15732
+ const rooms = /* @__PURE__ */ new Map();
15733
+ function leaveRoom(roomId) {
15734
+ const room = rooms.get(roomId);
15735
+ if (!room) return;
15736
+ room.log.info({ roomId: room.roomId }, "Leaving collab room");
15737
+ clearTimeout(room.expiryTimer);
15738
+ room.unsubMessage();
15739
+ room.unsubState();
15740
+ room.peerManager.destroy();
15741
+ room.connection.disconnect();
15742
+ rooms.delete(roomId);
15743
+ }
15744
+ return {
15745
+ join(config2) {
15746
+ const { roomId, taskId, token, expiresAt, signalingBaseUrl, userToken, machineId, log } = config2;
15747
+ if (rooms.has(roomId)) {
15748
+ log.info({ roomId }, "Already joined collab room, replacing connection");
15749
+ leaveRoom(roomId);
15750
+ }
15751
+ const wsUrl = new URL(signalingBaseUrl);
15752
+ wsUrl.pathname = ROUTES.WS_COLLAB.replace(":roomId", roomId);
15753
+ wsUrl.searchParams.set("token", token);
15754
+ wsUrl.searchParams.set("userToken", userToken);
15755
+ wsUrl.searchParams.set("clientType", "agent");
15756
+ wsUrl.searchParams.set("machineId", machineId);
15757
+ const connection = new CollabRoomConnection({
15758
+ url: wsUrl.toString(),
15759
+ maxRetries: -1,
15760
+ initialDelayMs: 1e3,
15761
+ maxDelayMs: 3e4,
15762
+ backoffMultiplier: 2
15763
+ });
15764
+ const peerManager = createRoomPeerManager(roomId, connection, config2.webrtcAdapter);
15765
+ const handle = {
15766
+ roomId,
15767
+ taskId,
15768
+ destroy() {
15769
+ leaveRoom(roomId);
15770
+ }
15771
+ };
15772
+ const delayMs = expiresAt - Date.now();
15773
+ if (delayMs <= 0 || !Number.isFinite(delayMs)) {
15774
+ log.warn({ expiresAt }, "Collab room already expired or invalid expiresAt");
15775
+ return handle;
15776
+ }
15777
+ const room = {
15778
+ roomId,
15779
+ taskId,
15780
+ connection,
15781
+ peerManager,
15782
+ myUserId: null,
15783
+ knownPeers: /* @__PURE__ */ new Set(),
15784
+ expiryTimer: setTimeout(() => {
15785
+ log.info({ roomId }, "Collab room token expired, leaving");
15786
+ leaveRoom(roomId);
15787
+ }, delayMs),
15788
+ unsubMessage: () => {
15789
+ },
15790
+ unsubState: () => {
15791
+ },
15792
+ log
15793
+ };
15794
+ rooms.set(roomId, room);
15795
+ room.unsubMessage = connection.onMessage((msg) => {
15796
+ handleCollabRoomMessage(room, msg);
15797
+ });
15798
+ room.unsubState = connection.onStateChange((state) => {
15799
+ log.info({ roomId, state }, "Collab room connection state changed");
15800
+ if (state === "disconnected" || state === "error") {
15801
+ room.peerManager.destroy();
15802
+ room.peerManager = createRoomPeerManager(roomId, connection, config2.webrtcAdapter);
15803
+ room.myUserId = null;
15804
+ room.knownPeers.clear();
15805
+ }
15806
+ });
15807
+ connection.connect();
15808
+ log.info({ roomId, taskId, url: `${wsUrl.origin}${wsUrl.pathname}` }, "Joining collab room");
15809
+ return handle;
15810
+ },
15811
+ leave(roomId) {
15812
+ leaveRoom(roomId);
15813
+ },
15814
+ destroy() {
15815
+ for (const roomId of [...rooms.keys()]) {
15816
+ leaveRoom(roomId);
15817
+ }
15818
+ }
15819
+ };
15820
+ }
15821
+
14786
15822
  // src/crash-recovery.ts
14787
15823
  function recoverOrphanedTask(taskDocs, log) {
14788
15824
  const metaJson = taskDocs.meta.toJSON();
@@ -14811,10 +15847,10 @@ function recoverOrphanedTask(taskDocs, log) {
14811
15847
  });
14812
15848
  }
14813
15849
  change(taskDocs.meta, (draft) => {
14814
- draft.meta.status.set("failed");
15850
+ draft.meta.status.set("input-required");
14815
15851
  draft.meta.updatedAt.set(Date.now());
14816
15852
  });
14817
- log.info({ previousStatus: status }, "Recovered orphaned task after daemon crash");
15853
+ log.info({ previousStatus: status }, "Recovered orphaned task after daemon crash (resumable)");
14818
15854
  return true;
14819
15855
  }
14820
15856
 
@@ -14910,15 +15946,52 @@ async function runEnhanceQuery(prompt, systemPrompt, abortController, callbacks,
14910
15946
  );
14911
15947
  }
14912
15948
 
15949
+ // src/file-lister.ts
15950
+ import { execFile as execFile2 } from "child_process";
15951
+ var TIMEOUT_MS2 = 1e4;
15952
+ var MAX_BUFFER = 5 * 1024 * 1024;
15953
+ function listWorkspaceFiles(repoPath) {
15954
+ return new Promise((resolve4, reject) => {
15955
+ execFile2(
15956
+ "git",
15957
+ ["ls-files", "--cached", "--others", "--exclude-standard"],
15958
+ { timeout: TIMEOUT_MS2, maxBuffer: MAX_BUFFER, cwd: repoPath },
15959
+ (error2, stdout) => {
15960
+ if (error2) {
15961
+ reject(error2);
15962
+ return;
15963
+ }
15964
+ const lines = stdout.split("\n").filter(Boolean);
15965
+ if (lines.length === 0) {
15966
+ resolve4([]);
15967
+ return;
15968
+ }
15969
+ const dirSet = /* @__PURE__ */ new Set();
15970
+ const files = [];
15971
+ for (const line of lines) {
15972
+ files.push({ path: line, isDirectory: false });
15973
+ const segments = line.split("/");
15974
+ for (let i = 1; i < segments.length; i++) {
15975
+ dirSet.add(segments.slice(0, i).join("/"));
15976
+ }
15977
+ }
15978
+ const dirs = [...dirSet].sort((a, b) => a.localeCompare(b)).map((p) => ({ path: p, isDirectory: true }));
15979
+ const sortedFiles = files.sort((a, b) => a.path.localeCompare(b.path));
15980
+ resolve4([...dirs, ...sortedFiles]);
15981
+ }
15982
+ );
15983
+ });
15984
+ }
15985
+
14913
15986
  // src/keep-awake.ts
14914
15987
  import { spawn } from "child_process";
14915
15988
  var darwinStrategy = {
14916
- spawn: () => spawn("caffeinate", ["-i"], { stdio: ["ignore", "ignore", "ignore"] })
15989
+ spawn: () => spawn("caffeinate", ["-di"], { stdio: ["ignore", "ignore", "ignore"] })
14917
15990
  };
14918
15991
  var linuxStrategy = {
14919
15992
  spawn: () => spawn(
14920
15993
  "systemd-inhibit",
14921
- ["--what=idle", "--why=Shipyard agent tasks running", "sleep", "infinity"],
15994
+ ["--what=idle:sleep", "--why=Shipyard agent tasks running", "sleep", "infinity"],
14922
15995
  { stdio: ["ignore", "ignore", "ignore"] }
14923
15996
  )
14924
15997
  };
@@ -14928,7 +16001,7 @@ var win32Strategy = {
14928
16001
  [
14929
16002
  "-NoProfile",
14930
16003
  "-Command",
14931
- "[System.Runtime.InteropServices.Marshal]::SetThreadExecutionState(0x80000001); Start-Sleep -Seconds 2147483"
16004
+ "[System.Runtime.InteropServices.Marshal]::SetThreadExecutionState(0x80000003); Start-Sleep -Seconds 2147483"
14932
16005
  ],
14933
16006
  { stdio: ["ignore", "ignore", "ignore"] }
14934
16007
  )
@@ -14953,9 +16026,12 @@ var KeepAwakeManager = class {
14953
16026
  #shouldBeRunning = false;
14954
16027
  #restartAttempts = 0;
14955
16028
  #log;
14956
- constructor(log) {
16029
+ #graceTimer = null;
16030
+ #gracePeriodMs;
16031
+ constructor(log, gracePeriodMs = KEEP_AWAKE_GRACE_PERIOD_MS) {
14957
16032
  this.#strategy = createKeepAwakeStrategy();
14958
16033
  this.#log = log;
16034
+ this.#gracePeriodMs = gracePeriodMs;
14959
16035
  if (!this.#strategy) {
14960
16036
  log.info({ platform: process.platform }, "Keep-awake not supported on this platform");
14961
16037
  }
@@ -14963,20 +16039,50 @@ var KeepAwakeManager = class {
14963
16039
  get running() {
14964
16040
  return this.#child !== null;
14965
16041
  }
16042
+ get graceActive() {
16043
+ return this.#graceTimer !== null;
16044
+ }
14966
16045
  update(enabled, hasActiveTasks) {
14967
16046
  const shouldRun = enabled && hasActiveTasks;
14968
- this.#shouldBeRunning = shouldRun;
14969
16047
  this.#restartAttempts = 0;
14970
- if (shouldRun && !this.#child) {
14971
- this.#start();
14972
- } else if (!shouldRun && this.#child) {
16048
+ if (shouldRun) {
16049
+ this.#cancelGrace();
16050
+ this.#shouldBeRunning = true;
16051
+ if (!this.#child) {
16052
+ this.#start();
16053
+ }
16054
+ } else if (this.#graceTimer && enabled && !hasActiveTasks) {
16055
+ return;
16056
+ } else if (this.#shouldBeRunning && enabled && !hasActiveTasks) {
16057
+ this.#shouldBeRunning = false;
16058
+ this.#startGrace();
16059
+ } else if (!shouldRun) {
16060
+ this.#shouldBeRunning = false;
16061
+ this.#cancelGrace();
14973
16062
  this.#stop();
14974
16063
  }
14975
16064
  }
14976
16065
  shutdown() {
14977
16066
  this.#shouldBeRunning = false;
16067
+ this.#cancelGrace();
14978
16068
  this.#stop();
14979
16069
  }
16070
+ #startGrace() {
16071
+ if (this.#graceTimer) return;
16072
+ this.#log.info({ gracePeriodMs: this.#gracePeriodMs }, "Keep-awake grace period started");
16073
+ this.#graceTimer = setTimeout(() => {
16074
+ this.#graceTimer = null;
16075
+ this.#log.info("Keep-awake grace period expired");
16076
+ this.#stop();
16077
+ }, this.#gracePeriodMs);
16078
+ this.#graceTimer.unref();
16079
+ }
16080
+ #cancelGrace() {
16081
+ if (!this.#graceTimer) return;
16082
+ clearTimeout(this.#graceTimer);
16083
+ this.#graceTimer = null;
16084
+ this.#log.info("Keep-awake grace period cancelled (new task started)");
16085
+ }
14980
16086
  #start() {
14981
16087
  if (!this.#strategy || this.#child) return;
14982
16088
  try {
@@ -15003,7 +16109,7 @@ var KeepAwakeManager = class {
15003
16109
  }
15004
16110
  }
15005
16111
  #scheduleRestart() {
15006
- if (!this.#shouldBeRunning) return;
16112
+ if (!this.#shouldBeRunning && !this.#graceTimer) return;
15007
16113
  this.#restartAttempts++;
15008
16114
  if (this.#restartAttempts > MAX_RESTART_ATTEMPTS) {
15009
16115
  this.#log.warn(
@@ -15018,7 +16124,7 @@ var KeepAwakeManager = class {
15018
16124
  "Scheduling keep-awake restart"
15019
16125
  );
15020
16126
  const timer = setTimeout(() => {
15021
- if (this.#shouldBeRunning && !this.#child) {
16127
+ if ((this.#shouldBeRunning || this.#graceTimer) && !this.#child) {
15022
16128
  this.#start();
15023
16129
  }
15024
16130
  }, delay);
@@ -15036,158 +16142,6 @@ var KeepAwakeManager = class {
15036
16142
  }
15037
16143
  };
15038
16144
 
15039
- // src/peer-manager.ts
15040
- var ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
15041
- function machineIdToPeerId(machineId) {
15042
- return machineId;
15043
- }
15044
- async function loadDefaultFactory() {
15045
- const { RTCPeerConnection } = await import("node-datachannel/polyfill");
15046
- return () => {
15047
- const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
15048
- return pc;
15049
- };
15050
- }
15051
- function createPeerManager(config2) {
15052
- const peers = /* @__PURE__ */ new Map();
15053
- const pendingCreates = /* @__PURE__ */ new Map();
15054
- let factoryPromise = null;
15055
- async function getFactory() {
15056
- if (config2.createPeerConnection) {
15057
- return config2.createPeerConnection;
15058
- }
15059
- if (!factoryPromise) {
15060
- factoryPromise = loadDefaultFactory();
15061
- }
15062
- return factoryPromise;
15063
- }
15064
- function setupPeerHandlers(machineId, pc) {
15065
- pc.onicecandidate = (event) => {
15066
- if (event.candidate) {
15067
- config2.onIceCandidate(machineId, {
15068
- candidate: event.candidate.candidate,
15069
- sdpMid: event.candidate.sdpMid,
15070
- sdpMLineIndex: event.candidate.sdpMLineIndex
15071
- });
15072
- }
15073
- };
15074
- pc.ondatachannel = (event) => {
15075
- const channel = event.channel;
15076
- if (channel.label?.startsWith("terminal-io:")) {
15077
- const taskId = channel.label.slice("terminal-io:".length);
15078
- if (!taskId) {
15079
- logger.warn({ machineId }, "Terminal channel with empty taskId, ignoring");
15080
- return;
15081
- }
15082
- logger.info({ machineId, taskId }, "Terminal data channel received");
15083
- config2.onTerminalChannel?.(machineId, event.channel, taskId);
15084
- } else {
15085
- logger.info({ machineId }, "Data channel received from browser");
15086
- config2.webrtcAdapter.attachDataChannel(
15087
- machineIdToPeerId(machineId),
15088
- event.channel
15089
- );
15090
- logger.info({ machineId }, "Data channel attached to Loro adapter");
15091
- }
15092
- };
15093
- pc.onconnectionstatechange = () => {
15094
- const state = pc.connectionState;
15095
- logger.info({ machineId, state }, "Peer connection state changed");
15096
- if (state === "failed" || state === "closed") {
15097
- config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
15098
- peers.delete(machineId);
15099
- pc.close();
15100
- }
15101
- };
15102
- pc.onsignalingstatechange = () => {
15103
- const sigState = pc.signalingState;
15104
- logger.info({ machineId, signalingState: sigState }, "Signaling state changed");
15105
- };
15106
- pc.onicegatheringstatechange = () => {
15107
- const iceState = pc.iceGatheringState;
15108
- logger.info({ machineId, iceGatheringState: iceState }, "ICE gathering state changed");
15109
- };
15110
- }
15111
- return {
15112
- async handleOffer(fromMachineId, offer) {
15113
- logger.info({ fromMachineId }, "Handling WebRTC offer");
15114
- const existing = peers.get(fromMachineId);
15115
- if (existing) {
15116
- logger.debug({ fromMachineId }, "Closing existing peer connection");
15117
- existing.close();
15118
- peers.delete(fromMachineId);
15119
- }
15120
- const promise = (async () => {
15121
- const factory = await getFactory();
15122
- const pc = factory();
15123
- logger.debug({ fromMachineId }, "Created peer connection");
15124
- setupPeerHandlers(fromMachineId, pc);
15125
- logger.debug({ fromMachineId }, "Setting remote description (offer)");
15126
- await pc.setRemoteDescription(offer);
15127
- logger.debug({ fromMachineId }, "Creating answer");
15128
- const answer = await pc.createAnswer();
15129
- logger.debug(
15130
- { fromMachineId, hasAnswerSdp: !!answer.sdp },
15131
- "Setting local description (answer)"
15132
- );
15133
- await pc.setLocalDescription(answer);
15134
- peers.set(fromMachineId, pc);
15135
- pendingCreates.delete(fromMachineId);
15136
- logger.info({ fromMachineId }, "Sending WebRTC answer");
15137
- config2.onAnswer(fromMachineId, { type: "answer", sdp: answer.sdp });
15138
- return pc;
15139
- })();
15140
- pendingCreates.set(fromMachineId, promise);
15141
- const HANDSHAKE_TIMEOUT_MS = 3e4;
15142
- setTimeout(() => {
15143
- if (pendingCreates.get(fromMachineId) === promise) {
15144
- pendingCreates.delete(fromMachineId);
15145
- logger.warn({ fromMachineId }, "WebRTC handshake timed out");
15146
- }
15147
- }, HANDSHAKE_TIMEOUT_MS);
15148
- await promise;
15149
- },
15150
- async handleAnswer(fromMachineId, answer) {
15151
- logger.debug({ fromMachineId }, "Handling WebRTC answer");
15152
- let pc = peers.get(fromMachineId);
15153
- if (!pc) {
15154
- const pending = pendingCreates.get(fromMachineId);
15155
- if (pending) {
15156
- pc = await pending;
15157
- } else {
15158
- logger.warn({ fromMachineId }, "Received answer for unknown peer");
15159
- return;
15160
- }
15161
- }
15162
- await pc.setRemoteDescription(answer);
15163
- logger.debug({ fromMachineId }, "Remote description (answer) set");
15164
- },
15165
- async handleIce(fromMachineId, candidate) {
15166
- logger.debug({ fromMachineId }, "Handling WebRTC ICE candidate");
15167
- let pc = peers.get(fromMachineId);
15168
- if (!pc) {
15169
- const pending = pendingCreates.get(fromMachineId);
15170
- if (pending) {
15171
- pc = await pending;
15172
- } else {
15173
- logger.warn({ fromMachineId }, "Received ICE candidate for unknown peer");
15174
- return;
15175
- }
15176
- }
15177
- await pc.addIceCandidate(candidate);
15178
- logger.debug({ fromMachineId }, "ICE candidate added");
15179
- },
15180
- destroy() {
15181
- for (const [machineId, pc] of peers) {
15182
- config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
15183
- pc.close();
15184
- }
15185
- peers.clear();
15186
- pendingCreates.clear();
15187
- }
15188
- };
15189
- }
15190
-
15191
16145
  // src/plan-editor/format-diff-feedback.ts
15192
16146
  function formatDiffFeedbackForClaudeCode(comments, generalFeedback) {
15193
16147
  const sections = [];
@@ -21261,8 +22215,8 @@ var EditorState = class _EditorState {
21261
22215
  throw new RangeError("Applying a mismatched transaction");
21262
22216
  let newInstance = new _EditorState(this.config), fields = this.config.fields;
21263
22217
  for (let i = 0; i < fields.length; i++) {
21264
- let field = fields[i];
21265
- newInstance[field.name] = field.apply(tr2, this[field.name], this, newInstance);
22218
+ let field2 = fields[i];
22219
+ newInstance[field2.name] = field2.apply(tr2, this[field2.name], this, newInstance);
21266
22220
  }
21267
22221
  return newInstance;
21268
22222
  }
@@ -21334,24 +22288,24 @@ var EditorState = class _EditorState {
21334
22288
  throw new RangeError("Required config field 'schema' missing");
21335
22289
  let $config = new Configuration(config2.schema, config2.plugins);
21336
22290
  let instance = new _EditorState($config);
21337
- $config.fields.forEach((field) => {
21338
- if (field.name == "doc") {
22291
+ $config.fields.forEach((field2) => {
22292
+ if (field2.name == "doc") {
21339
22293
  instance.doc = Node.fromJSON(config2.schema, json.doc);
21340
- } else if (field.name == "selection") {
22294
+ } else if (field2.name == "selection") {
21341
22295
  instance.selection = Selection.fromJSON(instance.doc, json.selection);
21342
- } else if (field.name == "storedMarks") {
22296
+ } else if (field2.name == "storedMarks") {
21343
22297
  if (json.storedMarks)
21344
22298
  instance.storedMarks = json.storedMarks.map(config2.schema.markFromJSON);
21345
22299
  } else {
21346
22300
  if (pluginFields)
21347
22301
  for (let prop in pluginFields) {
21348
22302
  let plugin = pluginFields[prop], state = plugin.spec.state;
21349
- if (plugin.key == field.name && state && state.fromJSON && Object.prototype.hasOwnProperty.call(json, prop)) {
21350
- instance[field.name] = state.fromJSON.call(plugin, config2, json[prop], instance);
22303
+ if (plugin.key == field2.name && state && state.fromJSON && Object.prototype.hasOwnProperty.call(json, prop)) {
22304
+ instance[field2.name] = state.fromJSON.call(plugin, config2, json[prop], instance);
21351
22305
  return;
21352
22306
  }
21353
22307
  }
21354
- instance[field.name] = field.init(config2, instance);
22308
+ instance[field2.name] = field2.init(config2, instance);
21355
22309
  }
21356
22310
  });
21357
22311
  return instance;
@@ -29057,18 +30011,18 @@ function findParentNodeClosestToPos($pos, predicate) {
29057
30011
  function findParentNode(predicate) {
29058
30012
  return (selection) => findParentNodeClosestToPos(selection.$from, predicate);
29059
30013
  }
29060
- function getExtensionField(extension, field, context) {
29061
- if (extension.config[field] === void 0 && extension.parent) {
29062
- return getExtensionField(extension.parent, field, context);
30014
+ function getExtensionField(extension, field2, context) {
30015
+ if (extension.config[field2] === void 0 && extension.parent) {
30016
+ return getExtensionField(extension.parent, field2, context);
29063
30017
  }
29064
- if (typeof extension.config[field] === "function") {
29065
- const value = extension.config[field].bind({
30018
+ if (typeof extension.config[field2] === "function") {
30019
+ const value = extension.config[field2].bind({
29066
30020
  ...context,
29067
- parent: extension.parent ? getExtensionField(extension.parent, field, context) : null
30021
+ parent: extension.parent ? getExtensionField(extension.parent, field2, context) : null
29068
30022
  });
29069
30023
  return value;
29070
30024
  }
29071
- return extension.config[field];
30025
+ return extension.config[field2];
29072
30026
  }
29073
30027
  function flattenExtensions(extensions) {
29074
30028
  return extensions.map((extension) => {
@@ -43619,7 +44573,7 @@ function initPlanEditorDoc(loroDoc, planId, markdown) {
43619
44573
  loroDoc.commit();
43620
44574
  return true;
43621
44575
  } catch (error2) {
43622
- logger.warn({ planId, error: error2 }, "initPlanEditorDoc failed");
44576
+ logger.warn({ planId, err: error2 }, "initPlanEditorDoc failed");
43623
44577
  return false;
43624
44578
  }
43625
44579
  }
@@ -43681,6 +44635,7 @@ ensureSpawnHelperExecutable();
43681
44635
  var KILL_TIMEOUT_MS = 5e3;
43682
44636
  var DEFAULT_COLS = 80;
43683
44637
  var DEFAULT_ROWS = 24;
44638
+ var SCROLLBACK_MAX_BYTES = 5e4;
43684
44639
  function createPtyManager() {
43685
44640
  const log = createChildLogger({ mode: "pty" });
43686
44641
  let process2 = null;
@@ -43688,6 +44643,10 @@ function createPtyManager() {
43688
44643
  let killTimer = null;
43689
44644
  const dataCallbacks = [];
43690
44645
  const exitCallbacks = [];
44646
+ let dataSink = null;
44647
+ let exitSink = null;
44648
+ const scrollbackChunks = [];
44649
+ let scrollbackBytes = 0;
43691
44650
  function getDefaultShell() {
43692
44651
  return globalThis.process.env.SHELL ?? "/bin/zsh";
43693
44652
  }
@@ -43718,6 +44677,8 @@ function createPtyManager() {
43718
44677
  }
43719
44678
  isAlive = true;
43720
44679
  process2.onData((data) => {
44680
+ appendScrollback(data);
44681
+ if (dataSink) dataSink(data);
43721
44682
  for (const cb of dataCallbacks) {
43722
44683
  cb(data);
43723
44684
  }
@@ -43726,6 +44687,7 @@ function createPtyManager() {
43726
44687
  log.info({ exitCode: exitCode3, signal, pid: process2?.pid }, "PTY exited");
43727
44688
  isAlive = false;
43728
44689
  clearKillTimer();
44690
+ if (exitSink) exitSink(exitCode3, signal);
43729
44691
  for (const cb of exitCallbacks) {
43730
44692
  cb(exitCode3, signal);
43731
44693
  }
@@ -43769,10 +44731,33 @@ function createPtyManager() {
43769
44731
  }
43770
44732
  }, KILL_TIMEOUT_MS);
43771
44733
  }
44734
+ function appendScrollback(data) {
44735
+ const byteLen = Buffer.byteLength(data);
44736
+ scrollbackChunks.push(data);
44737
+ scrollbackBytes += byteLen;
44738
+ while (scrollbackBytes > SCROLLBACK_MAX_BYTES && scrollbackChunks.length > 1) {
44739
+ const removed = scrollbackChunks.shift();
44740
+ if (!removed) break;
44741
+ scrollbackBytes -= Buffer.byteLength(removed);
44742
+ }
44743
+ }
44744
+ function setDataSink(callback) {
44745
+ dataSink = callback;
44746
+ }
44747
+ function setExitSink(callback) {
44748
+ exitSink = callback;
44749
+ }
44750
+ function getScrollback() {
44751
+ return scrollbackChunks.join("");
44752
+ }
43772
44753
  function dispose() {
43773
44754
  kill();
43774
44755
  dataCallbacks.length = 0;
43775
44756
  exitCallbacks.length = 0;
44757
+ dataSink = null;
44758
+ exitSink = null;
44759
+ scrollbackChunks.length = 0;
44760
+ scrollbackBytes = 0;
43776
44761
  process2 = null;
43777
44762
  isAlive = false;
43778
44763
  }
@@ -43788,6 +44773,9 @@ function createPtyManager() {
43788
44773
  resize,
43789
44774
  onData,
43790
44775
  onExit,
44776
+ setDataSink,
44777
+ setExitSink,
44778
+ getScrollback,
43791
44779
  kill,
43792
44780
  dispose
43793
44781
  };
@@ -43918,7 +44906,7 @@ function toSdkContent(blocks) {
43918
44906
  }
43919
44907
  return result;
43920
44908
  }
43921
- function extractStringField(message, key, fallback) {
44909
+ function extractStringField2(message, key, fallback) {
43922
44910
  const value = message[key];
43923
44911
  return typeof value === "string" ? value : fallback;
43924
44912
  }
@@ -44024,6 +45012,7 @@ var SessionManager = class {
44024
45012
  #reviewDoc;
44025
45013
  #onStatusChange;
44026
45014
  #onBackgroundAgent;
45015
+ #onTodoProgress;
44027
45016
  #userId;
44028
45017
  #userName;
44029
45018
  #currentModel = null;
@@ -44032,7 +45021,7 @@ var SessionManager = class {
44032
45021
  #inputController = null;
44033
45022
  #activeQuery = null;
44034
45023
  #knownSkills = /* @__PURE__ */ new Set();
44035
- constructor(taskDocs, userId, userName, onStatusChange, onBackgroundAgent) {
45024
+ constructor(taskDocs, userId, userName, onStatusChange, onBackgroundAgent, onTodoProgress) {
44036
45025
  this.#metaDoc = taskDocs.meta;
44037
45026
  this.#convDoc = taskDocs.conv;
44038
45027
  this.#reviewDoc = taskDocs.review;
@@ -44040,6 +45029,7 @@ var SessionManager = class {
44040
45029
  this.#userName = userName;
44041
45030
  this.#onStatusChange = onStatusChange;
44042
45031
  this.#onBackgroundAgent = onBackgroundAgent;
45032
+ this.#onTodoProgress = onTodoProgress;
44043
45033
  }
44044
45034
  #buildAttribution() {
44045
45035
  return {
@@ -44055,6 +45045,18 @@ var SessionManager = class {
44055
45045
  #notifyStatusChange(status) {
44056
45046
  this.#onStatusChange?.(status);
44057
45047
  }
45048
+ #notifyTodoProgress(items) {
45049
+ if (!this.#onTodoProgress) return;
45050
+ let completed = 0;
45051
+ let currentActivity = null;
45052
+ for (const item of items) {
45053
+ if (item.status === "completed") completed++;
45054
+ if (item.status === "in_progress" && !currentActivity) {
45055
+ currentActivity = item.activeForm ?? null;
45056
+ }
45057
+ }
45058
+ this.#onTodoProgress({ todoCompleted: completed, todoTotal: items.length, currentActivity });
45059
+ }
44058
45060
  /**
44059
45061
  * Extract the latest user message text from the conversation.
44060
45062
  * Walks backwards from the end to find the most recent user turn,
@@ -44206,17 +45208,19 @@ var SessionManager = class {
44206
45208
  this.#activeQuery = null;
44207
45209
  }
44208
45210
  async #fetchKnownSkills(response) {
44209
- const TIMEOUT_MS2 = 5e3;
45211
+ const TIMEOUT_MS3 = 1e4;
44210
45212
  try {
44211
45213
  const commands = await Promise.race([
44212
45214
  response.supportedCommands(),
44213
45215
  new Promise(
44214
- (_, reject) => setTimeout(() => reject(new Error("supportedCommands timed out")), TIMEOUT_MS2)
45216
+ (_, reject) => setTimeout(() => reject(new Error("supportedCommands timed out")), TIMEOUT_MS3)
44215
45217
  )
44216
45218
  ]);
44217
45219
  return new Set(commands.map((c) => c.name));
44218
45220
  } catch {
44219
- logger.warn("Failed to fetch supportedCommands, all slash-prefixed messages will be escaped");
45221
+ logger.debug(
45222
+ "Failed to fetch supportedCommands, all slash-prefixed messages will be escaped"
45223
+ );
44220
45224
  return /* @__PURE__ */ new Set();
44221
45225
  }
44222
45226
  }
@@ -44434,9 +45438,9 @@ var SessionManager = class {
44434
45438
  }
44435
45439
  #handleTaskStarted(message) {
44436
45440
  const msg = message;
44437
- const taskId = extractStringField(msg, "task_id", "");
44438
- const toolUseId = extractStringField(msg, "tool_use_id", "") || taskId;
44439
- const description = extractStringField(msg, "description", "");
45441
+ const taskId = extractStringField2(msg, "task_id", "");
45442
+ const toolUseId = extractStringField2(msg, "tool_use_id", "") || taskId;
45443
+ const description = extractStringField2(msg, "description", "");
44440
45444
  if (!toolUseId) {
44441
45445
  logger.warn("Received task_started with no tool_use_id or task_id, skipping");
44442
45446
  return;
@@ -44461,11 +45465,11 @@ var SessionManager = class {
44461
45465
  }
44462
45466
  #handleTaskNotification(message) {
44463
45467
  const msg = message;
44464
- const taskId = extractStringField(msg, "task_id", "unknown");
44465
- const toolUseId = extractStringField(msg, "tool_use_id", "") || taskId;
44466
- const msgStatus = extractStringField(msg, "status", "unknown");
44467
- const summary = extractStringField(msg, "summary", "");
44468
- const outputFile = extractStringField(msg, "output_file", "");
45468
+ const taskId = extractStringField2(msg, "task_id", "unknown");
45469
+ const toolUseId = extractStringField2(msg, "tool_use_id", "") || taskId;
45470
+ const msgStatus = extractStringField2(msg, "status", "unknown");
45471
+ const summary = extractStringField2(msg, "summary", "");
45472
+ const outputFile = extractStringField2(msg, "output_file", "");
44469
45473
  logger.info(
44470
45474
  { taskId, toolUseId, status: msgStatus, summary },
44471
45475
  "Received task_notification from subagent"
@@ -44673,6 +45677,7 @@ var SessionManager = class {
44673
45677
  { toolUseId: block2.toolUseId, count: enrichedItems.length },
44674
45678
  "Updated todoItems from TodoWrite tool call"
44675
45679
  );
45680
+ this.#notifyTodoProgress(enrichedItems);
44676
45681
  }
44677
45682
  }
44678
45683
  #handleResult(message, sessionId, agentSessionId) {
@@ -44711,6 +45716,7 @@ var SessionManager = class {
44711
45716
  }
44712
45717
  #handleProcessError(error2, sessionId, agentSessionId, idleTimedOut, abortController) {
44713
45718
  if (idleTimedOut) {
45719
+ logger.info({ sessionId, cause: "idle-timeout" }, "Session error: idle timeout");
44714
45720
  this.#markInterrupted(sessionId);
44715
45721
  return {
44716
45722
  sessionId,
@@ -44720,10 +45726,15 @@ var SessionManager = class {
44720
45726
  };
44721
45727
  }
44722
45728
  if (abortController?.signal.aborted) {
45729
+ logger.info({ sessionId, cause: "abort-signal" }, "Session error: abort signal");
44723
45730
  this.#markInterrupted(sessionId);
44724
45731
  return { sessionId, agentSessionId, status: "interrupted" };
44725
45732
  }
44726
45733
  const errorMsg = error2 instanceof Error ? error2.message : String(error2);
45734
+ logger.info(
45735
+ { sessionId, cause: "process-error", error: errorMsg },
45736
+ "Session error: process threw"
45737
+ );
44727
45738
  this.#markFailed(sessionId, errorMsg);
44728
45739
  return { sessionId, agentSessionId, status: "failed", error: errorMsg };
44729
45740
  }
@@ -44769,11 +45780,13 @@ var SessionManager = class {
44769
45780
  };
44770
45781
 
44771
45782
  // src/signaling-setup.ts
45783
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
44772
45784
  import { hostname } from "os";
45785
+ import { dirname as dirname3, join as join6 } from "path";
44773
45786
 
44774
45787
  // src/signaling.ts
44775
45788
  function createDaemonSignaling(config2) {
44776
- const agentId = nanoid();
45789
+ const agentId = config2.agentId ?? nanoid();
44777
45790
  function send(msg) {
44778
45791
  config2.connection.send(msg);
44779
45792
  }
@@ -44807,7 +45820,22 @@ function createDaemonSignaling(config2) {
44807
45820
  }
44808
45821
 
44809
45822
  // src/signaling-setup.ts
44810
- async function createSignalingHandle(env, log) {
45823
+ function getOrCreateAgentId(shipyardHome) {
45824
+ const filePath = join6(shipyardHome, "agent-id");
45825
+ try {
45826
+ const existing = readFileSync(filePath, "utf-8").trim();
45827
+ if (existing) return existing;
45828
+ } catch {
45829
+ }
45830
+ const id = crypto.randomUUID();
45831
+ try {
45832
+ mkdirSync(dirname3(filePath), { recursive: true });
45833
+ writeFileSync(filePath, id, { mode: 384 });
45834
+ } catch {
45835
+ }
45836
+ return id;
45837
+ }
45838
+ async function createSignalingHandle(env, log, shipyardHome) {
44811
45839
  if (!env.SHIPYARD_SIGNALING_URL) {
44812
45840
  return null;
44813
45841
  }
@@ -44840,11 +45868,14 @@ async function createSignalingHandle(env, log) {
44840
45868
  maxDelayMs: 3e4,
44841
45869
  backoffMultiplier: 2
44842
45870
  });
45871
+ const agentId = getOrCreateAgentId(shipyardHome);
45872
+ log.info({ agentId }, "Using persisted agent ID");
44843
45873
  const signaling = createDaemonSignaling({
44844
45874
  connection,
44845
45875
  machineId,
44846
45876
  machineName,
44847
- agentType: "daemon"
45877
+ agentType: "daemon",
45878
+ agentId
44848
45879
  });
44849
45880
  connection.onStateChange((state) => {
44850
45881
  if (state === "connected") {
@@ -44924,21 +45955,21 @@ function cleanupStaleSetupEntries(roomDoc, localMachineId, log) {
44924
45955
  }
44925
45956
 
44926
45957
  // src/worktree-command.ts
44927
- import { execFile as execFile2, spawn as spawn3 } from "child_process";
45958
+ import { execFile as execFile3, spawn as spawn3 } from "child_process";
44928
45959
  import { closeSync, openSync } from "fs";
44929
45960
  import { access, chmod, constants as constants2, copyFile, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
44930
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6 } from "path";
45961
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7 } from "path";
44931
45962
  var GIT_TIMEOUT_MS = 3e4;
44932
45963
  function isErrnoException(err) {
44933
45964
  return err instanceof Error && "code" in err;
44934
45965
  }
44935
- var MAX_BUFFER = 10 * 1024 * 1024;
45966
+ var MAX_BUFFER2 = 10 * 1024 * 1024;
44936
45967
  function runGit(args, cwd) {
44937
45968
  return new Promise((resolve4, reject) => {
44938
- execFile2(
45969
+ execFile3(
44939
45970
  "git",
44940
45971
  args,
44941
- { timeout: GIT_TIMEOUT_MS, cwd, maxBuffer: MAX_BUFFER },
45972
+ { timeout: GIT_TIMEOUT_MS, cwd, maxBuffer: MAX_BUFFER2 },
44942
45973
  (error2, stdout) => {
44943
45974
  if (error2) reject(error2);
44944
45975
  else resolve4(stdout.trim());
@@ -45017,9 +46048,9 @@ async function copyIgnoredFiles(sourceRepoPath, worktreePath) {
45017
46048
  const files = ignoredOutput.split("\n").filter((f) => f && !shouldExclude(f));
45018
46049
  for (const file of files) {
45019
46050
  try {
45020
- const destPath = join6(worktreePath, file);
45021
- await mkdir2(dirname3(destPath), { recursive: true });
45022
- await copyFile(join6(sourceRepoPath, file), destPath);
46051
+ const destPath = join7(worktreePath, file);
46052
+ await mkdir2(dirname4(destPath), { recursive: true });
46053
+ await copyFile(join7(sourceRepoPath, file), destPath);
45023
46054
  } catch (err) {
45024
46055
  const msg = err instanceof Error ? err.message : String(err);
45025
46056
  warnings.push(`Failed to copy ${file}: ${msg}`);
@@ -45029,9 +46060,9 @@ async function copyIgnoredFiles(sourceRepoPath, worktreePath) {
45029
46060
  }
45030
46061
  async function launchSetupScript(worktreePath, setupScript) {
45031
46062
  if (!setupScript) return null;
45032
- const shipyardDir = join6(worktreePath, ".shipyard");
45033
- const scriptPath = join6(shipyardDir, "worktree-setup.sh");
45034
- const logPath = join6(shipyardDir, "worktree-setup.log");
46063
+ const shipyardDir = join7(worktreePath, ".shipyard");
46064
+ const scriptPath = join7(shipyardDir, "worktree-setup.sh");
46065
+ const logPath = join7(shipyardDir, "worktree-setup.log");
45035
46066
  let fullScript;
45036
46067
  if (setupScript.startsWith("#!")) {
45037
46068
  const firstNewline = setupScript.indexOf("\n");
@@ -45063,7 +46094,7 @@ async function createWorktree(opts) {
45063
46094
  const { sourceRepoPath, branchName, baseRef, setupScript, onProgress } = opts;
45064
46095
  validateWorktreeInputs(sourceRepoPath, branchName, baseRef);
45065
46096
  const worktreeParent = `${sourceRepoPath}-wt`;
45066
- const worktreePath = join6(worktreeParent, branchName);
46097
+ const worktreePath = join7(worktreeParent, branchName);
45067
46098
  onProgress("creating-worktree", `Creating worktree at ${worktreePath}`);
45068
46099
  await mkdir2(worktreeParent, { recursive: true });
45069
46100
  await assertWorktreeNotExists(worktreePath);
@@ -45119,7 +46150,7 @@ function validateWorktreeInputs(sourceRepoPath, branchName, baseRef) {
45119
46150
  }
45120
46151
 
45121
46152
  // src/serve.ts
45122
- function assertNever2(x) {
46153
+ function assertNever3(x) {
45123
46154
  throw new Error(`Unhandled message type: ${JSON.stringify(x)}`);
45124
46155
  }
45125
46156
  var processedRequestIds = /* @__PURE__ */ new Set();
@@ -45131,10 +46162,71 @@ function scheduleEphemeralCleanup(fn, delayMs) {
45131
46162
  }, delayMs);
45132
46163
  pendingCleanupTimers.add(timer);
45133
46164
  }
45134
- var CONTROL_PREFIX = "\0\0";
46165
+ function parseFileChannelMessage(raw) {
46166
+ if (!raw.startsWith(FILE_IO_CONTROL_PREFIX)) return null;
46167
+ try {
46168
+ const msg = JSON.parse(raw.slice(FILE_IO_CONTROL_PREFIX.length));
46169
+ if (typeof msg.type !== "string" || typeof msg.requestId !== "string") {
46170
+ return null;
46171
+ }
46172
+ if (msg.type === "read") {
46173
+ if (typeof msg.path !== "string") return null;
46174
+ return {
46175
+ type: "read",
46176
+ requestId: msg.requestId,
46177
+ path: msg.path,
46178
+ envPath: typeof msg.envPath === "string" ? msg.envPath : null
46179
+ };
46180
+ }
46181
+ if (msg.type === "list-dir") {
46182
+ return {
46183
+ type: "list-dir",
46184
+ requestId: msg.requestId,
46185
+ envPath: typeof msg.envPath === "string" ? msg.envPath : null
46186
+ };
46187
+ }
46188
+ return null;
46189
+ } catch {
46190
+ return null;
46191
+ }
46192
+ }
45135
46193
  var TERMINAL_BUFFER_MAX_BYTES = 1048576;
45136
46194
  var TERMINAL_OPEN_TIMEOUT_MS = 1e4;
45137
46195
  var TERMINAL_CWD_TIMEOUT_MS = 5e3;
46196
+ var TERMINAL_MAX_PTYS = 20;
46197
+ function handleTerminalInput(event, ptyMgr, termLog) {
46198
+ const raw = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
46199
+ if (raw.startsWith(TERMINAL_CONTROL_PREFIX)) {
46200
+ try {
46201
+ const ctrl = JSON.parse(raw.slice(TERMINAL_CONTROL_PREFIX.length));
46202
+ if (ctrl.type === "resize" && typeof ctrl.cols === "number" && typeof ctrl.rows === "number") {
46203
+ ptyMgr.resize(ctrl.cols, ctrl.rows);
46204
+ }
46205
+ } catch {
46206
+ termLog.warn("Invalid control message");
46207
+ }
46208
+ } else {
46209
+ try {
46210
+ ptyMgr.write(raw);
46211
+ } catch {
46212
+ }
46213
+ }
46214
+ }
46215
+ function sweepDeadPtys(ptys, roomHandle) {
46216
+ let cleaned = 0;
46217
+ for (const [key, pty2] of ptys) {
46218
+ if (!pty2.alive) {
46219
+ pty2.dispose();
46220
+ ptys.delete(key);
46221
+ try {
46222
+ sync(roomHandle).terminalSessions.delete(key);
46223
+ } catch {
46224
+ }
46225
+ cleaned++;
46226
+ }
46227
+ }
46228
+ return cleaned;
46229
+ }
45138
46230
  var TERMINAL_STATUSES = new Set(TERMINAL_TASK_STATES);
45139
46231
  async function rehydrateTaskDocuments(roomHandle, roomDoc, repo, log) {
45140
46232
  try {
@@ -45168,7 +46260,7 @@ async function rehydrateTaskDocuments(roomHandle, roomDoc, repo, log) {
45168
46260
  review: reviewHandle
45169
46261
  };
45170
46262
  if (recoverOrphanedTask(taskDocs, createChildLogger({ mode: "rehydrate", taskId }))) {
45171
- updateTaskInIndex(roomDoc, taskId, { status: "failed", updatedAt: Date.now() });
46263
+ updateTaskInIndex(roomDoc, taskId, { status: "input-required", updatedAt: Date.now() });
45172
46264
  }
45173
46265
  log.debug({ taskId }, "Task documents rehydrated");
45174
46266
  } catch (err) {
@@ -45182,6 +46274,9 @@ async function serve(env) {
45182
46274
  logger.error("SHIPYARD_SIGNALING_URL is required for serve mode");
45183
46275
  process.exit(1);
45184
46276
  }
46277
+ if (!env.SHIPYARD_USER_ID) {
46278
+ throw new Error("SHIPYARD_USER_ID is required. Run `shipyard login` first.");
46279
+ }
45185
46280
  const log = createChildLogger({ mode: "serve" });
45186
46281
  if (env.SHIPYARD_USER_TOKEN && env.SHIPYARD_SIGNALING_URL) {
45187
46282
  const client = new SignalingClient(env.SHIPYARD_SIGNALING_URL);
@@ -45200,7 +46295,7 @@ async function serve(env) {
45200
46295
  }
45201
46296
  const lifecycle = new LifecycleManager();
45202
46297
  await lifecycle.acquirePidFile(getShipyardHome());
45203
- const handle = await createSignalingHandle(env, log);
46298
+ const handle = await createSignalingHandle(env, log, getShipyardHome());
45204
46299
  if (!handle) {
45205
46300
  logger.error("SHIPYARD_SIGNALING_URL is required for serve mode");
45206
46301
  process.exit(1);
@@ -45223,6 +46318,8 @@ async function serve(env) {
45223
46318
  });
45224
46319
  const keepAwakeManager = new KeepAwakeManager(log);
45225
46320
  const terminalPtys = /* @__PURE__ */ new Map();
46321
+ const FILE_MAX_SIZE = 2e5;
46322
+ const collabRoomManager = createCollabRoomManager();
45226
46323
  const peerManager = createPeerManager({
45227
46324
  webrtcAdapter,
45228
46325
  onAnswer(targetMachineId, answer) {
@@ -45239,22 +46336,144 @@ async function serve(env) {
45239
46336
  candidate
45240
46337
  });
45241
46338
  },
45242
- onTerminalChannel(fromMachineId, rawChannel, taskId) {
46339
+ onTerminalChannel(fromMachineId, rawChannel, taskId, terminalId) {
45243
46340
  const channel = rawChannel;
45244
- const terminalKey = `${fromMachineId}:${taskId}`;
45245
- const termLog = createChildLogger({ mode: `terminal:${fromMachineId}:${taskId}` });
46341
+ const terminalKey = `${fromMachineId}:${taskId}:${terminalId}`;
46342
+ const termLog = createChildLogger({
46343
+ mode: `terminal:${fromMachineId}:${taskId}:${terminalId}`
46344
+ });
46345
+ function publishTerminalSession(cwd, status, exitCode3) {
46346
+ try {
46347
+ sync(roomHandle).terminalSessions.set(terminalKey, {
46348
+ terminalId,
46349
+ taskId,
46350
+ machineId,
46351
+ cwd,
46352
+ status,
46353
+ exitCode: exitCode3,
46354
+ createdAt: Date.now()
46355
+ });
46356
+ } catch {
46357
+ termLog.warn("Failed to publish terminal session to ephemeral");
46358
+ }
46359
+ }
46360
+ function deleteTerminalSession() {
46361
+ try {
46362
+ sync(roomHandle).terminalSessions.delete(terminalKey);
46363
+ } catch {
46364
+ }
46365
+ }
45246
46366
  const existingPty = terminalPtys.get(terminalKey);
45247
- if (existingPty) {
45248
- termLog.info("Disposing existing PTY for reconnecting machine");
46367
+ if (existingPty?.alive) {
46368
+ termLog.info({ pid: existingPty.pid }, "Reattaching channel to existing PTY");
46369
+ const buf2 = createChannelBuffer(channel, {
46370
+ maxBytes: TERMINAL_BUFFER_MAX_BYTES,
46371
+ logPrefix: `terminal-reattach:${taskId}`
46372
+ });
46373
+ const openTimeout2 = setTimeout(() => {
46374
+ if (!buf2.isOpen) {
46375
+ termLog.warn("Reattach channel did not open within timeout");
46376
+ existingPty.setDataSink(null);
46377
+ existingPty.setExitSink(null);
46378
+ if (channel.readyState !== "closed") channel.close();
46379
+ }
46380
+ }, TERMINAL_OPEN_TIMEOUT_MS);
46381
+ const scrollbackSnapshot = existingPty.getScrollback();
46382
+ const dcAsEventTarget2 = rawChannel;
46383
+ dcAsEventTarget2.addEventListener("open", () => {
46384
+ termLog.info("Reattach channel open, replaying scrollback");
46385
+ buf2.markOpen();
46386
+ clearTimeout(openTimeout2);
46387
+ if (scrollbackSnapshot) {
46388
+ buf2.sendOrBuffer(scrollbackSnapshot);
46389
+ }
46390
+ buf2.flush();
46391
+ });
46392
+ existingPty.setDataSink((data) => buf2.sendOrBuffer(data));
46393
+ const existingCwd = (() => {
46394
+ try {
46395
+ const all = sync(roomHandle).terminalSessions.getAll();
46396
+ return all.get(terminalKey)?.cwd ?? "";
46397
+ } catch {
46398
+ return "";
46399
+ }
46400
+ })();
46401
+ existingPty.setExitSink((exitCode3, signal) => {
46402
+ termLog.info({ exitCode: exitCode3, signal }, "PTY exited (reattached)");
46403
+ publishTerminalSession(existingCwd, "exited", exitCode3);
46404
+ if (buf2.isOpen) {
46405
+ buf2.sendOrBuffer(
46406
+ TERMINAL_CONTROL_PREFIX + JSON.stringify({ type: "exited", exitCode: exitCode3, signal })
46407
+ );
46408
+ }
46409
+ });
46410
+ channel.onmessage = (event) => {
46411
+ handleTerminalInput(event, existingPty, termLog);
46412
+ };
46413
+ channel.onclose = () => {
46414
+ termLog.info("Reattached channel closed, detaching sink (PTY persists)");
46415
+ buf2.reset();
46416
+ clearTimeout(openTimeout2);
46417
+ existingPty.setDataSink(null);
46418
+ existingPty.setExitSink((exitCode3, signal) => {
46419
+ termLog.info({ exitCode: exitCode3, signal }, "PTY exited while detached (reattach)");
46420
+ publishTerminalSession(existingCwd, "exited", exitCode3);
46421
+ });
46422
+ };
46423
+ return;
46424
+ }
46425
+ if (existingPty && !existingPty.alive) {
46426
+ termLog.info("Cleaning up exited PTY for key");
45249
46427
  existingPty.dispose();
45250
46428
  terminalPtys.delete(terminalKey);
46429
+ deleteTerminalSession();
46430
+ }
46431
+ if (terminalPtys.size >= TERMINAL_MAX_PTYS) {
46432
+ sweepDeadPtys(terminalPtys, roomHandle);
46433
+ }
46434
+ if (terminalPtys.size >= TERMINAL_MAX_PTYS) {
46435
+ termLog.info(
46436
+ { limit: TERMINAL_MAX_PTYS, current: terminalPtys.size },
46437
+ "PTY limit reached, rejecting channel"
46438
+ );
46439
+ const reject = () => {
46440
+ try {
46441
+ channel.send(
46442
+ TERMINAL_CONTROL_PREFIX + JSON.stringify({ type: "error", reason: "pty_limit_reached" })
46443
+ );
46444
+ } catch {
46445
+ }
46446
+ channel.close();
46447
+ };
46448
+ if (channel.readyState === "open") {
46449
+ reject();
46450
+ } else {
46451
+ const dcTarget = rawChannel;
46452
+ const rejectTimer = setTimeout(() => {
46453
+ if (channel.readyState !== "open" && channel.readyState !== "closed") {
46454
+ channel.close();
46455
+ }
46456
+ }, TERMINAL_OPEN_TIMEOUT_MS);
46457
+ dcTarget.addEventListener(
46458
+ "open",
46459
+ () => {
46460
+ clearTimeout(rejectTimer);
46461
+ reject();
46462
+ },
46463
+ { once: true }
46464
+ );
46465
+ }
46466
+ return;
45251
46467
  }
45252
46468
  const ptyManager = createPtyManager();
45253
46469
  terminalPtys.set(terminalKey, ptyManager);
45254
46470
  let ptySpawned = false;
45255
- let channelOpen = channel.readyState === "open";
45256
- const pendingBuffer = [];
45257
- let pendingBufferBytes = 0;
46471
+ let terminalCwd = "";
46472
+ const buf = createChannelBuffer(channel, {
46473
+ maxBytes: TERMINAL_BUFFER_MAX_BYTES,
46474
+ logPrefix: `terminal-new:${taskId}`,
46475
+ onOverflow: () => disposeAndClose("Pending buffer exceeded max size")
46476
+ });
45258
46477
  const preSpawnInputBuffer = [];
45259
46478
  function disposeAndClose(reason) {
45260
46479
  termLog.warn({ reason }, "Disposing terminal");
@@ -45262,51 +46481,36 @@ async function serve(env) {
45262
46481
  clearTimeout(cwdTimeout);
45263
46482
  ptyManager.dispose();
45264
46483
  terminalPtys.delete(terminalKey);
46484
+ deleteTerminalSession();
45265
46485
  if (channel.readyState === "open") {
45266
46486
  channel.close();
45267
46487
  }
45268
46488
  }
45269
- function flushPendingBuffer() {
45270
- for (const chunk of pendingBuffer) {
45271
- try {
45272
- channel.send(chunk);
45273
- } catch {
45274
- }
45275
- }
45276
- pendingBuffer.length = 0;
45277
- pendingBufferBytes = 0;
45278
- }
45279
- function sendOrBuffer(data) {
45280
- if (channelOpen) {
45281
- try {
45282
- channel.send(data);
45283
- } catch {
45284
- }
45285
- } else {
45286
- const byteLen = Buffer.byteLength(data);
45287
- if (pendingBufferBytes + byteLen > TERMINAL_BUFFER_MAX_BYTES) {
45288
- disposeAndClose("Pending buffer exceeded max size");
45289
- return;
45290
- }
45291
- pendingBuffer.push(data);
45292
- pendingBufferBytes += byteLen;
45293
- }
45294
- }
45295
46489
  const openTimeout = setTimeout(() => {
45296
- if (!channelOpen) {
46490
+ if (!buf.isOpen) {
45297
46491
  disposeAndClose("Data channel did not open within timeout");
45298
46492
  }
45299
46493
  }, TERMINAL_OPEN_TIMEOUT_MS);
45300
46494
  const dcAsEventTarget = rawChannel;
45301
46495
  dcAsEventTarget.addEventListener("open", () => {
45302
46496
  termLog.info("Terminal data channel now open, flushing buffered output");
45303
- channelOpen = true;
46497
+ buf.markOpen();
45304
46498
  clearTimeout(openTimeout);
45305
- flushPendingBuffer();
46499
+ buf.flush();
45306
46500
  });
46501
+ function validateCwd(path) {
46502
+ try {
46503
+ if (!existsSync(path)) return null;
46504
+ if (!statSync(path).isDirectory()) return null;
46505
+ return path;
46506
+ } catch {
46507
+ return null;
46508
+ }
46509
+ }
45307
46510
  function spawnPty(cwd) {
45308
46511
  if (ptySpawned) return;
45309
46512
  ptySpawned = true;
46513
+ terminalCwd = cwd;
45310
46514
  clearTimeout(cwdTimeout);
45311
46515
  try {
45312
46516
  ptyManager.spawn({ cwd });
@@ -45316,15 +46520,16 @@ async function serve(env) {
45316
46520
  terminalPtys.delete(terminalKey);
45317
46521
  return;
45318
46522
  }
45319
- ptyManager.onData((data) => {
45320
- sendOrBuffer(data);
45321
- });
45322
- ptyManager.onExit((exitCode3, signal) => {
46523
+ publishTerminalSession(cwd, "running", null);
46524
+ ptyManager.setDataSink((data) => buf.sendOrBuffer(data));
46525
+ ptyManager.setExitSink((exitCode3, signal) => {
45323
46526
  termLog.info({ exitCode: exitCode3, signal }, "Terminal PTY exited");
45324
- if (channel.readyState === "open") {
45325
- channel.close();
46527
+ publishTerminalSession(cwd, "exited", exitCode3);
46528
+ if (buf.isOpen) {
46529
+ buf.sendOrBuffer(
46530
+ TERMINAL_CONTROL_PREFIX + JSON.stringify({ type: "exited", exitCode: exitCode3, signal })
46531
+ );
45326
46532
  }
45327
- terminalPtys.delete(terminalKey);
45328
46533
  });
45329
46534
  for (const input of preSpawnInputBuffer) {
45330
46535
  try {
@@ -45334,7 +46539,7 @@ async function serve(env) {
45334
46539
  }
45335
46540
  preSpawnInputBuffer.length = 0;
45336
46541
  termLog.info(
45337
- { cwd, pid: ptyManager.pid, channelReady: channelOpen },
46542
+ { cwd, pid: ptyManager.pid, channelReady: buf.isOpen },
45338
46543
  "Terminal PTY wired to data channel"
45339
46544
  );
45340
46545
  }
@@ -45349,7 +46554,13 @@ async function serve(env) {
45349
46554
  try {
45350
46555
  const ctrl = JSON.parse(payload);
45351
46556
  if (ctrl.type === "cwd" && typeof ctrl.path === "string") {
45352
- spawnPty(ctrl.path);
46557
+ const validCwd = validateCwd(ctrl.path);
46558
+ if (validCwd) {
46559
+ spawnPty(validCwd);
46560
+ } else {
46561
+ termLog.warn({ path: ctrl.path }, "Invalid cwd path, falling back to $HOME");
46562
+ spawnPty(process.env.HOME ?? process.cwd());
46563
+ }
45353
46564
  } else if (ctrl.type === "resize" && typeof ctrl.cols === "number" && typeof ctrl.rows === "number" && ptySpawned) {
45354
46565
  ptyManager.resize(ctrl.cols, ctrl.rows);
45355
46566
  }
@@ -45369,25 +46580,73 @@ async function serve(env) {
45369
46580
  }
45370
46581
  channel.onmessage = (event) => {
45371
46582
  const raw = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
45372
- if (raw.startsWith(CONTROL_PREFIX)) {
45373
- handleControlMessage(raw.slice(CONTROL_PREFIX.length));
46583
+ if (raw.startsWith(TERMINAL_CONTROL_PREFIX)) {
46584
+ handleControlMessage(raw.slice(TERMINAL_CONTROL_PREFIX.length));
45374
46585
  } else {
45375
46586
  handleDataInput(raw);
45376
46587
  }
45377
46588
  };
45378
46589
  channel.onclose = () => {
45379
- termLog.info("Terminal data channel closed");
45380
- channelOpen = false;
46590
+ termLog.info("Terminal data channel closed, detaching sink (PTY persists)");
46591
+ buf.reset();
45381
46592
  clearTimeout(openTimeout);
45382
46593
  clearTimeout(cwdTimeout);
45383
- ptyManager.dispose();
45384
- if (terminalPtys.get(terminalKey) === ptyManager) {
45385
- terminalPtys.delete(terminalKey);
46594
+ ptyManager.setDataSink(null);
46595
+ ptyManager.setExitSink((exitCode3, signal) => {
46596
+ termLog.info({ exitCode: exitCode3, signal }, "PTY exited while detached");
46597
+ publishTerminalSession(terminalCwd, "exited", exitCode3);
46598
+ });
46599
+ };
46600
+ },
46601
+ onFileChannel(_fromMachineId, rawChannel, _channelId) {
46602
+ const channel = rawChannel;
46603
+ const fileLog = createChildLogger({ mode: "file-channel" });
46604
+ const FILE_CHANNEL_BUFFER_MAX_BYTES = 5 * 1024 * 1024;
46605
+ const buf = createChannelBuffer(channel, {
46606
+ maxBytes: FILE_CHANNEL_BUFFER_MAX_BYTES,
46607
+ logPrefix: "file-channel"
46608
+ });
46609
+ if (channel.readyState === "open") {
46610
+ buf.markOpen();
46611
+ } else {
46612
+ const dcAsEventTarget = rawChannel;
46613
+ dcAsEventTarget.addEventListener("open", () => {
46614
+ fileLog.info("File I/O data channel now open");
46615
+ buf.markOpen();
46616
+ buf.flush();
46617
+ });
46618
+ }
46619
+ const bufferedChannel = {
46620
+ send(data) {
46621
+ buf.sendOrBuffer(data);
46622
+ }
46623
+ };
46624
+ channel.onclose = () => {
46625
+ fileLog.info("File I/O channel closed");
46626
+ };
46627
+ channel.onmessage = (event) => {
46628
+ const raw = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
46629
+ try {
46630
+ const msg = parseFileChannelMessage(raw);
46631
+ if (!msg) return;
46632
+ if (msg.type === "read") {
46633
+ handleFileRead(msg.requestId, msg.path, msg.envPath, bufferedChannel).catch((err) => {
46634
+ fileLog.warn({ err }, "Unhandled error in handleFileRead");
46635
+ });
46636
+ } else if (msg.type === "list-dir") {
46637
+ handleListDir(msg.requestId, msg.envPath, bufferedChannel).catch((err) => {
46638
+ fileLog.warn({ err }, "Unhandled error in handleListDir");
46639
+ });
46640
+ } else {
46641
+ assertNever3(msg);
46642
+ }
46643
+ } catch (err) {
46644
+ fileLog.warn({ err }, "File channel message handling failed");
45386
46645
  }
45387
46646
  };
45388
46647
  }
45389
46648
  });
45390
- const roomDocId = buildDocumentId("room", LOCAL_USER_ID, DEFAULT_EPOCH);
46649
+ const roomDocId = buildRoomDocId(env.SHIPYARD_USER_ID, DEFAULT_EPOCH);
45391
46650
  const roomHandle = repo.get(roomDocId, TaskIndexDocumentSchema, ROOM_EPHEMERAL_DECLARATIONS);
45392
46651
  function publishCapabilities(caps) {
45393
46652
  const value = {
@@ -45410,6 +46669,128 @@ async function serve(env) {
45410
46669
  sync(roomHandle).capabilities.set(machineId, value);
45411
46670
  log.info({ machineId }, "Published capabilities to room ephemeral");
45412
46671
  }
46672
+ async function handleFileRead(requestId, filePath, envPath, channel) {
46673
+ function sendResponse(payload) {
46674
+ try {
46675
+ channel.send(FILE_IO_CONTROL_PREFIX + JSON.stringify(payload));
46676
+ } catch (err) {
46677
+ log.warn(
46678
+ { err, payload: { type: payload.type, requestId: payload.requestId } },
46679
+ "Failed to send file-io response"
46680
+ );
46681
+ }
46682
+ }
46683
+ const workspaceRoot = envPath ?? capabilities.environments[0]?.path ?? process.cwd();
46684
+ const absolutePath = resolve2(workspaceRoot, filePath);
46685
+ const relativePath = relative(workspaceRoot, absolutePath);
46686
+ if (relativePath.startsWith("..") || isAbsolute3(relativePath)) {
46687
+ sendResponse({ type: "error", requestId, path: filePath, error: "Path traversal denied" });
46688
+ return;
46689
+ }
46690
+ try {
46691
+ const fileStat = await stat2(absolutePath);
46692
+ if (fileStat.size > FILE_MAX_SIZE) {
46693
+ sendResponse({
46694
+ type: "error",
46695
+ requestId,
46696
+ path: filePath,
46697
+ error: `File too large (${(fileStat.size / 1024).toFixed(0)}KB, limit 200KB)`
46698
+ });
46699
+ return;
46700
+ }
46701
+ const buffer = await readFile5(absolutePath);
46702
+ if (buffer.subarray(0, 8192).includes(0)) {
46703
+ sendResponse({
46704
+ type: "error",
46705
+ requestId,
46706
+ path: filePath,
46707
+ error: "Binary file \u2014 cannot display"
46708
+ });
46709
+ return;
46710
+ }
46711
+ sendResponse({
46712
+ type: "content",
46713
+ requestId,
46714
+ path: filePath,
46715
+ content: buffer.toString("utf-8")
46716
+ });
46717
+ } catch (err) {
46718
+ sendResponse({
46719
+ type: "error",
46720
+ requestId,
46721
+ path: filePath,
46722
+ error: `Read failed: ${err instanceof Error ? err.message : String(err)}`
46723
+ });
46724
+ }
46725
+ }
46726
+ async function handleListDir(requestId, envPath, channel) {
46727
+ function sendResponse(payload) {
46728
+ try {
46729
+ channel.send(FILE_IO_CONTROL_PREFIX + JSON.stringify(payload));
46730
+ } catch (err) {
46731
+ log.warn(
46732
+ { err, payload: { type: payload.type, requestId: payload.requestId } },
46733
+ "Failed to send file-io response"
46734
+ );
46735
+ }
46736
+ }
46737
+ const resolvedPath = envPath ?? capabilities.environments[0]?.path;
46738
+ if (!resolvedPath) {
46739
+ sendResponse({ type: "error", requestId, error: "No environment path" });
46740
+ return;
46741
+ }
46742
+ const validPaths = new Set(capabilities.environments.map((e) => e.path));
46743
+ if (!validPaths.has(resolvedPath)) {
46744
+ sendResponse({ type: "error", requestId, error: "Invalid environment path" });
46745
+ return;
46746
+ }
46747
+ try {
46748
+ const files = await listWorkspaceFiles(resolvedPath);
46749
+ sendDirContentsResponse(requestId, files, channel);
46750
+ } catch (err) {
46751
+ const msg = err instanceof Error ? err.message : String(err);
46752
+ sendResponse({ type: "error", requestId, error: msg });
46753
+ }
46754
+ }
46755
+ const MAX_CHUNK_FILES = 500;
46756
+ function sendDirContentsResponse(requestId, files, channel) {
46757
+ if (files.length <= MAX_CHUNK_FILES) {
46758
+ const serialized = JSON.stringify({ type: "dir-contents", requestId, files });
46759
+ if (serialized.length > 2e5) {
46760
+ log.debug({ requestId, size: serialized.length }, "dir-contents response exceeds 200KB");
46761
+ }
46762
+ try {
46763
+ channel.send(FILE_IO_CONTROL_PREFIX + serialized);
46764
+ } catch (err) {
46765
+ log.warn(
46766
+ { err, payload: { type: "dir-contents", requestId } },
46767
+ "Failed to send file-io response"
46768
+ );
46769
+ }
46770
+ return;
46771
+ }
46772
+ const totalChunks = Math.ceil(files.length / MAX_CHUNK_FILES);
46773
+ log.debug(
46774
+ { requestId, fileCount: files.length, totalChunks },
46775
+ "Chunking dir-contents response"
46776
+ );
46777
+ for (let i = 0; i < totalChunks; i++) {
46778
+ const chunk = files.slice(i * MAX_CHUNK_FILES, (i + 1) * MAX_CHUNK_FILES);
46779
+ const serialized = JSON.stringify({
46780
+ type: "dir-contents-chunk",
46781
+ requestId,
46782
+ files: chunk,
46783
+ chunkIndex: i,
46784
+ totalChunks
46785
+ });
46786
+ try {
46787
+ channel.send(FILE_IO_CONTROL_PREFIX + serialized);
46788
+ } catch (err) {
46789
+ log.warn({ requestId, chunkIndex: i, err }, "Failed to send dir-contents chunk");
46790
+ break;
46791
+ }
46792
+ }
46793
+ }
45413
46794
  publishCapabilities(capabilities);
45414
46795
  const branchWatcher = createBranchWatcher({
45415
46796
  environments: capabilities.environments,
@@ -45431,6 +46812,7 @@ async function serve(env) {
45431
46812
  keepAwakeManager.update(keepAwakeEnabled, activeTasks.size > 0);
45432
46813
  }
45433
46814
  });
46815
+ keepAwakeManager.update(keepAwakeEnabled, activeTasks.size > 0);
45434
46816
  sync(typedRoomHandle).enhancePromptReqs.subscribe(({ key: requestId, value, source }) => {
45435
46817
  if (source !== "remote") return;
45436
46818
  if (!value) return;
@@ -45588,7 +46970,10 @@ async function serve(env) {
45588
46970
  });
45589
46971
  });
45590
46972
  connection.onStateChange((state) => {
45591
- log.info({ state }, "Connection state changed");
46973
+ log.info(
46974
+ { state, activeTaskCount: activeTasks.size, activeTasks: [...activeTasks.keys()] },
46975
+ "Signaling connection state changed"
46976
+ );
45592
46977
  });
45593
46978
  const dispatchingTasks = /* @__PURE__ */ new Set();
45594
46979
  connection.onMessage((msg) => {
@@ -45612,10 +46997,19 @@ async function serve(env) {
45612
46997
  publishCapabilities,
45613
46998
  branchWatcher,
45614
46999
  keepAwakeManager,
45615
- getKeepAwakeEnabled: () => keepAwakeEnabled
47000
+ getKeepAwakeEnabled: () => keepAwakeEnabled,
47001
+ collabRoomManager,
47002
+ webrtcAdapter
45616
47003
  });
45617
47004
  });
45618
47005
  connection.connect();
47006
+ const DEAD_PTY_SWEEP_MS = 3e4;
47007
+ const deadPtySweep = setInterval(() => {
47008
+ const cleaned = sweepDeadPtys(terminalPtys, roomHandle);
47009
+ if (cleaned > 0) {
47010
+ log.info({ cleaned, remaining: terminalPtys.size }, "Dead PTY sweep completed");
47011
+ }
47012
+ }, DEAD_PTY_SWEEP_MS);
45619
47013
  log.info("Daemon running in serve mode, waiting for tasks...");
45620
47014
  lifecycle.onShutdown(async () => {
45621
47015
  log.info("Shutting down serve mode...");
@@ -45628,14 +47022,8 @@ async function serve(env) {
45628
47022
  }
45629
47023
  activeTasks.clear();
45630
47024
  dispatchingTasks.clear();
45631
- for (const timer of diffDebounceTimers.values()) {
45632
- clearTimeout(timer);
45633
- }
45634
- diffDebounceTimers.clear();
45635
- for (const timer of branchDiffTimers.values()) {
45636
- clearTimeout(timer);
45637
- }
45638
- branchDiffTimers.clear();
47025
+ clearAllTimers(diffDebounceTimers, branchDiffTimers, prPollTimers);
47026
+ prPollLastActivity.clear();
45639
47027
  for (const unsub of watchedTasks.values()) {
45640
47028
  unsub();
45641
47029
  }
@@ -45644,10 +47032,16 @@ async function serve(env) {
45644
47032
  lastProcessedConvLen.clear();
45645
47033
  for (const timer of pendingCleanupTimers) clearTimeout(timer);
45646
47034
  pendingCleanupTimers.clear();
47035
+ clearInterval(deadPtySweep);
45647
47036
  for (const [id, ptyMgr] of terminalPtys) {
45648
47037
  ptyMgr.dispose();
45649
- terminalPtys.delete(id);
47038
+ try {
47039
+ sync(roomHandle).terminalSessions.delete(id);
47040
+ } catch {
47041
+ }
45650
47042
  }
47043
+ terminalPtys.clear();
47044
+ collabRoomManager.destroy();
45651
47045
  peerManager.destroy();
45652
47046
  signaling.unregister();
45653
47047
  await new Promise((resolve4) => setTimeout(resolve4, 200));
@@ -45660,8 +47054,12 @@ async function serve(env) {
45660
47054
  }
45661
47055
  var DIFF_DEBOUNCE_MS = 2e3;
45662
47056
  var BRANCH_DIFF_DEBOUNCE_MS = 1e4;
47057
+ var PR_POLL_INTERVAL_MS = 3e4;
47058
+ var PR_POLL_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
45663
47059
  var diffDebounceTimers = /* @__PURE__ */ new Map();
45664
47060
  var branchDiffTimers = /* @__PURE__ */ new Map();
47061
+ var prPollTimers = /* @__PURE__ */ new Map();
47062
+ var prPollLastActivity = /* @__PURE__ */ new Map();
45665
47063
  function debouncedDiffCapture(taskId, cwd, taskHandle, log) {
45666
47064
  const existing = diffDebounceTimers.get(taskId);
45667
47065
  if (existing) clearTimeout(existing);
@@ -45732,6 +47130,100 @@ async function captureBranchDiffState(cwd, taskHandle, log) {
45732
47130
  });
45733
47131
  log.debug({ baseBranch, fileCount: branchFiles.length }, "Branch diff state captured");
45734
47132
  }
47133
+ async function capturePRState(cwd, taskHandle, log) {
47134
+ const available = await isGhAvailable();
47135
+ change(taskHandle.conv, (draft) => {
47136
+ draft.diffState.prAvailable.set(available);
47137
+ });
47138
+ if (!available) return;
47139
+ const [currentPR, assignedReviews, userPRs] = await Promise.allSettled([
47140
+ getPRForCurrentBranch(cwd),
47141
+ getAssignedReviews(cwd),
47142
+ getUserPRs(cwd)
47143
+ ]);
47144
+ const allPRs = [];
47145
+ const seen = /* @__PURE__ */ new Set();
47146
+ const currentBranchPR = currentPR.status === "fulfilled" ? currentPR.value : null;
47147
+ if (currentBranchPR) {
47148
+ allPRs.push(currentBranchPR);
47149
+ seen.add(currentBranchPR.number);
47150
+ }
47151
+ for (const pr of assignedReviews.status === "fulfilled" ? assignedReviews.value : []) {
47152
+ if (!seen.has(pr.number)) {
47153
+ allPRs.push(pr);
47154
+ seen.add(pr.number);
47155
+ }
47156
+ }
47157
+ for (const pr of userPRs.status === "fulfilled" ? userPRs.value : []) {
47158
+ if (!seen.has(pr.number)) {
47159
+ allPRs.push(pr);
47160
+ seen.add(pr.number);
47161
+ }
47162
+ }
47163
+ let prDiff = "";
47164
+ let prFiles = [];
47165
+ if (currentBranchPR) {
47166
+ [prDiff, prFiles] = await Promise.all([
47167
+ getPRDiff(cwd, currentBranchPR.number),
47168
+ getPRFiles(cwd, currentBranchPR.number)
47169
+ ]);
47170
+ }
47171
+ change(taskHandle.conv, (draft) => {
47172
+ draft.diffState.currentBranchPRNumber.set(currentBranchPR?.number ?? 0);
47173
+ draft.diffState.prData.set(allPRs);
47174
+ draft.diffState.prDiff.set(prDiff);
47175
+ const fileList = draft.diffState.prFiles;
47176
+ if (fileList.length > 0) {
47177
+ fileList.delete(0, fileList.length);
47178
+ }
47179
+ for (const file of prFiles) {
47180
+ fileList.push(file);
47181
+ }
47182
+ draft.diffState.prUpdatedAt.set(Date.now());
47183
+ });
47184
+ log.debug({ prCount: allPRs.length, hasBranchPR: !!currentBranchPR }, "PR state captured");
47185
+ }
47186
+ function getLatestCwd(taskHandle) {
47187
+ const convJson = taskHandle.conv.toJSON();
47188
+ const lastUserMsg = [...convJson.conversation].reverse().find((m) => m.role === "user");
47189
+ return lastUserMsg?.cwd ?? process.cwd();
47190
+ }
47191
+ function refreshPRPoll(taskId, taskHandle, taskLog) {
47192
+ const cwd = getLatestCwd(taskHandle);
47193
+ prPollLastActivity.set(taskId, Date.now());
47194
+ capturePRState(cwd, taskHandle, taskLog).catch((err) => {
47195
+ taskLog.warn({ err }, "PR refresh on re-notify failed");
47196
+ });
47197
+ if (!prPollTimers.has(taskId)) {
47198
+ const convHandle = taskHandle.conv;
47199
+ const initialCwd = cwd;
47200
+ prPollTimers.set(
47201
+ taskId,
47202
+ setInterval(() => {
47203
+ try {
47204
+ const lastActivity = prPollLastActivity.get(taskId) ?? 0;
47205
+ if (Date.now() - lastActivity > PR_POLL_IDLE_TIMEOUT_MS) {
47206
+ const timer = prPollTimers.get(taskId);
47207
+ if (timer) clearInterval(timer);
47208
+ prPollTimers.delete(taskId);
47209
+ prPollLastActivity.delete(taskId);
47210
+ taskLog.info("PR poll stopped after idle timeout");
47211
+ return;
47212
+ }
47213
+ const convJson = convHandle.toJSON();
47214
+ const latestUserMsg = [...convJson.conversation].reverse().find((m) => m.role === "user");
47215
+ const pollCwd = latestUserMsg?.cwd ?? initialCwd;
47216
+ capturePRState(pollCwd, taskHandle, taskLog).catch((err) => {
47217
+ taskLog.warn({ err }, "PR poll failed");
47218
+ });
47219
+ } catch (err) {
47220
+ taskLog.warn({ err }, "PR poll tick failed");
47221
+ }
47222
+ }, PR_POLL_INTERVAL_MS)
47223
+ );
47224
+ taskLog.info("PR poll timer restarted after idle");
47225
+ }
47226
+ }
45735
47227
  async function captureTurnDiff(cwd, turnStartRef, taskHandle, log) {
45736
47228
  const turnEndRef = await captureTreeSnapshot(cwd);
45737
47229
  if (!turnStartRef || !turnEndRef) {
@@ -45817,21 +47309,55 @@ function handleMessage(msg, ctx) {
45817
47309
  ctx.log.debug({ type: msg.type }, "Worktree create echo");
45818
47310
  break;
45819
47311
  case "cancel-task":
47312
+ ctx.log.info(
47313
+ { taskId: msg.taskId, requestId: msg.requestId, machineId: msg.machineId },
47314
+ "Received cancel-task from browser"
47315
+ );
45820
47316
  handleCancelTask(msg, ctx);
45821
47317
  break;
45822
47318
  case "control-ack":
45823
47319
  ctx.log.debug({ type: msg.type }, "Control ack echo");
45824
47320
  break;
45825
47321
  case "authenticated":
47322
+ ctx.log.info({ type: msg.type }, "Server notification: authenticated");
47323
+ break;
45826
47324
  case "agent-joined":
45827
47325
  case "agent-left":
45828
47326
  case "agent-status-changed":
47327
+ ctx.log.info({ type: msg.type }, "Server notification: agent change");
47328
+ break;
45829
47329
  case "error":
45830
- ctx.log.debug({ type: msg.type }, "Server notification");
47330
+ ctx.log.warn({ type: msg.type, code: msg.code, message: msg.message }, "Server error");
47331
+ break;
47332
+ case "notify-collab-room":
47333
+ handleNotifyCollabRoom(msg, ctx);
45831
47334
  break;
45832
47335
  default:
45833
- assertNever2(msg);
47336
+ assertNever3(msg);
47337
+ }
47338
+ }
47339
+ function handleNotifyCollabRoom(msg, ctx) {
47340
+ const collabLog = createChildLogger({ mode: `collab:${msg.roomId}`, taskId: msg.taskId });
47341
+ if (!ctx.env.SHIPYARD_SIGNALING_URL) {
47342
+ collabLog.warn("No signaling URL configured, cannot join collab room");
47343
+ return;
45834
47344
  }
47345
+ if (!ctx.env.SHIPYARD_USER_TOKEN) {
47346
+ collabLog.warn("No user token configured, cannot join collab room");
47347
+ return;
47348
+ }
47349
+ collabLog.info("Joining collab room");
47350
+ ctx.collabRoomManager.join({
47351
+ roomId: msg.roomId,
47352
+ taskId: msg.taskId,
47353
+ token: msg.token,
47354
+ expiresAt: msg.expiresAt,
47355
+ signalingBaseUrl: ctx.env.SHIPYARD_SIGNALING_URL,
47356
+ userToken: ctx.env.SHIPYARD_USER_TOKEN,
47357
+ machineId: ctx.machineId,
47358
+ webrtcAdapter: ctx.webrtcAdapter,
47359
+ log: collabLog
47360
+ });
45835
47361
  }
45836
47362
  function handleNotifyTask(msg, ctx) {
45837
47363
  const { taskId, requestId } = msg;
@@ -45858,6 +47384,7 @@ function handleNotifyTask(msg, ctx) {
45858
47384
  const taskHandle = ctx.taskHandles.get(taskId);
45859
47385
  if (taskHandle) {
45860
47386
  onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
47387
+ refreshPRPoll(taskId, taskHandle, taskLog);
45861
47388
  }
45862
47389
  return;
45863
47390
  }
@@ -45889,7 +47416,7 @@ function handleCancelTask(msg, ctx) {
45889
47416
  });
45890
47417
  return;
45891
47418
  }
45892
- taskLog.info("Canceling active task");
47419
+ taskLog.info({ requestId, machineId: msg.machineId }, "Canceling active task");
45893
47420
  activeTask.abortController.abort();
45894
47421
  ctx.connection.send({
45895
47422
  type: "control-ack",
@@ -46384,7 +47911,7 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
46384
47911
  review: reviewHandle
46385
47912
  };
46386
47913
  if (recoverOrphanedTask(taskDocs, taskLog)) {
46387
- updateTaskInIndex(ctx.roomDoc, taskId, { status: "failed", updatedAt: Date.now() });
47914
+ updateTaskInIndex(ctx.roomDoc, taskId, { status: "input-required", updatedAt: Date.now() });
46388
47915
  }
46389
47916
  const convJson = convHandle.toJSON();
46390
47917
  const lastUserMsg = [...convJson.conversation].reverse().find((m) => m.role === "user");
@@ -46392,6 +47919,36 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
46392
47919
  captureBranchDiffState(initialCwd, taskHandle, taskLog).catch((err) => {
46393
47920
  taskLog.warn({ err }, "Failed to capture initial branch diff");
46394
47921
  });
47922
+ capturePRState(initialCwd, taskHandle, taskLog).catch((err) => {
47923
+ taskLog.warn({ err }, "Initial PR capture failed");
47924
+ });
47925
+ const existingPrTimer = prPollTimers.get(taskId);
47926
+ if (existingPrTimer) clearInterval(existingPrTimer);
47927
+ prPollLastActivity.set(taskId, Date.now());
47928
+ prPollTimers.set(
47929
+ taskId,
47930
+ setInterval(() => {
47931
+ try {
47932
+ const lastActivity = prPollLastActivity.get(taskId) ?? 0;
47933
+ if (Date.now() - lastActivity > PR_POLL_IDLE_TIMEOUT_MS) {
47934
+ const timer = prPollTimers.get(taskId);
47935
+ if (timer) clearInterval(timer);
47936
+ prPollTimers.delete(taskId);
47937
+ prPollLastActivity.delete(taskId);
47938
+ taskLog.info("PR poll stopped after idle timeout");
47939
+ return;
47940
+ }
47941
+ const convJson2 = convHandle.toJSON();
47942
+ const latestUserMsg = [...convJson2.conversation].reverse().find((m) => m.role === "user");
47943
+ const cwd = latestUserMsg?.cwd ?? initialCwd;
47944
+ capturePRState(cwd, taskHandle, taskLog).catch((err) => {
47945
+ taskLog.warn({ err }, "PR poll failed");
47946
+ });
47947
+ } catch (err) {
47948
+ taskLog.warn({ err }, "PR poll tick failed");
47949
+ }
47950
+ }, PR_POLL_INTERVAL_MS)
47951
+ );
46395
47952
  const opCountBefore = loro(convHandle).opCount();
46396
47953
  taskLog.info({ convDocId, opCount: opCountBefore }, "Doc state before subscribe");
46397
47954
  const unsubscribe = subscribe(convHandle, (event) => {
@@ -46582,7 +48139,17 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
46582
48139
  };
46583
48140
  const userId = ctx.env.SHIPYARD_USER_ID ?? null;
46584
48141
  const userName = ctx.env.SHIPYARD_USER_DISPLAY_NAME ?? null;
46585
- const manager = new SessionManager(taskDocs, userId, userName, onStatusChange, onBackgroundAgent);
48142
+ const onTodoProgress = (progress) => {
48143
+ updateTaskInIndex(ctx.roomDoc, taskId, { ...progress, updatedAt: Date.now() });
48144
+ };
48145
+ const manager = new SessionManager(
48146
+ taskDocs,
48147
+ userId,
48148
+ userName,
48149
+ onStatusChange,
48150
+ onBackgroundAgent,
48151
+ onTodoProgress
48152
+ );
46586
48153
  const activeTask = {
46587
48154
  taskId,
46588
48155
  abortController,
@@ -46644,6 +48211,7 @@ async function cleanupTaskRun(opts) {
46644
48211
  abortController.abort();
46645
48212
  clearDebouncedTimer(diffDebounceTimers, taskId);
46646
48213
  clearDebouncedTimer(branchDiffTimers, taskId);
48214
+ prPollLastActivity.set(taskId, Date.now());
46647
48215
  try {
46648
48216
  await captureDiffState(cwd, taskHandle, taskLog);
46649
48217
  } catch (err) {
@@ -46654,6 +48222,11 @@ async function cleanupTaskRun(opts) {
46654
48222
  } catch (err) {
46655
48223
  taskLog.warn({ err }, "Failed to capture final branch diff state");
46656
48224
  }
48225
+ try {
48226
+ await capturePRState(cwd, taskHandle, taskLog);
48227
+ } catch (err) {
48228
+ taskLog.warn({ err }, "Failed to capture final PR state");
48229
+ }
46657
48230
  try {
46658
48231
  const turnStartRef = await turnStartRefPromise;
46659
48232
  await captureTurnDiff(cwd, turnStartRef, taskHandle, taskLog);
@@ -46681,6 +48254,14 @@ function clearDebouncedTimer(timers, taskId) {
46681
48254
  timers.delete(taskId);
46682
48255
  }
46683
48256
  }
48257
+ function clearAllTimers(...timerMaps) {
48258
+ for (const timers of timerMaps) {
48259
+ for (const timer of timers.values()) {
48260
+ clearTimeout(timer);
48261
+ }
48262
+ timers.clear();
48263
+ }
48264
+ }
46684
48265
  function mapPermissionMode(mode) {
46685
48266
  switch (mode) {
46686
48267
  case "accept-edits":
@@ -47011,6 +48592,16 @@ async function runTask(opts) {
47011
48592
  }
47012
48593
 
47013
48594
  // src/index.ts
48595
+ function getVersion() {
48596
+ try {
48597
+ const thisFile = fileURLToPath(import.meta.url);
48598
+ const pkgPath = resolve3(thisFile, "../../package.json");
48599
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
48600
+ return pkg.version ?? "unknown";
48601
+ } catch {
48602
+ return "unknown";
48603
+ }
48604
+ }
47014
48605
  function parseCliArgs() {
47015
48606
  const { values } = parseArgs({
47016
48607
  options: {
@@ -47021,23 +48612,29 @@ function parseCliArgs() {
47021
48612
  cwd: { type: "string" },
47022
48613
  model: { type: "string", short: "m" },
47023
48614
  serve: { type: "boolean", short: "s" },
48615
+ version: { type: "boolean", short: "v" },
47024
48616
  help: { type: "boolean", short: "h" }
47025
48617
  },
47026
48618
  strict: true
47027
48619
  });
48620
+ if (values.version) {
48621
+ process.stdout.write(`${getVersion()}
48622
+ `);
48623
+ process.exit(0);
48624
+ }
47028
48625
  if (values.help) {
47029
48626
  logger.info(
47030
48627
  [
47031
- "shipyard-daemon - Claude Agent SDK + Loro CRDT sync",
48628
+ `shipyard v${getVersion()} - Agent management hub for human-agent collaboration`,
47032
48629
  "",
47033
48630
  "Usage:",
47034
48631
  " shipyard login Authenticate with Shipyard",
47035
48632
  " shipyard login --check Check current auth status",
47036
48633
  " shipyard logout Clear stored credentials",
47037
48634
  "",
47038
- ' shipyard-daemon --prompt "Fix the bug in auth.ts" [options]',
47039
- ' shipyard-daemon --resume <session-id> --task-id <id> [--prompt "Continue"]',
47040
- " shipyard-daemon --serve",
48635
+ ' shipyard --prompt "Fix the bug in auth.ts" [options]',
48636
+ ' shipyard --resume <session-id> --task-id <id> [--prompt "Continue"]',
48637
+ " shipyard --serve",
47041
48638
  "",
47042
48639
  "Options:",
47043
48640
  " -p, --prompt <text> Prompt for the agent",
@@ -47047,6 +48644,7 @@ function parseCliArgs() {
47047
48644
  " --cwd <path> Working directory for agent",
47048
48645
  " -m, --model <name> Model to use",
47049
48646
  " -s, --serve Run in serve mode (signaling + spawn-agent)",
48647
+ " -v, --version Show version",
47050
48648
  " -h, --help Show this help",
47051
48649
  "",
47052
48650
  "Authentication:",
@@ -47077,8 +48675,8 @@ function parseCliArgs() {
47077
48675
  serve: values.serve
47078
48676
  };
47079
48677
  }
47080
- async function setupSignaling(env, log) {
47081
- const handle = await createSignalingHandle(env, log);
48678
+ async function setupSignaling(env, log, shipyardHome) {
48679
+ const handle = await createSignalingHandle(env, log, shipyardHome);
47082
48680
  if (handle) {
47083
48681
  handle.connection.connect();
47084
48682
  }
@@ -47171,13 +48769,13 @@ function handleResult(log, result, startTime) {
47171
48769
  async function handleSubcommand() {
47172
48770
  const subcommand = process.argv[2];
47173
48771
  if (subcommand === "login") {
47174
- const { loginCommand } = await import("./login-5RRT37UW.js");
48772
+ const { loginCommand } = await import("./login-BS6C2BRS.js");
47175
48773
  const hasCheck = process.argv.includes("--check");
47176
48774
  await loginCommand({ check: hasCheck });
47177
48775
  return true;
47178
48776
  }
47179
48777
  if (subcommand === "logout") {
47180
- const { logoutCommand } = await import("./logout-NWYPFICH.js");
48778
+ const { logoutCommand } = await import("./logout-XX5ULFHB.js");
47181
48779
  await logoutCommand();
47182
48780
  return true;
47183
48781
  }
@@ -47241,7 +48839,7 @@ async function main() {
47241
48839
  const repo = await setupRepo(dataDir);
47242
48840
  const lifecycle = new LifecycleManager();
47243
48841
  await lifecycle.acquirePidFile(getShipyardHome());
47244
- const signalingHandle = await setupSignaling(env, log);
48842
+ const signalingHandle = await setupSignaling(env, log, getShipyardHome());
47245
48843
  const cleanup = createCleanup(signalingHandle, lifecycle, repo);
47246
48844
  lifecycle.onShutdown(async () => {
47247
48845
  log.info("Cleaning up...");