@schoolai/shipyard-mcp 0.3.2-next.523 → 0.3.2-next.527

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.
@@ -1,7 +1,19 @@
1
1
  import {
2
2
  __commonJS,
3
- __toESM
4
- } from "./chunk-JSBRDJBE.js";
3
+ __toESM,
4
+ assertNever,
5
+ getSessionIdByPlanId,
6
+ getSessionState,
7
+ getSessionStateByPlanId,
8
+ isSessionStateApproved,
9
+ isSessionStateApprovedAwaitingToken,
10
+ isSessionStateReviewed,
11
+ isSessionStateSynced,
12
+ loadEnv,
13
+ logger,
14
+ setSessionState,
15
+ startPeriodicCleanup
16
+ } from "./chunk-HFZCBGQ3.js";
5
17
 
6
18
  // ../../node_modules/.pnpm/lz-string@1.5.0/node_modules/lz-string/libs/lz-string.js
7
19
  var require_lz_string = __commonJS({
@@ -306,7 +318,7 @@ var require_lz_string = __commonJS({
306
318
  return compressed.charCodeAt(index);
307
319
  });
308
320
  },
309
- _decompress: function(length, resetValue, getNextValue) {
321
+ _decompress: function(length2, resetValue, getNextValue) {
310
322
  var dictionary = [], next, enlargeIn = 4, dictSize = 4, numBits = 3, entry = "", result = [], i, w, bits, resb, maxpower, power, c, data = { val: getNextValue(0), position: resetValue, index: 1 };
311
323
  for (i = 0; i < 3; i += 1) {
312
324
  dictionary[i] = i;
@@ -364,7 +376,7 @@ var require_lz_string = __commonJS({
364
376
  w = c;
365
377
  result.push(c);
366
378
  while (true) {
367
- if (data.index > length) {
379
+ if (data.index > length2) {
368
380
  return "";
369
381
  }
370
382
  bits = 0;
@@ -470,12 +482,6 @@ var PlanStatusValues = [
470
482
  "in_progress",
471
483
  "completed"
472
484
  ];
473
- var PlanViewTabValues = [
474
- "plan",
475
- "activity",
476
- "deliverables",
477
- "changes"
478
- ];
479
485
  var OriginPlatformValues = [
480
486
  "claude-code",
481
487
  "devin",
@@ -532,38 +538,6 @@ var ConversationVersionSchema = z.discriminatedUnion("handedOff", [ConversationV
532
538
  handedOffAt: z.number(),
533
539
  handedOffTo: z.string()
534
540
  })]);
535
- var PlanEventTypes = [
536
- "plan_created",
537
- "status_changed",
538
- "comment_added",
539
- "comment_resolved",
540
- "artifact_uploaded",
541
- "deliverable_linked",
542
- "pr_linked",
543
- "content_edited",
544
- "approved",
545
- "changes_requested",
546
- "completed",
547
- "conversation_imported",
548
- "conversation_handed_off",
549
- "step_completed",
550
- "plan_archived",
551
- "plan_unarchived",
552
- "conversation_exported",
553
- "plan_shared",
554
- "approval_requested",
555
- "input_request_created",
556
- "input_request_answered",
557
- "input_request_declined",
558
- "agent_activity",
559
- "session_token_regenerated"
560
- ];
561
- var AgentActivityTypes = [
562
- "help_request",
563
- "help_request_resolved",
564
- "blocker",
565
- "blocker_resolved"
566
- ];
567
541
  var PlanEventBaseSchema = z.object({
568
542
  id: z.string(),
569
543
  actor: z.string(),
@@ -728,13 +702,6 @@ var PlanEventSchema = z.discriminatedUnion("type", [
728
702
  }),
729
703
  PlanEventBaseSchema.extend({ type: z.literal("session_token_regenerated") })
730
704
  ]);
731
- function isInboxWorthy(event, username, ownerId) {
732
- if (!event.inboxWorthy) return false;
733
- if (!event.inboxFor) return true;
734
- const resolvedInboxFor = event.inboxFor === "owner" && ownerId ? ownerId : event.inboxFor;
735
- if (Array.isArray(resolvedInboxFor)) return resolvedInboxFor.includes(username);
736
- return resolvedInboxFor === username;
737
- }
738
705
  var PlanMetadataBaseSchema = z.object({
739
706
  id: z.string(),
740
707
  title: z.string(),
@@ -800,9 +767,6 @@ var LocalArtifactSchema = BaseArtifactSchema.extend({
800
767
  localArtifactId: z.string()
801
768
  });
802
769
  var ArtifactSchema = z.discriminatedUnion("storage", [GitHubArtifactSchema, LocalArtifactSchema]);
803
- function getArtifactUrl(repo, pr, planId, filename) {
804
- return `https://raw.githubusercontent.com/${repo}/plan-artifacts/pr-${pr}/${planId}/${filename}`;
805
- }
806
770
  var DeliverableSchema = z.object({
807
771
  id: z.string(),
808
772
  text: z.string(),
@@ -879,15 +843,6 @@ var GitHubArtifactParseSchema = z.object({
879
843
  storage: z.literal("github"),
880
844
  url: z.string()
881
845
  });
882
- function createGitHubArtifact(params) {
883
- const artifact = {
884
- id: nanoid(),
885
- ...params,
886
- storage: "github",
887
- uploadedAt: params.uploadedAt ?? Date.now()
888
- };
889
- return GitHubArtifactParseSchema.parse(artifact);
890
- }
891
846
  var LocalArtifactParseSchema = z.object({
892
847
  id: z.string(),
893
848
  type: z.enum([
@@ -901,15 +856,6 @@ var LocalArtifactParseSchema = z.object({
901
856
  storage: z.literal("local"),
902
857
  localArtifactId: z.string()
903
858
  });
904
- function createLocalArtifact(params) {
905
- const artifact = {
906
- id: nanoid(),
907
- ...params,
908
- storage: "local",
909
- uploadedAt: params.uploadedAt ?? Date.now()
910
- };
911
- return LocalArtifactParseSchema.parse(artifact);
912
- }
913
859
  function createInitialConversationVersion(params) {
914
860
  const version = {
915
861
  ...params,
@@ -917,19 +863,12 @@ function createInitialConversationVersion(params) {
917
863
  };
918
864
  return ConversationVersionSchema.parse(version);
919
865
  }
920
- function createHandedOffConversationVersion(params) {
921
- const version = {
922
- ...params,
923
- handedOff: true
924
- };
925
- return ConversationVersionSchema.parse(version);
926
- }
927
866
 
928
867
  // ../../packages/schema/dist/yjs-helpers-DzEyLz-f.mjs
929
868
  import { z as z2 } from "zod";
930
869
  import { nanoid as nanoid2 } from "nanoid";
931
870
  import * as Y from "yjs";
932
- function assertNever(value) {
871
+ function assertNever2(value) {
933
872
  throw new Error(`Unhandled discriminated union member: ${JSON.stringify(value)}`);
934
873
  }
935
874
  var AgentPresenceSchema = z2.object({
@@ -1028,17 +967,6 @@ var CreateSubscriptionRequestSchema = z2.object({
1028
967
  threshold: z2.number().positive().optional()
1029
968
  });
1030
969
  var CreateSubscriptionResponseSchema = z2.object({ clientId: z2.string() });
1031
- var DEFAULT_INPUT_REQUEST_TIMEOUT_SECONDS = 1800;
1032
- var InputRequestTypeValues = [
1033
- "text",
1034
- "multiline",
1035
- "choice",
1036
- "confirm",
1037
- "number",
1038
- "email",
1039
- "date",
1040
- "rating"
1041
- ];
1042
970
  var InputRequestStatusValues = [
1043
971
  "pending",
1044
972
  "answered",
@@ -1206,17 +1134,6 @@ function createInputRequest(params) {
1206
1134
  if (!parseResult.success) throw new Error(`Invalid input request: ${parseResult.error.issues[0]?.message}`);
1207
1135
  return parseResult.data;
1208
1136
  }
1209
- function normalizeChoiceOptions(options) {
1210
- return options.map((opt) => typeof opt === "string" ? {
1211
- value: opt,
1212
- label: opt
1213
- } : {
1214
- ...opt,
1215
- value: opt.value,
1216
- label: opt.label || opt.value
1217
- });
1218
- }
1219
- var CHOICE_DROPDOWN_THRESHOLD = 9;
1220
1137
  var MAX_QUESTIONS_PER_REQUEST = 10;
1221
1138
  var QuestionBaseSchema = z2.object({
1222
1139
  message: z2.string().min(1, "Message cannot be empty"),
@@ -1369,9 +1286,6 @@ var YDOC_KEYS = {
1369
1286
  LOCAL_DIFF_COMMENTS: "localDiffComments"
1370
1287
  };
1371
1288
  var validKeys = new Set(Object.values(YDOC_KEYS));
1372
- function isValidYDocKey(key) {
1373
- return validKeys.has(key);
1374
- }
1375
1289
  var CommentBodySchema = z2.union([z2.string(), z2.array(z2.unknown())]);
1376
1290
  var ThreadCommentSchema = z2.object({
1377
1291
  id: z2.string(),
@@ -1385,9 +1299,6 @@ var ThreadSchema = z2.object({
1385
1299
  resolved: z2.boolean().optional(),
1386
1300
  selectedText: z2.string().optional()
1387
1301
  });
1388
- function isThread(value) {
1389
- return ThreadSchema.safeParse(value).success;
1390
- }
1391
1302
  function parseThreads(data) {
1392
1303
  const threads = [];
1393
1304
  for (const [_key, value] of Object.entries(data)) {
@@ -1421,20 +1332,6 @@ function extractMentions(body) {
1421
1332
  function toUnknownArray(array) {
1422
1333
  return array.toJSON();
1423
1334
  }
1424
- function findInputRequestById(data, requestId) {
1425
- for (let i = 0; i < data.length; i++) {
1426
- const item = data[i];
1427
- if (item && typeof item === "object" && "id" in item && item.id === requestId) {
1428
- const parsed = AnyInputRequestSchema.safeParse(item);
1429
- if (parsed.success) return {
1430
- rawIndex: i,
1431
- request: parsed.data
1432
- };
1433
- return null;
1434
- }
1435
- }
1436
- return null;
1437
- }
1438
1335
  var VALID_STATUS_TRANSITIONS = {
1439
1336
  draft: [
1440
1337
  "pending_review",
@@ -1539,7 +1436,7 @@ function applyStatusTransitionFields(map, transition) {
1539
1436
  applyCompletedTransition(map, transition);
1540
1437
  break;
1541
1438
  default:
1542
- assertNever(transition);
1439
+ assertNever2(transition);
1543
1440
  }
1544
1441
  }
1545
1442
  function resetPlanToDraft(ydoc, actor) {
@@ -1607,20 +1504,6 @@ function initPlanMetadata(ydoc, init) {
1607
1504
  const result = getPlanMetadataWithValidation(ydoc);
1608
1505
  if (!result.success) throw new Error(`Failed to initialize metadata: ${result.error}`);
1609
1506
  }
1610
- function getStepCompletions(ydoc) {
1611
- const steps = ydoc.getMap("stepCompletions");
1612
- return new Map(steps.entries());
1613
- }
1614
- function toggleStepCompletion(ydoc, stepId, actor) {
1615
- ydoc.transact(() => {
1616
- const steps = ydoc.getMap("stepCompletions");
1617
- const current = steps.get(stepId) || false;
1618
- steps.set(stepId, !current);
1619
- }, actor ? { actor } : void 0);
1620
- }
1621
- function isStepCompleted(ydoc, stepId) {
1622
- return ydoc.getMap("stepCompletions").get(stepId) || false;
1623
- }
1624
1507
  function getArtifacts(ydoc) {
1625
1508
  return toUnknownArray(ydoc.getArray(YDOC_KEYS.ARTIFACTS)).map((item) => {
1626
1509
  if (!item || typeof item !== "object") return null;
@@ -1639,40 +1522,12 @@ function addArtifact(ydoc, artifact, actor) {
1639
1522
  ydoc.getArray(YDOC_KEYS.ARTIFACTS).push([validated]);
1640
1523
  }, actor ? { actor } : void 0);
1641
1524
  }
1642
- function removeArtifact(ydoc, artifactId) {
1643
- const array = ydoc.getArray(YDOC_KEYS.ARTIFACTS);
1644
- const index = toUnknownArray(array).map((item) => ArtifactSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((a) => a.id === artifactId);
1645
- if (index === -1) return false;
1646
- array.delete(index, 1);
1647
- return true;
1648
- }
1649
- function getAgentPresences(ydoc) {
1650
- const map = ydoc.getMap(YDOC_KEYS.PRESENCE);
1651
- const result = /* @__PURE__ */ new Map();
1652
- for (const [sessionId, value] of map.entries()) {
1653
- const parsed = AgentPresenceSchema.safeParse(value);
1654
- if (parsed.success) result.set(sessionId, parsed.data);
1655
- }
1656
- return result;
1657
- }
1658
1525
  function setAgentPresence(ydoc, presence, actor) {
1659
1526
  const validated = AgentPresenceSchema.parse(presence);
1660
1527
  ydoc.transact(() => {
1661
1528
  ydoc.getMap(YDOC_KEYS.PRESENCE).set(validated.sessionId, validated);
1662
1529
  }, actor ? { actor } : void 0);
1663
1530
  }
1664
- function clearAgentPresence(ydoc, sessionId) {
1665
- const map = ydoc.getMap(YDOC_KEYS.PRESENCE);
1666
- if (!map.has(sessionId)) return false;
1667
- map.delete(sessionId);
1668
- return true;
1669
- }
1670
- function getAgentPresence(ydoc, sessionId) {
1671
- const value = ydoc.getMap(YDOC_KEYS.PRESENCE).get(sessionId);
1672
- if (!value) return null;
1673
- const parsed = AgentPresenceSchema.safeParse(value);
1674
- return parsed.success ? parsed.data : null;
1675
- }
1676
1531
  function getDeliverables(ydoc) {
1677
1532
  return toUnknownArray(ydoc.getArray(YDOC_KEYS.DELIVERABLES)).map((item) => DeliverableSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
1678
1533
  }
@@ -1701,75 +1556,6 @@ function linkArtifactToDeliverable(ydoc, deliverableId, artifactId, actor) {
1701
1556
  }, actor ? { actor } : void 0);
1702
1557
  return true;
1703
1558
  }
1704
- function getPlanOwnerId(ydoc) {
1705
- const ownerId = ydoc.getMap(YDOC_KEYS.METADATA).get("ownerId");
1706
- return typeof ownerId === "string" ? ownerId : null;
1707
- }
1708
- function isApprovalRequired(ydoc) {
1709
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
1710
- const approvalRequired = map.get("approvalRequired");
1711
- if (typeof approvalRequired === "boolean") return approvalRequired;
1712
- const ownerId = map.get("ownerId");
1713
- return typeof ownerId === "string" && ownerId.length > 0;
1714
- }
1715
- function getApprovedUsers(ydoc) {
1716
- const approvedUsers = ydoc.getMap(YDOC_KEYS.METADATA).get("approvedUsers");
1717
- if (!Array.isArray(approvedUsers)) return [];
1718
- return approvedUsers.filter((id) => typeof id === "string");
1719
- }
1720
- function isUserApproved(ydoc, userId) {
1721
- if (getPlanOwnerId(ydoc) === userId) return true;
1722
- return getApprovedUsers(ydoc).includes(userId);
1723
- }
1724
- function approveUser(ydoc, userId, actor) {
1725
- const currentApproved = getApprovedUsers(ydoc);
1726
- if (currentApproved.includes(userId)) return;
1727
- ydoc.transact(() => {
1728
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
1729
- map.set("approvedUsers", [...currentApproved, userId]);
1730
- map.set("updatedAt", Date.now());
1731
- }, actor ? { actor } : void 0);
1732
- }
1733
- function revokeUser(ydoc, userId, actor) {
1734
- if (userId === getPlanOwnerId(ydoc)) return false;
1735
- const currentApproved = getApprovedUsers(ydoc);
1736
- if (currentApproved.indexOf(userId) === -1) return false;
1737
- ydoc.transact(() => {
1738
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
1739
- map.set("approvedUsers", currentApproved.filter((id) => id !== userId));
1740
- map.set("updatedAt", Date.now());
1741
- }, actor ? { actor } : void 0);
1742
- return true;
1743
- }
1744
- function getRejectedUsers(ydoc) {
1745
- const rejectedUsers = ydoc.getMap(YDOC_KEYS.METADATA).get("rejectedUsers");
1746
- if (!Array.isArray(rejectedUsers)) return [];
1747
- return rejectedUsers.filter((id) => typeof id === "string");
1748
- }
1749
- function isUserRejected(ydoc, userId) {
1750
- return getRejectedUsers(ydoc).includes(userId);
1751
- }
1752
- function rejectUser(ydoc, userId, actor) {
1753
- if (userId === getPlanOwnerId(ydoc)) return;
1754
- const currentRejected = getRejectedUsers(ydoc);
1755
- const currentApproved = getApprovedUsers(ydoc);
1756
- ydoc.transact(() => {
1757
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
1758
- if (!currentRejected.includes(userId)) map.set("rejectedUsers", [...currentRejected, userId]);
1759
- if (currentApproved.includes(userId)) map.set("approvedUsers", currentApproved.filter((id) => id !== userId));
1760
- map.set("updatedAt", Date.now());
1761
- }, actor ? { actor } : void 0);
1762
- }
1763
- function unrejectUser(ydoc, userId, actor) {
1764
- const currentRejected = getRejectedUsers(ydoc);
1765
- if (currentRejected.indexOf(userId) === -1) return false;
1766
- ydoc.transact(() => {
1767
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
1768
- map.set("rejectedUsers", currentRejected.filter((id) => id !== userId));
1769
- map.set("updatedAt", Date.now());
1770
- }, actor ? { actor } : void 0);
1771
- return true;
1772
- }
1773
1559
  function getLinkedPRs(ydoc) {
1774
1560
  return toUnknownArray(ydoc.getArray(YDOC_KEYS.LINKED_PRS)).map((item) => LinkedPRSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
1775
1561
  }
@@ -1782,126 +1568,12 @@ function linkPR(ydoc, pr, actor) {
1782
1568
  array.push([validated]);
1783
1569
  }, actor ? { actor } : void 0);
1784
1570
  }
1785
- function unlinkPR(ydoc, prNumber) {
1786
- const array = ydoc.getArray(YDOC_KEYS.LINKED_PRS);
1787
- const index = toUnknownArray(array).map((item) => LinkedPRSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((p) => p.prNumber === prNumber);
1788
- if (index === -1) return false;
1789
- array.delete(index, 1);
1790
- return true;
1791
- }
1792
- function getLinkedPR(ydoc, prNumber) {
1793
- return getLinkedPRs(ydoc).find((pr) => pr.prNumber === prNumber) ?? null;
1794
- }
1795
- function updateLinkedPRStatus(ydoc, prNumber, status) {
1796
- const array = ydoc.getArray(YDOC_KEYS.LINKED_PRS);
1797
- const existing = toUnknownArray(array).map((item) => LinkedPRSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data);
1798
- const index = existing.findIndex((p) => p.prNumber === prNumber);
1799
- if (index === -1) return false;
1800
- const pr = existing[index];
1801
- if (!pr) return false;
1802
- array.delete(index, 1);
1803
- array.insert(index, [{
1804
- ...pr,
1805
- status
1806
- }]);
1807
- return true;
1808
- }
1809
1571
  function getPRReviewComments(ydoc) {
1810
1572
  return toUnknownArray(ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS)).map((item) => PRReviewCommentSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
1811
1573
  }
1812
- function getPRReviewCommentsForPR(ydoc, prNumber) {
1813
- return getPRReviewComments(ydoc).filter((c) => c.prNumber === prNumber);
1814
- }
1815
- function addPRReviewComment(ydoc, comment, actor) {
1816
- const validated = PRReviewCommentSchema.parse(comment);
1817
- ydoc.transact(() => {
1818
- ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS).push([validated]);
1819
- }, actor ? { actor } : void 0);
1820
- }
1821
- function resolvePRReviewComment(ydoc, commentId, resolved) {
1822
- const array = ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS);
1823
- const existing = toUnknownArray(array).map((item) => PRReviewCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data);
1824
- const index = existing.findIndex((c) => c.id === commentId);
1825
- if (index === -1) return false;
1826
- const comment = existing[index];
1827
- if (!comment) return false;
1828
- array.delete(index, 1);
1829
- array.insert(index, [{
1830
- ...comment,
1831
- resolved
1832
- }]);
1833
- return true;
1834
- }
1835
- function removePRReviewComment(ydoc, commentId) {
1836
- const array = ydoc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS);
1837
- const index = toUnknownArray(array).map((item) => PRReviewCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((c) => c.id === commentId);
1838
- if (index === -1) return false;
1839
- array.delete(index, 1);
1840
- return true;
1841
- }
1842
1574
  function getLocalDiffComments(ydoc) {
1843
1575
  return toUnknownArray(ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS)).map((item) => LocalDiffCommentSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
1844
1576
  }
1845
- function getLocalDiffCommentsForFile(ydoc, path) {
1846
- return getLocalDiffComments(ydoc).filter((c) => c.path === path);
1847
- }
1848
- function addLocalDiffComment(ydoc, comment, actor) {
1849
- const validated = LocalDiffCommentSchema.parse(comment);
1850
- ydoc.transact(() => {
1851
- ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS).push([validated]);
1852
- }, actor ? { actor } : void 0);
1853
- }
1854
- function resolveLocalDiffComment(ydoc, commentId, resolved) {
1855
- const array = ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS);
1856
- const existing = toUnknownArray(array).map((item) => LocalDiffCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data);
1857
- const index = existing.findIndex((c) => c.id === commentId);
1858
- if (index === -1) return false;
1859
- const comment = existing[index];
1860
- if (!comment) return false;
1861
- array.delete(index, 1);
1862
- array.insert(index, [{
1863
- ...comment,
1864
- resolved
1865
- }]);
1866
- return true;
1867
- }
1868
- function removeLocalDiffComment(ydoc, commentId) {
1869
- const array = ydoc.getArray(YDOC_KEYS.LOCAL_DIFF_COMMENTS);
1870
- const index = toUnknownArray(array).map((item) => LocalDiffCommentSchema.safeParse(item)).filter((r) => r.success).map((r) => r.data).findIndex((c) => c.id === commentId);
1871
- if (index === -1) return false;
1872
- array.delete(index, 1);
1873
- return true;
1874
- }
1875
- function extractViewedByFromCrdt(existingViewedBy) {
1876
- const viewedBy = {};
1877
- if (existingViewedBy instanceof Y.Map) {
1878
- for (const [key, value] of existingViewedBy.entries()) if (typeof key === "string" && typeof value === "number") viewedBy[key] = value;
1879
- } else if (existingViewedBy && typeof existingViewedBy === "object" && !Array.isArray(existingViewedBy)) {
1880
- for (const [key, value] of Object.entries(existingViewedBy)) if (typeof value === "number") viewedBy[key] = value;
1881
- }
1882
- return viewedBy;
1883
- }
1884
- function markPlanAsViewed(ydoc, username) {
1885
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
1886
- ydoc.transact(() => {
1887
- const viewedBy = extractViewedByFromCrdt(map.get("viewedBy"));
1888
- viewedBy[username] = Date.now();
1889
- const viewedByMap = new Y.Map();
1890
- for (const [user, timestamp] of Object.entries(viewedBy)) viewedByMap.set(user, timestamp);
1891
- map.set("viewedBy", viewedByMap);
1892
- });
1893
- }
1894
- function getViewedBy(ydoc) {
1895
- return extractViewedByFromCrdt(ydoc.getMap(YDOC_KEYS.METADATA).get("viewedBy"));
1896
- }
1897
- function isPlanUnread(metadata, username, viewedBy) {
1898
- const lastViewed = (viewedBy ?? {})[username];
1899
- if (!lastViewed) return true;
1900
- return lastViewed < metadata.updatedAt;
1901
- }
1902
- function getConversationVersions(ydoc) {
1903
- return getPlanMetadata(ydoc)?.conversationVersions || [];
1904
- }
1905
1577
  function addConversationVersion(ydoc, version, actor) {
1906
1578
  const validated = ConversationVersionSchema.parse(version);
1907
1579
  ydoc.transact(() => {
@@ -1912,21 +1584,6 @@ function addConversationVersion(ydoc, version, actor) {
1912
1584
  metadata.set("conversationVersions", [...versions, validated]);
1913
1585
  }, actor ? { actor } : void 0);
1914
1586
  }
1915
- function markVersionHandedOff(ydoc, versionId, handedOffTo, actor) {
1916
- const updated = getConversationVersions(ydoc).map((v) => {
1917
- if (v.versionId !== versionId) return v;
1918
- const handedOffVersion = {
1919
- ...v,
1920
- handedOff: true,
1921
- handedOffAt: Date.now(),
1922
- handedOffTo
1923
- };
1924
- return ConversationVersionSchema.parse(handedOffVersion);
1925
- });
1926
- ydoc.transact(() => {
1927
- ydoc.getMap(YDOC_KEYS.METADATA).set("conversationVersions", updated);
1928
- }, actor ? { actor } : void 0);
1929
- }
1930
1587
  function logPlanEvent(ydoc, type, actor, ...args) {
1931
1588
  const eventsArray = ydoc.getArray(YDOC_KEYS.EVENTS);
1932
1589
  const [data, options] = args;
@@ -1981,229 +1638,6 @@ function createPlanSnapshot(ydoc, reason, actor, status, blocks) {
1981
1638
  deliverables: deliverables.length > 0 ? deliverables : void 0
1982
1639
  };
1983
1640
  }
1984
- function getLatestSnapshot(ydoc) {
1985
- const snapshots = getSnapshots(ydoc);
1986
- if (snapshots.length === 0) return null;
1987
- return snapshots[snapshots.length - 1] ?? null;
1988
- }
1989
- function getValidatedTags(rawTags) {
1990
- if (!Array.isArray(rawTags)) return [];
1991
- return rawTags.filter((t2) => typeof t2 === "string");
1992
- }
1993
- function addPlanTag(ydoc, tag, actor) {
1994
- ydoc.transact(() => {
1995
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
1996
- const currentTags = getValidatedTags(map.get("tags"));
1997
- const normalizedTag = tag.toLowerCase().trim();
1998
- if (!normalizedTag || currentTags.includes(normalizedTag)) return;
1999
- map.set("tags", [...currentTags, normalizedTag]);
2000
- map.set("updatedAt", Date.now());
2001
- }, actor ? { actor } : void 0);
2002
- }
2003
- function removePlanTag(ydoc, tag, actor) {
2004
- ydoc.transact(() => {
2005
- const map = ydoc.getMap(YDOC_KEYS.METADATA);
2006
- const currentTags = getValidatedTags(map.get("tags"));
2007
- const normalizedTag = tag.toLowerCase().trim();
2008
- map.set("tags", currentTags.filter((t2) => t2 !== normalizedTag));
2009
- map.set("updatedAt", Date.now());
2010
- }, actor ? { actor } : void 0);
2011
- }
2012
- function getAllTagsFromIndex(indexEntries) {
2013
- const tagSet = /* @__PURE__ */ new Set();
2014
- for (const entry of indexEntries) if (entry.tags) for (const tag of entry.tags) tagSet.add(tag);
2015
- return Array.from(tagSet).sort();
2016
- }
2017
- function archivePlan(ydoc, actorId) {
2018
- const metadata = getPlanMetadata(ydoc);
2019
- if (!metadata) return {
2020
- success: false,
2021
- error: "Plan metadata not found"
2022
- };
2023
- if (metadata.archivedAt) return {
2024
- success: false,
2025
- error: "Plan is already archived"
2026
- };
2027
- ydoc.transact(() => {
2028
- const metadataMap = ydoc.getMap(YDOC_KEYS.METADATA);
2029
- metadataMap.set("archivedAt", Date.now());
2030
- metadataMap.set("archivedBy", actorId);
2031
- metadataMap.set("updatedAt", Date.now());
2032
- }, { actor: actorId });
2033
- return { success: true };
2034
- }
2035
- function unarchivePlan(ydoc, actorId) {
2036
- const metadata = getPlanMetadata(ydoc);
2037
- if (!metadata) return {
2038
- success: false,
2039
- error: "Plan metadata not found"
2040
- };
2041
- if (!metadata.archivedAt) return {
2042
- success: false,
2043
- error: "Plan is not archived"
2044
- };
2045
- ydoc.transact(() => {
2046
- const metadataMap = ydoc.getMap(YDOC_KEYS.METADATA);
2047
- metadataMap.delete("archivedAt");
2048
- metadataMap.delete("archivedBy");
2049
- metadataMap.set("updatedAt", Date.now());
2050
- }, { actor: actorId });
2051
- return { success: true };
2052
- }
2053
- function answerInputRequest(ydoc, requestId, response, answeredBy) {
2054
- const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
2055
- const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
2056
- if (!found) return {
2057
- success: false,
2058
- error: "Request not found"
2059
- };
2060
- const { rawIndex: index, request } = found;
2061
- if (request.status !== "pending") switch (request.status) {
2062
- case "answered":
2063
- return {
2064
- success: false,
2065
- error: "Request already answered",
2066
- answeredBy: request.answeredBy
2067
- };
2068
- case "declined":
2069
- return {
2070
- success: false,
2071
- error: "Request was declined"
2072
- };
2073
- case "cancelled":
2074
- return {
2075
- success: false,
2076
- error: "Request was cancelled"
2077
- };
2078
- default:
2079
- return {
2080
- success: false,
2081
- error: `Request is not pending`
2082
- };
2083
- }
2084
- const answeredRequest = {
2085
- ...request,
2086
- status: "answered",
2087
- response,
2088
- answeredAt: Date.now(),
2089
- answeredBy
2090
- };
2091
- const validated = InputRequestSchema.parse(answeredRequest);
2092
- ydoc.transact(() => {
2093
- requestsArray.delete(index, 1);
2094
- requestsArray.insert(index, [validated]);
2095
- logPlanEvent(ydoc, "input_request_answered", answeredBy, {
2096
- requestId,
2097
- response,
2098
- answeredBy,
2099
- requestMessage: "message" in request ? request.message : void 0,
2100
- requestType: request.type
2101
- });
2102
- });
2103
- return { success: true };
2104
- }
2105
- function answerMultiQuestionInputRequest(ydoc, requestId, responses, answeredBy) {
2106
- const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
2107
- const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
2108
- if (!found) return {
2109
- success: false,
2110
- error: "Request not found"
2111
- };
2112
- const { rawIndex: index, request } = found;
2113
- if (request.type !== "multi") return {
2114
- success: false,
2115
- error: "Request is not pending"
2116
- };
2117
- if (request.status !== "pending") switch (request.status) {
2118
- case "answered":
2119
- return {
2120
- success: false,
2121
- error: "Request already answered",
2122
- answeredBy: request.answeredBy
2123
- };
2124
- case "declined":
2125
- return {
2126
- success: false,
2127
- error: "Request was declined"
2128
- };
2129
- case "cancelled":
2130
- return {
2131
- success: false,
2132
- error: "Request was cancelled"
2133
- };
2134
- default:
2135
- return {
2136
- success: false,
2137
- error: `Request is not pending`
2138
- };
2139
- }
2140
- const answeredRequest = {
2141
- ...request,
2142
- status: "answered",
2143
- responses,
2144
- answeredAt: Date.now(),
2145
- answeredBy
2146
- };
2147
- const validated = MultiQuestionInputRequestSchema.parse(answeredRequest);
2148
- ydoc.transact(() => {
2149
- requestsArray.delete(index, 1);
2150
- requestsArray.insert(index, [validated]);
2151
- logPlanEvent(ydoc, "input_request_answered", answeredBy, {
2152
- requestId,
2153
- response: responses,
2154
- answeredBy,
2155
- requestType: "multi"
2156
- });
2157
- });
2158
- return { success: true };
2159
- }
2160
- function cancelInputRequest(ydoc, requestId) {
2161
- const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
2162
- const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
2163
- if (!found) return {
2164
- success: false,
2165
- error: "Request not found"
2166
- };
2167
- const { rawIndex: index, request } = found;
2168
- if (request.status !== "pending") return {
2169
- success: false,
2170
- error: `Request is not pending`
2171
- };
2172
- const cancelledRequest = {
2173
- ...request,
2174
- status: "cancelled"
2175
- };
2176
- const validated = request.type === "multi" ? MultiQuestionInputRequestSchema.parse(cancelledRequest) : InputRequestSchema.parse(cancelledRequest);
2177
- ydoc.transact(() => {
2178
- requestsArray.delete(index, 1);
2179
- requestsArray.insert(index, [validated]);
2180
- });
2181
- return { success: true };
2182
- }
2183
- function declineInputRequest(ydoc, requestId) {
2184
- const requestsArray = ydoc.getArray(YDOC_KEYS.INPUT_REQUESTS);
2185
- const found = findInputRequestById(toUnknownArray(requestsArray), requestId);
2186
- if (!found) return {
2187
- success: false,
2188
- error: "Request not found"
2189
- };
2190
- const { rawIndex: index, request } = found;
2191
- if (request.status !== "pending") return {
2192
- success: false,
2193
- error: `Request is not pending`
2194
- };
2195
- const declinedRequest = {
2196
- ...request,
2197
- status: "declined"
2198
- };
2199
- const validated = request.type === "multi" ? MultiQuestionInputRequestSchema.parse(declinedRequest) : InputRequestSchema.parse(declinedRequest);
2200
- ydoc.transact(() => {
2201
- requestsArray.delete(index, 1);
2202
- requestsArray.insert(index, [validated]);
2203
- logPlanEvent(ydoc, "input_request_declined", "User", { requestId });
2204
- });
2205
- return { success: true };
2206
- }
2207
1641
  function atomicRegenerateTokenIfOwner(ydoc, expectedOwnerId, newTokenHash, actor) {
2208
1642
  let result = {
2209
1643
  success: false,
@@ -2272,29 +1706,10 @@ var UrlEncodedPlanV2Schema = z3.object({
2272
1706
  keyVersions: z3.array(UrlKeyVersionSchema).optional()
2273
1707
  });
2274
1708
  var UrlEncodedPlanSchema = z3.discriminatedUnion("v", [UrlEncodedPlanV1Schema, UrlEncodedPlanV2Schema]);
2275
- function isUrlEncodedPlanV1(plan) {
2276
- return plan.v === 1;
2277
- }
2278
- function isUrlEncodedPlanV2(plan) {
2279
- return plan.v === 2;
2280
- }
2281
1709
  function encodePlan(plan) {
2282
1710
  const json = JSON.stringify(plan);
2283
1711
  return import_lz_string.default.compressToEncodedURIComponent(json);
2284
1712
  }
2285
- function decodePlan(encoded) {
2286
- try {
2287
- const json = import_lz_string.default.decompressFromEncodedURIComponent(encoded);
2288
- if (!json) return null;
2289
- const parsed = JSON.parse(json);
2290
- const result = UrlEncodedPlanSchema.safeParse(parsed);
2291
- if (!result.success)
2292
- return null;
2293
- return result.data;
2294
- } catch (_error) {
2295
- return null;
2296
- }
2297
- }
2298
1713
  function createPlanUrl(baseUrl, plan) {
2299
1714
  const encoded = encodePlan(plan);
2300
1715
  const url = new URL(baseUrl);
@@ -2334,22 +1749,6 @@ function createPlanUrlWithHistory(baseUrl, plan, snapshots) {
2334
1749
  keyVersions: keyVersions.length > 0 ? keyVersions : void 0
2335
1750
  });
2336
1751
  }
2337
- function getLocationSearch() {
2338
- if (typeof globalThis === "undefined") return null;
2339
- if (!("location" in globalThis)) return null;
2340
- const location = Object.fromEntries(Object.entries(globalThis)).location;
2341
- if (typeof location !== "object" || location === null) return null;
2342
- if (!("search" in location)) return null;
2343
- const search = Object.fromEntries(Object.entries(location)).search;
2344
- return typeof search === "string" ? search : null;
2345
- }
2346
- function getPlanFromUrl() {
2347
- const search = getLocationSearch();
2348
- if (!search) return null;
2349
- const encoded = new URLSearchParams(search).get("d");
2350
- if (!encoded) return null;
2351
- return decodePlan(encoded);
2352
- }
2353
1752
 
2354
1753
  // ../../packages/schema/dist/index.mjs
2355
1754
  import { z as z4 } from "zod";
@@ -2560,88 +1959,6 @@ var ClaudeCodeMessageSchema = z4.object({
2560
1959
  costUSD: z4.number().optional(),
2561
1960
  durationMs: z4.number().optional()
2562
1961
  });
2563
- function parseClaudeCodeTranscriptString(content) {
2564
- const lines = content.split("\n").filter((line) => line.trim());
2565
- const messages = [];
2566
- const errors = [];
2567
- for (let i = 0; i < lines.length; i++) {
2568
- const line = lines[i];
2569
- if (!line) continue;
2570
- try {
2571
- const parsed = JSON.parse(line);
2572
- const result = ClaudeCodeMessageSchema.safeParse(parsed);
2573
- if (result.success) messages.push(result.data);
2574
- else errors.push({
2575
- line: i + 1,
2576
- error: `Validation failed: ${result.error.message}`
2577
- });
2578
- } catch (err) {
2579
- const errorMessage = err instanceof Error ? err.message : String(err);
2580
- errors.push({
2581
- line: i + 1,
2582
- error: `JSON parse error: ${errorMessage}`
2583
- });
2584
- }
2585
- }
2586
- return {
2587
- messages,
2588
- errors
2589
- };
2590
- }
2591
- function assertNever$1(x) {
2592
- throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
2593
- }
2594
- function convertContentBlock(block) {
2595
- switch (block.type) {
2596
- case "text":
2597
- return [{
2598
- type: "text",
2599
- text: block.text
2600
- }];
2601
- case "tool_use":
2602
- return [{
2603
- type: "data",
2604
- data: { toolUse: {
2605
- name: block.name,
2606
- id: block.id,
2607
- input: block.input
2608
- } }
2609
- }];
2610
- case "tool_result":
2611
- return [{
2612
- type: "data",
2613
- data: { toolResult: {
2614
- toolUseId: block.tool_use_id,
2615
- content: block.content,
2616
- isError: block.is_error ?? false
2617
- } }
2618
- }];
2619
- default:
2620
- return assertNever$1(block);
2621
- }
2622
- }
2623
- function convertMessage(msg, contextId) {
2624
- const role = msg.message.role === "user" ? "user" : "agent";
2625
- const parts = msg.message.content.flatMap((block) => convertContentBlock(block));
2626
- return {
2627
- messageId: msg.uuid,
2628
- role,
2629
- parts,
2630
- contextId,
2631
- metadata: {
2632
- timestamp: msg.timestamp,
2633
- platform: "claude-code",
2634
- parentMessageId: msg.parentUuid,
2635
- model: msg.message.model,
2636
- usage: msg.message.usage,
2637
- costUSD: msg.costUSD,
2638
- durationMs: msg.durationMs
2639
- }
2640
- };
2641
- }
2642
- function claudeCodeToA2A(messages, contextId) {
2643
- return messages.filter((msg) => msg.type !== "summary").map((msg) => convertMessage(msg, contextId));
2644
- }
2645
1962
  function validateA2AMessages(messages) {
2646
1963
  const valid = [];
2647
1964
  const errors = [];
@@ -2658,39 +1975,6 @@ function validateA2AMessages(messages) {
2658
1975
  errors
2659
1976
  };
2660
1977
  }
2661
- function getFirstTextPart(parts) {
2662
- return parts.filter((p) => p.type === "text")[0];
2663
- }
2664
- function extractTitleFromMessage(msg) {
2665
- if (!msg) return "Imported Conversation";
2666
- const firstPart = getFirstTextPart(msg.parts);
2667
- if (!firstPart) return "Imported Conversation";
2668
- const text = firstPart.text;
2669
- return text.length > 50 ? `${text.slice(0, 50)}...` : text;
2670
- }
2671
- function isToolDataPart(part) {
2672
- const data = part.data;
2673
- return Boolean(data && typeof data === "object" && ("toolUse" in data || "toolResult" in data));
2674
- }
2675
- function countToolInteractions(parts) {
2676
- return parts.filter((p) => p.type === "data").filter(isToolDataPart).length;
2677
- }
2678
- function summarizeMessage(msg) {
2679
- const prefix = msg.role === "user" ? "User" : "Agent";
2680
- const firstTextPart = getFirstTextPart(msg.parts);
2681
- if (firstTextPart) return `${prefix}: ${firstTextPart.text.slice(0, 100)}${firstTextPart.text.length > 100 ? "..." : ""}`;
2682
- const toolCount = countToolInteractions(msg.parts);
2683
- if (toolCount > 0) return `${prefix}: [${toolCount} tool interaction(s)]`;
2684
- }
2685
- function summarizeA2AConversation(messages, maxMessages = 3) {
2686
- const title = extractTitleFromMessage(messages.find((m) => m.role === "user"));
2687
- const summaryLines = messages.slice(0, maxMessages).map(summarizeMessage).filter((line) => typeof line === "string");
2688
- if (messages.length > maxMessages) summaryLines.push(`... and ${messages.length - maxMessages} more messages`);
2689
- return {
2690
- title,
2691
- text: summaryLines.join("\n")
2692
- };
2693
- }
2694
1978
  function isToolUseData(data) {
2695
1979
  if (!data || typeof data !== "object") return false;
2696
1980
  const d = toRecord(data);
@@ -2862,26 +2146,6 @@ function computeCommentStaleness(comment, currentHeadSha, currentLineContent) {
2862
2146
  isStale: false
2863
2147
  };
2864
2148
  }
2865
- function isLineContentStale(comment, currentLineContent) {
2866
- if (!comment.lineContentHash || currentLineContent === void 0) return false;
2867
- const currentHash = hashLineContent(currentLineContent);
2868
- return comment.lineContentHash !== currentHash;
2869
- }
2870
- function withStalenessInfo(comment, currentHeadSha, currentLineContent) {
2871
- const staleness = computeCommentStaleness(comment, currentHeadSha, currentLineContent);
2872
- return {
2873
- ...comment,
2874
- isStale: staleness.isStale,
2875
- stalenessType: staleness.type
2876
- };
2877
- }
2878
- function withStalenessInfoBatch(comments, currentHeadSha, lineContentMap) {
2879
- return comments.map((comment) => {
2880
- const key = `${comment.path}:${comment.line}`;
2881
- const currentLineContent = lineContentMap?.get(key);
2882
- return withStalenessInfo(comment, currentHeadSha, currentLineContent);
2883
- });
2884
- }
2885
2149
  function isDiffHeader(line) {
2886
2150
  return line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("---") || line.startsWith("+++");
2887
2151
  }
@@ -2996,26 +2260,6 @@ function formatDiffCommentsForLLM(comments, options = {}) {
2996
2260
  (${resolvedCount} resolved comment(s) not shown)`;
2997
2261
  return output;
2998
2262
  }
2999
- function formatPRCommentsForLLM(comments, options = {}) {
3000
- return formatDiffCommentsForLLM(comments, options);
3001
- }
3002
- function getPRCommentsSummary(comments) {
3003
- const byFile = /* @__PURE__ */ new Map();
3004
- let unresolved = 0;
3005
- let resolved = 0;
3006
- for (const comment of comments) {
3007
- if (comment.resolved) resolved++;
3008
- else unresolved++;
3009
- const count = byFile.get(comment.path) ?? 0;
3010
- byFile.set(comment.path, count + 1);
3011
- }
3012
- return {
3013
- total: comments.length,
3014
- unresolved,
3015
- resolved,
3016
- byFile
3017
- };
3018
- }
3019
2263
  var EnvironmentContextSchema = z4.object({
3020
2264
  projectName: z4.string().optional(),
3021
2265
  branch: z4.string().optional(),
@@ -3031,18 +2275,6 @@ var GitHubPRResponseSchema = z4.object({
3031
2275
  merged: z4.boolean(),
3032
2276
  head: z4.object({ ref: z4.string() })
3033
2277
  });
3034
- function asPlanId(id) {
3035
- return id;
3036
- }
3037
- function asAwarenessClientId(id) {
3038
- return id;
3039
- }
3040
- function asWebRTCPeerId(id) {
3041
- return id;
3042
- }
3043
- function asGitHubUsername(username) {
3044
- return username;
3045
- }
3046
2278
  var ROUTES = {
3047
2279
  REGISTRY_LIST: "/registry",
3048
2280
  REGISTRY_REGISTER: "/register",
@@ -3083,49 +2315,6 @@ var InviteRedemptionSchema = z4.object({
3083
2315
  redeemedAt: z4.number(),
3084
2316
  tokenId: z4.string()
3085
2317
  });
3086
- function parseInviteFromUrl(url) {
3087
- try {
3088
- const inviteParam = new URL(url).searchParams.get("invite");
3089
- if (!inviteParam) return null;
3090
- const [tokenId, tokenValue] = inviteParam.split(":");
3091
- if (!tokenId || !tokenValue) return null;
3092
- return {
3093
- tokenId,
3094
- tokenValue
3095
- };
3096
- } catch {
3097
- return null;
3098
- }
3099
- }
3100
- function buildInviteUrl(baseUrl, planId, tokenId, tokenValue) {
3101
- const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
3102
- const url = new URL(`${normalizedBase}${ROUTES.WEB_TASK(planId)}`);
3103
- url.searchParams.set("invite", `${tokenId}:${tokenValue}`);
3104
- return url.toString();
3105
- }
3106
- function getTokenTimeRemaining(expiresAt) {
3107
- const remaining = expiresAt - Date.now();
3108
- if (remaining <= 0) return {
3109
- expired: true,
3110
- minutes: 0,
3111
- formatted: "Expired"
3112
- };
3113
- const minutes = Math.ceil(remaining / 6e4);
3114
- if (minutes >= 60) {
3115
- const hours = Math.floor(minutes / 60);
3116
- const mins = minutes % 60;
3117
- return {
3118
- expired: false,
3119
- minutes,
3120
- formatted: mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
3121
- };
3122
- }
3123
- return {
3124
- expired: false,
3125
- minutes,
3126
- formatted: `${minutes}m`
3127
- };
3128
- }
3129
2318
  var GitFileStatusSchema = z4.enum([
3130
2319
  "added",
3131
2320
  "modified",
@@ -3163,11 +2352,6 @@ var LocalChangesUnavailableSchema = z4.object({
3163
2352
  message: z4.string()
3164
2353
  });
3165
2354
  var LocalChangesResultSchema = z4.discriminatedUnion("available", [LocalChangesResponseSchema, LocalChangesUnavailableSchema]);
3166
- var P2PMessageType = {
3167
- CONVERSATION_EXPORT_START: 240,
3168
- CONVERSATION_CHUNK: 241,
3169
- CONVERSATION_EXPORT_END: 242
3170
- };
3171
2355
  var ConversationExportStartMetaSchema = z4.object({
3172
2356
  exportId: z4.string(),
3173
2357
  totalChunks: z4.number().int().positive(),
@@ -3187,112 +2371,9 @@ var ConversationExportEndSchema = z4.object({
3187
2371
  exportId: z4.string(),
3188
2372
  checksum: z4.string()
3189
2373
  });
3190
- function isConversationExportStart(data) {
3191
- return data.length > 0 && data[0] === P2PMessageType.CONVERSATION_EXPORT_START;
3192
- }
3193
- function isConversationChunk(data) {
3194
- return data.length > 0 && data[0] === P2PMessageType.CONVERSATION_CHUNK;
3195
- }
3196
- function isConversationExportEnd(data) {
3197
- return data.length > 0 && data[0] === P2PMessageType.CONVERSATION_EXPORT_END;
3198
- }
3199
- function isP2PConversationMessage(data) {
3200
- if (data.length === 0) return false;
3201
- const type = data[0];
3202
- return type === P2PMessageType.CONVERSATION_EXPORT_START || type === P2PMessageType.CONVERSATION_CHUNK || type === P2PMessageType.CONVERSATION_EXPORT_END;
3203
- }
3204
2374
  var textEncoder = new TextEncoder();
3205
2375
  var textDecoder = new TextDecoder();
3206
- function encodeExportStartMessage(meta) {
3207
- const jsonBytes = textEncoder.encode(JSON.stringify(meta));
3208
- const result = new Uint8Array(1 + jsonBytes.length);
3209
- result[0] = P2PMessageType.CONVERSATION_EXPORT_START;
3210
- result.set(jsonBytes, 1);
3211
- return result;
3212
- }
3213
- function decodeExportStartMessage(data) {
3214
- if (data.length === 0 || data[0] !== P2PMessageType.CONVERSATION_EXPORT_START) throw new Error("Invalid export start message: wrong type byte");
3215
- const jsonStr = textDecoder.decode(data.slice(1));
3216
- const parsed = JSON.parse(jsonStr);
3217
- return ConversationExportStartMetaSchema.parse(parsed);
3218
- }
3219
- function encodeChunkMessage(chunk) {
3220
- const exportIdBytes = textEncoder.encode(chunk.exportId);
3221
- const result = new Uint8Array(5 + exportIdBytes.length + 4 + chunk.data.length);
3222
- let offset = 0;
3223
- result[offset] = P2PMessageType.CONVERSATION_CHUNK;
3224
- offset += 1;
3225
- const view = new DataView(result.buffer);
3226
- view.setUint32(offset, exportIdBytes.length, false);
3227
- offset += 4;
3228
- result.set(exportIdBytes, offset);
3229
- offset += exportIdBytes.length;
3230
- view.setUint32(offset, chunk.chunkIndex, false);
3231
- offset += 4;
3232
- result.set(chunk.data, offset);
3233
- return result;
3234
- }
3235
- function decodeChunkMessage(data) {
3236
- if (data.length < 9 || data[0] !== P2PMessageType.CONVERSATION_CHUNK) throw new Error("Invalid chunk message: too short or wrong type byte");
3237
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
3238
- let offset = 1;
3239
- const exportIdLength = view.getUint32(offset, false);
3240
- offset += 4;
3241
- if (data.length < 9 + exportIdLength) throw new Error("Invalid chunk message: exportId extends beyond message");
3242
- const exportId = textDecoder.decode(data.slice(offset, offset + exportIdLength));
3243
- offset += exportIdLength;
3244
- const chunkIndex = view.getUint32(offset, false);
3245
- offset += 4;
3246
- const chunkData = data.slice(offset);
3247
- return ChunkMessageSchema.parse({
3248
- exportId,
3249
- chunkIndex,
3250
- data: chunkData
3251
- });
3252
- }
3253
- function encodeExportEndMessage(end) {
3254
- const jsonBytes = textEncoder.encode(JSON.stringify(end));
3255
- const result = new Uint8Array(1 + jsonBytes.length);
3256
- result[0] = P2PMessageType.CONVERSATION_EXPORT_END;
3257
- result.set(jsonBytes, 1);
3258
- return result;
3259
- }
3260
- function decodeExportEndMessage(data) {
3261
- if (data.length === 0 || data[0] !== P2PMessageType.CONVERSATION_EXPORT_END) throw new Error("Invalid export end message: wrong type byte");
3262
- const jsonStr = textDecoder.decode(data.slice(1));
3263
- const parsed = JSON.parse(jsonStr);
3264
- return ConversationExportEndSchema.parse(parsed);
3265
- }
3266
- function decodeP2PMessage(data) {
3267
- if (data.length === 0) throw new Error("Cannot decode empty message");
3268
- const type = data[0];
3269
- if (type === void 0) throw new Error("Message type byte is missing");
3270
- switch (type) {
3271
- case P2PMessageType.CONVERSATION_EXPORT_START:
3272
- return {
3273
- type: "export_start",
3274
- payload: decodeExportStartMessage(data)
3275
- };
3276
- case P2PMessageType.CONVERSATION_CHUNK:
3277
- return {
3278
- type: "chunk",
3279
- payload: decodeChunkMessage(data)
3280
- };
3281
- case P2PMessageType.CONVERSATION_EXPORT_END:
3282
- return {
3283
- type: "export_end",
3284
- payload: decodeExportEndMessage(data)
3285
- };
3286
- default:
3287
- throw new Error(`Unknown P2P message type: 0x${type.toString(16)}`);
3288
- }
3289
- }
3290
- function assertNeverP2PMessage(msg) {
3291
- throw new Error(`Unhandled P2P message type: ${JSON.stringify(msg)}`);
3292
- }
3293
2376
  var PLAN_INDEX_DOC_NAME = "plan-index";
3294
- var PLAN_INDEX_VIEWED_BY_KEY = "viewedBy";
3295
- var NON_PLAN_DB_NAMES = ["plan-index", "idb-keyval"];
3296
2377
  var PlanIndexEntrySchema = z4.discriminatedUnion("deleted", [z4.object({
3297
2378
  deleted: z4.literal(false),
3298
2379
  id: z4.string(),
@@ -3314,18 +2395,6 @@ var PlanIndexEntrySchema = z4.discriminatedUnion("deleted", [z4.object({
3314
2395
  deletedAt: z4.number(),
3315
2396
  deletedBy: z4.string()
3316
2397
  })]);
3317
- function getPlanIndex(ydoc, includeArchived = false) {
3318
- const plansMap = ydoc.getMap(YDOC_KEYS.PLANS);
3319
- const entries = [];
3320
- for (const [_id, data] of plansMap.entries()) {
3321
- const result = PlanIndexEntrySchema.safeParse(data);
3322
- if (result.success) {
3323
- if (!includeArchived && result.data.deleted) continue;
3324
- entries.push(result.data);
3325
- }
3326
- }
3327
- return entries.sort((a, b) => b.updatedAt - a.updatedAt);
3328
- }
3329
2398
  function getPlanIndexEntry(ydoc, planId) {
3330
2399
  const data = ydoc.getMap(YDOC_KEYS.PLANS).get(planId);
3331
2400
  if (!data) return null;
@@ -3336,9 +2405,6 @@ function setPlanIndexEntry(ydoc, entry) {
3336
2405
  const validated = PlanIndexEntrySchema.parse(entry);
3337
2406
  ydoc.getMap(YDOC_KEYS.PLANS).set(validated.id, validated);
3338
2407
  }
3339
- function removePlanIndexEntry(ydoc, planId) {
3340
- ydoc.getMap(YDOC_KEYS.PLANS).delete(planId);
3341
- }
3342
2408
  function touchPlanIndexEntry(ydoc, planId) {
3343
2409
  const entry = getPlanIndexEntry(ydoc, planId);
3344
2410
  if (entry) setPlanIndexEntry(ydoc, {
@@ -3346,84 +2412,6 @@ function touchPlanIndexEntry(ydoc, planId) {
3346
2412
  updatedAt: Date.now()
3347
2413
  });
3348
2414
  }
3349
- function getViewedByFromIndex(ydoc, planId) {
3350
- const planViewedBy = ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY).get(planId);
3351
- if (!planViewedBy || !(planViewedBy instanceof Y2.Map)) return {};
3352
- const result = {};
3353
- for (const [username, timestamp] of planViewedBy.entries()) if (typeof timestamp === "number") result[username] = timestamp;
3354
- return result;
3355
- }
3356
- function updatePlanIndexViewedBy(ydoc, planId, username) {
3357
- ydoc.transact(() => {
3358
- const viewedByRoot = ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY);
3359
- let planViewedBy = viewedByRoot.get(planId);
3360
- if (!planViewedBy || !(planViewedBy instanceof Y2.Map)) {
3361
- planViewedBy = new Y2.Map();
3362
- viewedByRoot.set(planId, planViewedBy);
3363
- }
3364
- planViewedBy.set(username, Date.now());
3365
- });
3366
- }
3367
- function clearPlanIndexViewedBy(ydoc, planId, username) {
3368
- ydoc.transact(() => {
3369
- const planViewedBy = ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY).get(planId);
3370
- if (planViewedBy && planViewedBy instanceof Y2.Map) planViewedBy.delete(username);
3371
- });
3372
- }
3373
- function getAllViewedByFromIndex(ydoc, planIds) {
3374
- const result = {};
3375
- for (const planId of planIds) result[planId] = getViewedByFromIndex(ydoc, planId);
3376
- return result;
3377
- }
3378
- function removeViewedByFromIndex(ydoc, planId) {
3379
- ydoc.getMap(PLAN_INDEX_VIEWED_BY_KEY).delete(planId);
3380
- }
3381
- var PLAN_INDEX_EVENT_VIEWED_BY_KEY = "event-viewedBy";
3382
- function markEventAsViewed(ydoc, planId, eventId, username) {
3383
- const viewedByRoot = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY);
3384
- const rawPlanEvents = viewedByRoot.get(planId);
3385
- let planEvents;
3386
- if (rawPlanEvents instanceof Y2.Map) planEvents = rawPlanEvents;
3387
- else {
3388
- planEvents = new Y2.Map();
3389
- viewedByRoot.set(planId, planEvents);
3390
- }
3391
- const rawEventViews = planEvents.get(eventId);
3392
- let eventViews;
3393
- if (rawEventViews instanceof Y2.Map) eventViews = rawEventViews;
3394
- else {
3395
- eventViews = new Y2.Map();
3396
- planEvents.set(eventId, eventViews);
3397
- }
3398
- eventViews.set(username, Date.now());
3399
- }
3400
- function clearEventViewedBy(ydoc, planId, eventId, username) {
3401
- const planEvents = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY).get(planId);
3402
- if (!planEvents || !(planEvents instanceof Y2.Map)) return;
3403
- const eventViews = planEvents.get(eventId);
3404
- if (!eventViews || !(eventViews instanceof Y2.Map)) return;
3405
- eventViews.delete(username);
3406
- }
3407
- function isEventUnread(ydoc, planId, eventId, username) {
3408
- const planEvents = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY).get(planId);
3409
- if (!planEvents || !(planEvents instanceof Y2.Map)) return true;
3410
- const eventViews = planEvents.get(eventId);
3411
- if (!eventViews || !(eventViews instanceof Y2.Map)) return true;
3412
- return !eventViews.has(username);
3413
- }
3414
- function getAllEventViewedByForPlan(ydoc, planId) {
3415
- const planEvents = ydoc.getMap(PLAN_INDEX_EVENT_VIEWED_BY_KEY).get(planId);
3416
- if (!planEvents || !(planEvents instanceof Y2.Map)) return {};
3417
- const result = {};
3418
- for (const [eventId, eventViews] of planEvents.entries()) {
3419
- if (!(eventViews instanceof Y2.Map)) continue;
3420
- const views = {};
3421
- for (const [username, timestamp] of eventViews.entries())
3422
- if (typeof timestamp === "number") views[username] = timestamp;
3423
- result[eventId] = views;
3424
- }
3425
- return result;
3426
- }
3427
2415
  function formatThreadsForLLM(threads, options = {}) {
3428
2416
  const { includeResolved = false, selectedTextMaxLength = 100, resolveUser } = options;
3429
2417
  const unresolvedThreads = threads.filter((t$1) => !t$1.resolved);
@@ -3457,18 +2445,17 @@ function truncate(text, maxLength) {
3457
2445
  }
3458
2446
  var TOOL_NAMES = {
3459
2447
  ADD_ARTIFACT: "add_artifact",
3460
- ADD_PR_REVIEW_COMMENT: "add_pr_review_comment",
3461
2448
  COMPLETE_TASK: "complete_task",
3462
- CREATE_PLAN: "create_plan",
2449
+ CREATE_TASK: "create_task",
3463
2450
  EXECUTE_CODE: "execute_code",
3464
2451
  LINK_PR: "link_pr",
3465
2452
  READ_DIFF_COMMENTS: "read_diff_comments",
3466
- READ_PLAN: "read_plan",
2453
+ READ_TASK: "read_task",
3467
2454
  REGENERATE_SESSION_TOKEN: "regenerate_session_token",
3468
2455
  REQUEST_USER_INPUT: "request_user_input",
3469
2456
  SETUP_REVIEW_NOTIFICATION: "setup_review_notification",
3470
2457
  UPDATE_BLOCK_CONTENT: "update_block_content",
3471
- UPDATE_PLAN: "update_plan"
2458
+ UPDATE_TASK: "update_task"
3472
2459
  };
3473
2460
  var PlanIdSchema = z4.object({ planId: z4.string().min(1) });
3474
2461
  var PlanStatusResponseSchema = z4.object({ status: z4.string() });
@@ -3662,9 +2649,6 @@ function isErrnoException(err) {
3662
2649
  function hasErrorCode(err, code) {
3663
2650
  return isErrnoException(err) && err.code === code;
3664
2651
  }
3665
- function isBuffer(value) {
3666
- return Buffer.isBuffer(value);
3667
- }
3668
2652
  function createUserResolver(ydoc, fallbackLength = 8) {
3669
2653
  const usersMap = ydoc.getMap("users");
3670
2654
  return (userId) => {
@@ -3681,258 +2665,2966 @@ function getSignalingConnections(provider) {
3681
2665
  if (hasSignalingConns(provider)) return provider.signalingConns;
3682
2666
  return [];
3683
2667
  }
3684
- function getWebrtcPeerId(provider) {
3685
- return provider?.room?.peerId;
2668
+
2669
+ // src/registry-server.ts
2670
+ import { mkdirSync, readFileSync as readFileSync2, unlinkSync } from "fs";
2671
+ import { readFile, unlink, writeFile as writeFile2 } from "fs/promises";
2672
+ import http from "http";
2673
+ import { homedir as homedir3 } from "os";
2674
+ import { join as join4, resolve, sep } from "path";
2675
+ import { createExpressMiddleware } from "@trpc/server/adapters/express";
2676
+ import express from "express";
2677
+ import * as decoding from "lib0/decoding";
2678
+ import * as encoding from "lib0/encoding";
2679
+ import { WebSocketServer } from "ws";
2680
+ import { LeveldbPersistence } from "y-leveldb";
2681
+ import * as awarenessProtocol from "y-protocols/awareness";
2682
+ import * as syncProtocol from "y-protocols/sync";
2683
+ import * as Y4 from "yjs";
2684
+
2685
+ // src/config/env/registry.ts
2686
+ import { homedir } from "os";
2687
+ import { join } from "path";
2688
+
2689
+ // ../../packages/shared/dist/registry-config.mjs
2690
+ var DEFAULT_REGISTRY_PORTS = [32191, 32192];
2691
+
2692
+ // src/config/env/registry.ts
2693
+ import { z as z5 } from "zod";
2694
+ var schema = z5.object({
2695
+ REGISTRY_PORT: z5.string().optional().transform((val) => {
2696
+ if (!val) return DEFAULT_REGISTRY_PORTS;
2697
+ const port = Number.parseInt(val, 10);
2698
+ if (Number.isNaN(port)) {
2699
+ throw new Error(`REGISTRY_PORT must be a valid number, got: ${val}`);
2700
+ }
2701
+ return [port];
2702
+ }),
2703
+ SHIPYARD_STATE_DIR: z5.string().optional().default(() => join(homedir(), ".shipyard"))
2704
+ });
2705
+ var registryConfig = loadEnv(schema);
2706
+
2707
+ // src/conversation-handlers.ts
2708
+ import { mkdir, writeFile } from "fs/promises";
2709
+ import { homedir as homedir2 } from "os";
2710
+ import { join as join2 } from "path";
2711
+ import { nanoid as nanoid3 } from "nanoid";
2712
+ async function importConversationHandler(input, ctx) {
2713
+ const { a2aMessages, meta } = input;
2714
+ if (!a2aMessages || !Array.isArray(a2aMessages)) {
2715
+ return {
2716
+ success: false,
2717
+ error: "Missing or invalid a2aMessages"
2718
+ };
2719
+ }
2720
+ if (a2aMessages.length === 0) {
2721
+ return {
2722
+ success: false,
2723
+ error: "a2aMessages array is empty"
2724
+ };
2725
+ }
2726
+ try {
2727
+ const { valid, errors } = validateA2AMessages(a2aMessages);
2728
+ if (errors.length > 0) {
2729
+ return {
2730
+ success: false,
2731
+ error: `Invalid A2A messages: ${errors.map((e) => e.error).join(", ")}`
2732
+ };
2733
+ }
2734
+ const sessionId = nanoid3();
2735
+ const claudeMessages = a2aToClaudeCode(valid, sessionId);
2736
+ const jsonl = formatAsClaudeCodeJSONL(claudeMessages);
2737
+ const projectName = meta?.planId ? `shipyard-${meta.planId.slice(0, 8)}` : process.cwd().split("/").pop() || "shipyard";
2738
+ const projectPath = join2(homedir2(), ".claude", "projects", projectName);
2739
+ await mkdir(projectPath, { recursive: true });
2740
+ const transcriptPath = join2(projectPath, `${sessionId}.jsonl`);
2741
+ await writeFile(transcriptPath, jsonl, "utf-8");
2742
+ ctx.logger.info(
2743
+ {
2744
+ sessionId,
2745
+ transcriptPath,
2746
+ messageCount: claudeMessages.length,
2747
+ sourcePlatform: meta?.sourcePlatform
2748
+ },
2749
+ "Created Claude Code session from imported conversation"
2750
+ );
2751
+ return {
2752
+ success: true,
2753
+ sessionId,
2754
+ transcriptPath,
2755
+ messageCount: claudeMessages.length
2756
+ };
2757
+ } catch (error) {
2758
+ ctx.logger.error({ error }, "Failed to import conversation");
2759
+ return {
2760
+ success: false,
2761
+ error: error instanceof Error ? error.message : "Unknown error"
2762
+ };
2763
+ }
3686
2764
  }
3687
- function getWebrtcRoom(provider) {
3688
- return provider?.room ?? null;
2765
+ function createConversationHandlers() {
2766
+ return {
2767
+ importConversation: (input, ctx) => importConversationHandler(input, ctx)
2768
+ };
3689
2769
  }
3690
2770
 
3691
- export {
3692
- PlanStatusValues,
3693
- PlanViewTabValues,
3694
- OriginPlatformValues,
3695
- ClaudeCodeOriginMetadataSchema,
3696
- DevinOriginMetadataSchema,
3697
- CursorOriginMetadataSchema,
3698
- OriginMetadataSchema,
3699
- parseClaudeCodeOrigin,
3700
- ConversationVersionSchema,
3701
- PlanEventTypes,
3702
- AgentActivityTypes,
3703
- AgentActivityDataSchema,
3704
- PlanEventSchema,
3705
- isInboxWorthy,
3706
- PlanMetadataSchema,
3707
- ArtifactSchema,
3708
- getArtifactUrl,
3709
- DeliverableSchema,
3710
- PlanSnapshotSchema,
3711
- LinkedPRStatusValues,
3712
- LinkedPRSchema,
3713
- PRReviewCommentSchema,
3714
- LocalDiffCommentSchema,
3715
- createLinkedPR,
3716
- createGitHubArtifact,
3717
- createLocalArtifact,
3718
- createInitialConversationVersion,
3719
- createHandedOffConversationVersion,
3720
- assertNever,
3721
- AgentPresenceSchema,
3722
- ReviewCommentSchema,
3723
- ReviewFeedbackSchema,
3724
- CreateHookSessionRequestSchema,
3725
- CreateHookSessionResponseSchema,
3726
- UpdatePlanContentRequestSchema,
3727
- UpdatePlanContentResponseSchema,
3728
- GetReviewStatusResponseSchema,
3729
- UpdatePresenceRequestSchema,
3730
- UpdatePresenceResponseSchema,
3731
- HookApiErrorSchema,
3732
- RegisterServerRequestSchema,
3733
- RegisterServerResponseSchema,
3734
- UnregisterServerRequestSchema,
3735
- UnregisterServerResponseSchema,
3736
- ChangeTypeSchema,
3737
- CreateSubscriptionRequestSchema,
3738
- CreateSubscriptionResponseSchema,
3739
- DEFAULT_INPUT_REQUEST_TIMEOUT_SECONDS,
3740
- InputRequestTypeValues,
3741
- InputRequestStatusValues,
3742
- ChoiceOptionSchema,
2771
+ // src/crdt-validation.ts
2772
+ var corruptionState = /* @__PURE__ */ new Map();
2773
+ var CORRUPTION_REPORT_INTERVAL_MS = 5 * 60 * 1e3;
2774
+ function shouldReportCorruption(planId, key) {
2775
+ const state = corruptionState.get(planId);
2776
+ const now = Date.now();
2777
+ if (!state) {
2778
+ corruptionState.set(planId, {
2779
+ lastReported: now,
2780
+ corruptedKeys: /* @__PURE__ */ new Set([key])
2781
+ });
2782
+ return true;
2783
+ }
2784
+ if (!state.corruptedKeys.has(key)) {
2785
+ state.corruptedKeys.add(key);
2786
+ state.lastReported = now;
2787
+ return true;
2788
+ }
2789
+ if (now - state.lastReported > CORRUPTION_REPORT_INTERVAL_MS) {
2790
+ state.lastReported = now;
2791
+ return true;
2792
+ }
2793
+ return false;
2794
+ }
2795
+ function clearCorruptionState(planId, key) {
2796
+ const state = corruptionState.get(planId);
2797
+ if (state) {
2798
+ state.corruptedKeys.delete(key);
2799
+ if (state.corruptedKeys.size === 0) {
2800
+ corruptionState.delete(planId);
2801
+ }
2802
+ }
2803
+ }
2804
+ function validateMetadata(doc, planId) {
2805
+ const result = getPlanMetadataWithValidation(doc);
2806
+ if (result.success) {
2807
+ clearCorruptionState(planId, YDOC_KEYS.METADATA);
2808
+ return { key: YDOC_KEYS.METADATA, valid: true };
2809
+ }
2810
+ return {
2811
+ key: YDOC_KEYS.METADATA,
2812
+ valid: false,
2813
+ errors: [result.error]
2814
+ };
2815
+ }
2816
+ function validateArray(doc, key, schema4) {
2817
+ const array = doc.getArray(key);
2818
+ const items = array.toJSON();
2819
+ if (items.length === 0) {
2820
+ return { key, valid: true, totalItems: 0, invalidItems: 0 };
2821
+ }
2822
+ const errors = [];
2823
+ let invalidCount = 0;
2824
+ for (let i = 0; i < items.length; i++) {
2825
+ const result = schema4.safeParse(items[i]);
2826
+ if (!result.success) {
2827
+ invalidCount++;
2828
+ errors.push(`Item ${i}: ${result.error?.message ?? "Unknown error"}`);
2829
+ }
2830
+ }
2831
+ return {
2832
+ key,
2833
+ valid: invalidCount === 0,
2834
+ totalItems: items.length,
2835
+ invalidItems: invalidCount,
2836
+ errors: errors.length > 0 ? errors : void 0
2837
+ };
2838
+ }
2839
+ function logCorruption(planId, result, origin) {
2840
+ if (!shouldReportCorruption(planId, result.key)) {
2841
+ return;
2842
+ }
2843
+ logger.error(
2844
+ {
2845
+ planId,
2846
+ key: result.key,
2847
+ totalItems: result.totalItems,
2848
+ invalidItems: result.invalidItems,
2849
+ errors: result.errors?.slice(0, 5),
2850
+ origin: typeof origin === "string" ? origin : void 0
2851
+ },
2852
+ "CRDT corruption detected from peer sync"
2853
+ );
2854
+ }
2855
+ function createArrayObserver(planId, key, schema4) {
2856
+ return (event, transaction) => {
2857
+ const doc = event.target.doc;
2858
+ if (!doc) return;
2859
+ const result = validateArray(doc, key, schema4);
2860
+ if (!result.valid) {
2861
+ logCorruption(planId, result, transaction.origin);
2862
+ } else {
2863
+ clearCorruptionState(planId, key);
2864
+ }
2865
+ };
2866
+ }
2867
+ function attachCRDTValidation(planId, doc) {
2868
+ doc.getMap(YDOC_KEYS.METADATA).observe((_event, transaction) => {
2869
+ const result = validateMetadata(doc, planId);
2870
+ if (!result.valid) {
2871
+ logCorruption(planId, result, transaction.origin);
2872
+ } else {
2873
+ clearCorruptionState(planId, YDOC_KEYS.METADATA);
2874
+ }
2875
+ });
2876
+ doc.getArray(YDOC_KEYS.ARTIFACTS).observe(createArrayObserver(planId, YDOC_KEYS.ARTIFACTS, ArtifactSchema));
2877
+ doc.getArray(YDOC_KEYS.DELIVERABLES).observe(createArrayObserver(planId, YDOC_KEYS.DELIVERABLES, DeliverableSchema));
2878
+ doc.getArray(YDOC_KEYS.LINKED_PRS).observe(createArrayObserver(planId, YDOC_KEYS.LINKED_PRS, LinkedPRSchema));
2879
+ doc.getArray(YDOC_KEYS.EVENTS).observe(createArrayObserver(planId, YDOC_KEYS.EVENTS, PlanEventSchema));
2880
+ doc.getArray(YDOC_KEYS.SNAPSHOTS).observe(createArrayObserver(planId, YDOC_KEYS.SNAPSHOTS, PlanSnapshotSchema));
2881
+ doc.getArray(YDOC_KEYS.PR_REVIEW_COMMENTS).observe(
2882
+ createArrayObserver(
2883
+ planId,
2884
+ YDOC_KEYS.PR_REVIEW_COMMENTS,
2885
+ PRReviewCommentSchema
2886
+ )
2887
+ );
2888
+ doc.getArray(YDOC_KEYS.INPUT_REQUESTS).observe(
2889
+ createArrayObserver(planId, YDOC_KEYS.INPUT_REQUESTS, InputRequestSchema)
2890
+ );
2891
+ logger.debug({ planId }, "CRDT validation observers attached");
2892
+ }
2893
+
2894
+ // src/git-local-changes.ts
2895
+ import { execSync } from "child_process";
2896
+ import { readFileSync } from "fs";
2897
+ import { isAbsolute, join as join3, normalize } from "path";
2898
+ function execGit(command, opts) {
2899
+ try {
2900
+ return execSync(command, {
2901
+ cwd: opts.cwd,
2902
+ encoding: "utf-8",
2903
+ timeout: opts.timeout ?? 5e3,
2904
+ maxBuffer: opts.maxBuffer,
2905
+ stdio: ["pipe", "pipe", "pipe"]
2906
+ }).trim();
2907
+ } catch {
2908
+ return null;
2909
+ }
2910
+ }
2911
+ function ensureGitRepo(cwd) {
2912
+ const isRepo = execGit("git rev-parse --is-inside-work-tree", { cwd });
2913
+ if (isRepo !== null) return null;
2914
+ logger.info({ cwd }, "Not a git repo, initializing with git init");
2915
+ const initResult = execGit("git init", { cwd });
2916
+ if (initResult === null) {
2917
+ logger.error({ cwd }, "Failed to initialize git repository");
2918
+ return {
2919
+ available: false,
2920
+ reason: "git_error",
2921
+ message: "Failed to initialize git repository"
2922
+ };
2923
+ }
2924
+ logger.info({ cwd }, "Git repository initialized");
2925
+ return null;
2926
+ }
2927
+ function getCurrentBranchName(cwd) {
2928
+ const branch = execGit("git rev-parse --abbrev-ref HEAD", { cwd });
2929
+ if (!branch) {
2930
+ logger.warn({ cwd }, "Could not get current branch");
2931
+ return "unknown";
2932
+ }
2933
+ if (branch === "HEAD") {
2934
+ return execGit("git rev-parse --short HEAD", { cwd }) ?? "unknown";
2935
+ }
2936
+ return branch;
2937
+ }
2938
+ function getGitDiff(cwd) {
2939
+ const headDiff = execGit("git diff HEAD", { cwd, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 });
2940
+ if (headDiff !== null) return headDiff;
2941
+ logger.debug({ cwd }, "git diff HEAD failed, trying --cached");
2942
+ return execGit("git diff --cached", { cwd, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }) ?? "";
2943
+ }
2944
+ function mergeFilesWithStatus(staged, unstaged, diffFiles) {
2945
+ const allPaths = /* @__PURE__ */ new Set([
2946
+ ...staged.map((f) => f.path),
2947
+ ...unstaged.map((f) => f.path),
2948
+ ...diffFiles.map((f) => f.path)
2949
+ ]);
2950
+ const mergedFiles = [];
2951
+ for (const path of allPaths) {
2952
+ const diffFile = diffFiles.find((f) => f.path === path);
2953
+ const stagedFile = staged.find((f) => f.path === path);
2954
+ const unstagedFile = unstaged.find((f) => f.path === path);
2955
+ if (diffFile) {
2956
+ mergedFiles.push(diffFile);
2957
+ } else {
2958
+ const status = stagedFile?.status ?? unstagedFile?.status ?? "modified";
2959
+ mergedFiles.push({
2960
+ path,
2961
+ status,
2962
+ additions: 0,
2963
+ deletions: 0,
2964
+ patch: void 0
2965
+ });
2966
+ }
2967
+ }
2968
+ return mergedFiles.sort((a, b) => a.path.localeCompare(b.path));
2969
+ }
2970
+ function getLocalChanges(cwd) {
2971
+ try {
2972
+ const repoError = ensureGitRepo(cwd);
2973
+ if (repoError) return repoError;
2974
+ const branch = getCurrentBranchName(cwd);
2975
+ let headSha;
2976
+ try {
2977
+ headSha = execGit("git rev-parse HEAD", { cwd }) ?? void 0;
2978
+ } catch {
2979
+ headSha = void 0;
2980
+ }
2981
+ const statusOutput = execGit("git status --porcelain", { cwd, timeout: 1e4 }) ?? "";
2982
+ const { staged, unstaged, untracked } = parseGitStatus(statusOutput);
2983
+ const diffOutput = getGitDiff(cwd);
2984
+ const diffFiles = parseDiffOutput(diffOutput);
2985
+ const mergedFiles = mergeFilesWithStatus(staged, unstaged, diffFiles);
2986
+ logger.debug(
2987
+ {
2988
+ cwd,
2989
+ branch,
2990
+ headSha,
2991
+ stagedCount: staged.length,
2992
+ unstagedCount: unstaged.length,
2993
+ untrackedCount: untracked.length,
2994
+ filesCount: mergedFiles.length
2995
+ },
2996
+ "Got local changes"
2997
+ );
2998
+ return {
2999
+ available: true,
3000
+ branch,
3001
+ baseBranch: "HEAD",
3002
+ headSha,
3003
+ staged,
3004
+ unstaged,
3005
+ untracked,
3006
+ files: mergedFiles
3007
+ };
3008
+ } catch (error) {
3009
+ const message = error instanceof Error ? error.message : "Unknown error";
3010
+ logger.error({ error, cwd }, "Failed to get local changes");
3011
+ return {
3012
+ available: false,
3013
+ reason: "git_error",
3014
+ message: `Git error: ${message}`
3015
+ };
3016
+ }
3017
+ }
3018
+ function parseGitStatus(output) {
3019
+ const staged = [];
3020
+ const unstaged = [];
3021
+ const untracked = [];
3022
+ for (const line of output.split("\n")) {
3023
+ if (!line || line.length < 3) continue;
3024
+ const x = line[0];
3025
+ const y = line[1];
3026
+ let path = line.slice(3);
3027
+ if (path.includes(" -> ")) {
3028
+ path = path.split(" -> ")[1] ?? path;
3029
+ }
3030
+ if (x === "?" && y === "?") {
3031
+ untracked.push(path);
3032
+ continue;
3033
+ }
3034
+ if (x === "!" && y === "!") {
3035
+ continue;
3036
+ }
3037
+ if (x && x !== " " && x !== "?") {
3038
+ staged.push({
3039
+ path,
3040
+ status: parseStatusChar(x),
3041
+ additions: 0,
3042
+ deletions: 0
3043
+ });
3044
+ }
3045
+ if (y && y !== " " && y !== "?") {
3046
+ unstaged.push({
3047
+ path,
3048
+ status: parseStatusChar(y),
3049
+ additions: 0,
3050
+ deletions: 0
3051
+ });
3052
+ }
3053
+ }
3054
+ return { staged, unstaged, untracked };
3055
+ }
3056
+ function parseStatusChar(char) {
3057
+ switch (char) {
3058
+ case "A":
3059
+ return "added";
3060
+ case "M":
3061
+ return "modified";
3062
+ case "D":
3063
+ return "deleted";
3064
+ case "R":
3065
+ return "renamed";
3066
+ case "C":
3067
+ return "copied";
3068
+ case "U":
3069
+ return "modified";
3070
+ default:
3071
+ return "modified";
3072
+ }
3073
+ }
3074
+ function parseDiffOutput(diff) {
3075
+ const files = [];
3076
+ if (!diff.trim()) {
3077
+ return files;
3078
+ }
3079
+ const fileDiffs = diff.split(/(?=diff --git )/);
3080
+ for (const fileDiff of fileDiffs) {
3081
+ if (!fileDiff.trim()) continue;
3082
+ const headerMatch = fileDiff.match(/^diff --git a\/(.+?) b\/(.+)/m);
3083
+ if (!headerMatch) continue;
3084
+ const path = headerMatch[2] ?? headerMatch[1];
3085
+ if (!path) continue;
3086
+ if (fileDiff.includes("Binary files")) {
3087
+ files.push({
3088
+ path,
3089
+ status: detectStatus(fileDiff),
3090
+ additions: 0,
3091
+ deletions: 0,
3092
+ patch: void 0
3093
+ });
3094
+ continue;
3095
+ }
3096
+ let additions = 0;
3097
+ let deletions = 0;
3098
+ for (const line of fileDiff.split("\n")) {
3099
+ if (line.startsWith("+") && !line.startsWith("+++")) {
3100
+ additions++;
3101
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
3102
+ deletions++;
3103
+ }
3104
+ }
3105
+ const patchStart = fileDiff.indexOf("@@");
3106
+ const patch = patchStart >= 0 ? fileDiff.slice(patchStart) : void 0;
3107
+ files.push({
3108
+ path,
3109
+ status: detectStatus(fileDiff),
3110
+ additions,
3111
+ deletions,
3112
+ patch
3113
+ });
3114
+ }
3115
+ return files;
3116
+ }
3117
+ function detectStatus(fileDiff) {
3118
+ if (fileDiff.includes("new file mode")) {
3119
+ return "added";
3120
+ }
3121
+ if (fileDiff.includes("deleted file mode")) {
3122
+ return "deleted";
3123
+ }
3124
+ if (fileDiff.includes("rename from")) {
3125
+ return "renamed";
3126
+ }
3127
+ if (fileDiff.includes("copy from")) {
3128
+ return "copied";
3129
+ }
3130
+ return "modified";
3131
+ }
3132
+ function getFileContent(cwd, filePath) {
3133
+ try {
3134
+ const normalizedPath = normalize(filePath);
3135
+ if (isAbsolute(normalizedPath) || normalizedPath.startsWith("..")) {
3136
+ return { content: null, error: "Invalid file path" };
3137
+ }
3138
+ const fullPath = join3(cwd, normalizedPath);
3139
+ if (!fullPath.startsWith(cwd)) {
3140
+ return { content: null, error: "Invalid file path" };
3141
+ }
3142
+ const content = readFileSync(fullPath, { encoding: "utf-8" });
3143
+ if (content.length > 10 * 1024 * 1024) {
3144
+ return { content: null, error: "File too large to display" };
3145
+ }
3146
+ return { content };
3147
+ } catch (error) {
3148
+ const message = error instanceof Error ? error.message : "Unknown error";
3149
+ if (message.includes("ENOENT")) {
3150
+ return { content: null, error: "File not found" };
3151
+ }
3152
+ if (message.includes("EISDIR")) {
3153
+ return { content: null, error: "Path is a directory" };
3154
+ }
3155
+ return { content: null, error: `Failed to read file: ${message}` };
3156
+ }
3157
+ }
3158
+
3159
+ // src/github-artifacts.ts
3160
+ import { Octokit } from "@octokit/rest";
3161
+
3162
+ // src/config/env/github.ts
3163
+ import { execSync as execSync2 } from "child_process";
3164
+ import { z as z6 } from "zod";
3165
+ function getTokenFromGhCli() {
3166
+ try {
3167
+ const token = execSync2("gh auth token", {
3168
+ encoding: "utf-8",
3169
+ timeout: 5e3,
3170
+ stdio: ["pipe", "pipe", "pipe"]
3171
+ }).trim();
3172
+ if (token) {
3173
+ return token;
3174
+ }
3175
+ } catch {
3176
+ }
3177
+ return null;
3178
+ }
3179
+ var schema2 = z6.object({
3180
+ GITHUB_USERNAME: z6.string().optional(),
3181
+ GITHUB_TOKEN: z6.string().optional().transform((val) => val || getTokenFromGhCli() || null),
3182
+ SHIPYARD_ARTIFACTS: z6.string().optional().transform((val) => {
3183
+ if (!val) return true;
3184
+ const setting = val.toLowerCase();
3185
+ return setting !== "disabled" && setting !== "false" && setting !== "0";
3186
+ })
3187
+ });
3188
+ var githubConfig = loadEnv(schema2);
3189
+
3190
+ // src/github-artifacts.ts
3191
+ var ARTIFACTS_BRANCH = "plan-artifacts";
3192
+ function parseRepoString(repo) {
3193
+ const parts = repo.split("/");
3194
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
3195
+ throw new Error(`Invalid repo format: "${repo}". Expected "owner/repo".`);
3196
+ }
3197
+ return { owner: parts[0], repoName: parts[1] };
3198
+ }
3199
+ function isArtifactsEnabled() {
3200
+ return githubConfig.SHIPYARD_ARTIFACTS;
3201
+ }
3202
+ function resolveGitHubToken() {
3203
+ return githubConfig.GITHUB_TOKEN;
3204
+ }
3205
+ function getErrorStatus(error) {
3206
+ if (!error || typeof error !== "object") return void 0;
3207
+ const record = Object.fromEntries(Object.entries(error));
3208
+ const status = record.status;
3209
+ return typeof status === "number" ? status : void 0;
3210
+ }
3211
+ function isAuthError(error) {
3212
+ const status = getErrorStatus(error);
3213
+ return status === 401 || status === 403;
3214
+ }
3215
+ var GitHubAuthError = class extends Error {
3216
+ constructor(message) {
3217
+ super(message);
3218
+ this.name = "GitHubAuthError";
3219
+ }
3220
+ };
3221
+ async function withTokenRetry(operation) {
3222
+ try {
3223
+ return await operation();
3224
+ } catch (error) {
3225
+ if (isAuthError(error)) {
3226
+ logger.info("GitHub auth error, checking token and retrying...");
3227
+ const newToken = resolveGitHubToken();
3228
+ if (!newToken) {
3229
+ throw new GitHubAuthError(
3230
+ "GitHub token expired and could not be refreshed.\n\nTo fix this, run in your terminal:\n gh auth login\n\nOr set GITHUB_TOKEN environment variable in your MCP config."
3231
+ );
3232
+ }
3233
+ try {
3234
+ return await operation();
3235
+ } catch (retryError) {
3236
+ if (isAuthError(retryError)) {
3237
+ throw new GitHubAuthError(
3238
+ "GitHub authentication failed after token refresh.\n\nYour token may not have the required permissions.\nRun: gh auth login --scopes repo\n\nOr check your GITHUB_TOKEN has repo access."
3239
+ );
3240
+ }
3241
+ throw retryError;
3242
+ }
3243
+ }
3244
+ throw error;
3245
+ }
3246
+ }
3247
+ function getOctokit() {
3248
+ const token = resolveGitHubToken();
3249
+ if (!token) {
3250
+ return null;
3251
+ }
3252
+ return new Octokit({ auth: token });
3253
+ }
3254
+ function isGitHubConfigured() {
3255
+ return !!resolveGitHubToken();
3256
+ }
3257
+ async function ensureArtifactsBranch(repo) {
3258
+ return withTokenRetry(async () => {
3259
+ const octokit = getOctokit();
3260
+ if (!octokit) {
3261
+ throw new Error("GITHUB_TOKEN not set");
3262
+ }
3263
+ const { owner, repoName } = parseRepoString(repo);
3264
+ try {
3265
+ await octokit.repos.getBranch({
3266
+ owner,
3267
+ repo: repoName,
3268
+ branch: ARTIFACTS_BRANCH
3269
+ });
3270
+ logger.debug({ repo }, "Artifacts branch exists");
3271
+ return;
3272
+ } catch (error) {
3273
+ if (getErrorStatus(error) !== 404) {
3274
+ throw error;
3275
+ }
3276
+ }
3277
+ logger.info({ repo }, "Creating artifacts branch");
3278
+ try {
3279
+ const { data: repoData } = await octokit.repos.get({ owner, repo: repoName });
3280
+ const defaultBranch = repoData.default_branch;
3281
+ const { data: refData } = await octokit.git.getRef({
3282
+ owner,
3283
+ repo: repoName,
3284
+ ref: `heads/${defaultBranch}`
3285
+ });
3286
+ await octokit.git.createRef({
3287
+ owner,
3288
+ repo: repoName,
3289
+ ref: `refs/heads/${ARTIFACTS_BRANCH}`,
3290
+ sha: refData.object.sha
3291
+ });
3292
+ logger.info({ repo, branch: ARTIFACTS_BRANCH }, "Created artifacts branch");
3293
+ } catch (error) {
3294
+ const message = error instanceof Error ? error.message : "Unknown error";
3295
+ throw new Error(
3296
+ `Failed to create "${ARTIFACTS_BRANCH}" branch. Please create it manually:
3297
+
3298
+ git checkout --orphan ${ARTIFACTS_BRANCH}
3299
+ git rm -rf .
3300
+ git commit --allow-empty -m "Initialize plan artifacts"
3301
+ git push -u origin ${ARTIFACTS_BRANCH}
3302
+ git checkout main
3303
+
3304
+ Error: ${message}`
3305
+ );
3306
+ }
3307
+ });
3308
+ }
3309
+ async function uploadArtifact(params) {
3310
+ return withTokenRetry(async () => {
3311
+ const octokit = getOctokit();
3312
+ if (!octokit) {
3313
+ throw new Error("GITHUB_TOKEN not set");
3314
+ }
3315
+ const { repo, planId, filename, content } = params;
3316
+ const { owner, repoName } = parseRepoString(repo);
3317
+ const path = `plans/${planId}/${filename}`;
3318
+ await ensureArtifactsBranch(repo);
3319
+ let existingSha;
3320
+ try {
3321
+ const { data } = await octokit.repos.getContent({
3322
+ owner,
3323
+ repo: repoName,
3324
+ path,
3325
+ ref: ARTIFACTS_BRANCH
3326
+ });
3327
+ if (!Array.isArray(data) && data.type === "file") {
3328
+ existingSha = data.sha;
3329
+ }
3330
+ } catch (error) {
3331
+ if (getErrorStatus(error) !== 404) {
3332
+ throw error;
3333
+ }
3334
+ }
3335
+ await octokit.repos.createOrUpdateFileContents({
3336
+ owner,
3337
+ repo: repoName,
3338
+ path,
3339
+ message: `Add artifact: ${filename}`,
3340
+ content,
3341
+ branch: ARTIFACTS_BRANCH,
3342
+ sha: existingSha
3343
+ });
3344
+ const url = `https://raw.githubusercontent.com/${repo}/${ARTIFACTS_BRANCH}/${path}`;
3345
+ logger.info({ repo, path, url }, "Artifact uploaded");
3346
+ return url;
3347
+ });
3348
+ }
3349
+
3350
+ // src/hook-handlers.ts
3351
+ import { ServerBlockNoteEditor } from "@blocknote/server-util";
3352
+
3353
+ // ../../packages/shared/dist/index.mjs
3354
+ import { createHash, randomBytes } from "crypto";
3355
+ function computeHash(content) {
3356
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
3357
+ }
3358
+ function generateSessionToken() {
3359
+ return randomBytes(32).toString("base64url");
3360
+ }
3361
+ function hashSessionToken(token) {
3362
+ return createHash("sha256").update(token).digest("hex");
3363
+ }
3364
+ var APPROVAL_LONG_POLL_TIMEOUT_MS = 1800 * 1e3;
3365
+ var DEFAULT_TRPC_TIMEOUT_MS = 10 * 1e3;
3366
+
3367
+ // src/hook-handlers.ts
3368
+ import { TRPCError as TRPCError2 } from "@trpc/server";
3369
+ import { nanoid as nanoid4 } from "nanoid";
3370
+ import open from "open";
3371
+
3372
+ // src/config/env/web.ts
3373
+ import { z as z7 } from "zod";
3374
+ var schema3 = z7.object({
3375
+ SHIPYARD_WEB_URL: z7.string().url().default("https://schoolai.github.io/shipyard")
3376
+ });
3377
+ var webConfig = loadEnv(schema3);
3378
+
3379
+ // src/hub-client.ts
3380
+ import { WebsocketProvider } from "y-websocket";
3381
+ import * as Y3 from "yjs";
3382
+ var providers = /* @__PURE__ */ new Map();
3383
+ var docs = /* @__PURE__ */ new Map();
3384
+ var hubPort = null;
3385
+ var initialized = false;
3386
+ async function initHubClient(port) {
3387
+ if (initialized) {
3388
+ logger.warn("Hub client already initialized");
3389
+ return;
3390
+ }
3391
+ hubPort = port;
3392
+ initialized = true;
3393
+ logger.info({ hubPort }, "Hub client initialized, will connect to registry hub");
3394
+ }
3395
+ function isHubClientInitialized() {
3396
+ return initialized;
3397
+ }
3398
+ async function getOrCreateDoc(docName) {
3399
+ const existing = docs.get(docName);
3400
+ if (existing) {
3401
+ return existing;
3402
+ }
3403
+ if (!initialized || !hubPort) {
3404
+ throw new Error("Hub client not initialized. Call initHubClient() first.");
3405
+ }
3406
+ const doc = new Y3.Doc();
3407
+ docs.set(docName, doc);
3408
+ const hubUrl = `ws://localhost:${hubPort}`;
3409
+ const provider = new WebsocketProvider(hubUrl, docName, doc, {
3410
+ connect: true,
3411
+ maxBackoffTime: 2500
3412
+ });
3413
+ providers.set(docName, provider);
3414
+ await new Promise((resolve2, reject) => {
3415
+ if (provider.synced) {
3416
+ logger.debug({ docName }, "Provider already synced");
3417
+ resolve2();
3418
+ return;
3419
+ }
3420
+ const onSync = (isSynced) => {
3421
+ if (isSynced) {
3422
+ logger.debug({ docName }, "Provider synced via sync event");
3423
+ provider.off("sync", onSync);
3424
+ clearTimeout(timeoutId);
3425
+ resolve2();
3426
+ }
3427
+ };
3428
+ provider.on("sync", onSync);
3429
+ const timeoutId = setTimeout(() => {
3430
+ if (!provider.synced) {
3431
+ provider.off("sync", onSync);
3432
+ logger.error({ docName, synced: provider.synced }, "Hub sync timeout - cannot proceed");
3433
+ reject(new Error(`Failed to sync document '${docName}' with hub within 10 seconds`));
3434
+ }
3435
+ }, 1e4);
3436
+ });
3437
+ logger.info({ docName, hubUrl }, "Connected to hub for document sync");
3438
+ return doc;
3439
+ }
3440
+ async function hasActiveConnections(planId) {
3441
+ if (!hubPort) return false;
3442
+ try {
3443
+ const res = await fetch(`http://localhost:${hubPort}${ROUTES.PLAN_HAS_CONNECTIONS(planId)}`, {
3444
+ signal: AbortSignal.timeout(500)
3445
+ });
3446
+ if (!res.ok) return false;
3447
+ const data = HasConnectionsResponseSchema.parse(await res.json());
3448
+ return data.hasConnections;
3449
+ } catch {
3450
+ return false;
3451
+ }
3452
+ }
3453
+
3454
+ // src/webrtc-provider.ts
3455
+ import wrtc from "@roamhq/wrtc";
3456
+ import { WebrtcProvider } from "y-webrtc";
3457
+
3458
+ // src/server-identity.ts
3459
+ import { execSync as execSync3 } from "child_process";
3460
+ import os from "os";
3461
+ import { basename } from "path";
3462
+ import { z as z8 } from "zod";
3463
+ var GitHubUserResponseSchema = z8.object({
3464
+ login: z8.string().optional()
3465
+ });
3466
+ var cachedUsername = null;
3467
+ var usernameResolved = false;
3468
+ var cachedRepoName = null;
3469
+ function getRepositoryFullName() {
3470
+ if (cachedRepoName !== null) {
3471
+ return cachedRepoName || null;
3472
+ }
3473
+ try {
3474
+ const repoName = execSync3("gh repo view --json nameWithOwner --jq .nameWithOwner", {
3475
+ encoding: "utf-8",
3476
+ timeout: 5e3,
3477
+ stdio: ["pipe", "pipe", "pipe"]
3478
+ }).trim();
3479
+ if (!repoName) {
3480
+ cachedRepoName = "";
3481
+ return null;
3482
+ }
3483
+ cachedRepoName = repoName;
3484
+ return cachedRepoName;
3485
+ } catch {
3486
+ cachedRepoName = "";
3487
+ return null;
3488
+ }
3489
+ }
3490
+ async function getGitHubUsername() {
3491
+ if (usernameResolved && cachedUsername) {
3492
+ return cachedUsername;
3493
+ }
3494
+ if (githubConfig.GITHUB_USERNAME) {
3495
+ cachedUsername = githubConfig.GITHUB_USERNAME;
3496
+ usernameResolved = true;
3497
+ logger.info({ username: cachedUsername }, "Using GITHUB_USERNAME from env");
3498
+ return cachedUsername;
3499
+ }
3500
+ if (githubConfig.GITHUB_TOKEN) {
3501
+ const username = await getUsernameFromToken(githubConfig.GITHUB_TOKEN);
3502
+ if (username) {
3503
+ cachedUsername = username;
3504
+ usernameResolved = true;
3505
+ logger.info({ username }, "Resolved username from GITHUB_TOKEN via API");
3506
+ return cachedUsername;
3507
+ }
3508
+ }
3509
+ const cliUsername = getUsernameFromCLI();
3510
+ if (cliUsername) {
3511
+ cachedUsername = cliUsername;
3512
+ usernameResolved = true;
3513
+ logger.info({ username: cliUsername }, "Resolved username from gh CLI");
3514
+ return cachedUsername;
3515
+ }
3516
+ const gitUsername = getUsernameFromGitConfig();
3517
+ if (gitUsername) {
3518
+ cachedUsername = gitUsername;
3519
+ usernameResolved = true;
3520
+ logger.warn({ username: gitUsername }, "Using git config user.name (UNVERIFIED)");
3521
+ return cachedUsername;
3522
+ }
3523
+ const osUsername = process.env.USER || process.env.USERNAME;
3524
+ if (osUsername) {
3525
+ cachedUsername = osUsername.replace(/[^a-zA-Z0-9_-]/g, "_");
3526
+ usernameResolved = true;
3527
+ logger.warn(
3528
+ { username: cachedUsername, original: osUsername },
3529
+ "Using sanitized OS username (UNVERIFIED)"
3530
+ );
3531
+ return cachedUsername;
3532
+ }
3533
+ usernameResolved = true;
3534
+ throw new Error(
3535
+ 'GitHub username required but could not be determined.\n\nConfigure ONE of:\n1. GITHUB_USERNAME=your-username (explicit)\n2. GITHUB_TOKEN=ghp_xxx (will fetch from API)\n3. gh auth login (uses CLI)\n4. git config --global user.name "your-username"\n5. Set USER or USERNAME environment variable\n\nFor remote agents: Use option 1 or 2'
3536
+ );
3537
+ }
3538
+ async function getUsernameFromToken(token) {
3539
+ try {
3540
+ const response = await fetch("https://api.github.com/user", {
3541
+ headers: {
3542
+ Authorization: `Bearer ${token}`,
3543
+ Accept: "application/vnd.github.v3+json",
3544
+ "User-Agent": "shipyard-mcp-server"
3545
+ },
3546
+ signal: AbortSignal.timeout(5e3)
3547
+ });
3548
+ if (!response.ok) return null;
3549
+ const user = GitHubUserResponseSchema.parse(await response.json());
3550
+ return user.login ?? null;
3551
+ } catch (error) {
3552
+ logger.debug({ error }, "GitHub API failed");
3553
+ return null;
3554
+ }
3555
+ }
3556
+ function getUsernameFromCLI() {
3557
+ try {
3558
+ const username = execSync3("gh api user --jq .login", {
3559
+ encoding: "utf-8",
3560
+ timeout: 5e3,
3561
+ stdio: ["pipe", "pipe", "pipe"]
3562
+ }).trim();
3563
+ return username || null;
3564
+ } catch {
3565
+ return null;
3566
+ }
3567
+ }
3568
+ function getUsernameFromGitConfig() {
3569
+ try {
3570
+ const username = execSync3("git config user.name", {
3571
+ encoding: "utf-8",
3572
+ timeout: 5e3,
3573
+ stdio: ["pipe", "pipe", "pipe"]
3574
+ }).trim();
3575
+ return username || null;
3576
+ } catch {
3577
+ return null;
3578
+ }
3579
+ }
3580
+ async function getVerifiedGitHubUsername() {
3581
+ if (githubConfig.GITHUB_USERNAME) {
3582
+ logger.info({ username: githubConfig.GITHUB_USERNAME }, "Using GITHUB_USERNAME from env");
3583
+ return githubConfig.GITHUB_USERNAME;
3584
+ }
3585
+ if (githubConfig.GITHUB_TOKEN) {
3586
+ try {
3587
+ const username = await getUsernameFromToken(githubConfig.GITHUB_TOKEN);
3588
+ if (username) {
3589
+ logger.info({ username }, "Got verified username from GITHUB_TOKEN");
3590
+ return username;
3591
+ }
3592
+ } catch (error) {
3593
+ logger.warn({ error }, "Failed to get username from GITHUB_TOKEN");
3594
+ }
3595
+ }
3596
+ try {
3597
+ const username = getUsernameFromCLI();
3598
+ if (username) {
3599
+ logger.info({ username }, "Got verified username from gh CLI");
3600
+ return username;
3601
+ }
3602
+ } catch (error) {
3603
+ logger.debug({ error }, "Failed to get username from gh CLI");
3604
+ }
3605
+ logger.warn("No verified GitHub authentication available");
3606
+ return null;
3607
+ }
3608
+ function getGitBranch() {
3609
+ try {
3610
+ return execSync3("git branch --show-current", {
3611
+ encoding: "utf-8",
3612
+ timeout: 2e3,
3613
+ stdio: ["pipe", "pipe", "pipe"]
3614
+ }).trim() || void 0;
3615
+ } catch {
3616
+ return void 0;
3617
+ }
3618
+ }
3619
+ function getEnvironmentContext() {
3620
+ return {
3621
+ projectName: basename(process.cwd()) || void 0,
3622
+ branch: getGitBranch(),
3623
+ hostname: os.hostname(),
3624
+ repo: getRepositoryFullName() || void 0
3625
+ };
3626
+ }
3627
+
3628
+ // src/webrtc-provider.ts
3629
+ var SIGNALING_SERVER = process.env.SIGNALING_URL || "wss://shipyard-signaling.jacob-191.workers.dev";
3630
+ var globalAny = globalThis;
3631
+ if (typeof globalAny.RTCPeerConnection === "undefined") {
3632
+ globalAny.RTCPeerConnection = wrtc.RTCPeerConnection;
3633
+ globalAny.RTCSessionDescription = wrtc.RTCSessionDescription;
3634
+ globalAny.RTCIceCandidate = wrtc.RTCIceCandidate;
3635
+ }
3636
+ async function createWebRtcProvider(ydoc, planId) {
3637
+ const iceServers = [
3638
+ { urls: "stun:stun.l.google.com:19302" },
3639
+ { urls: "stun:stun1.l.google.com:19302" }
3640
+ ];
3641
+ if (process.env.TURN_URL && process.env.TURN_USERNAME && process.env.TURN_CREDENTIAL) {
3642
+ iceServers.push({
3643
+ urls: process.env.TURN_URL,
3644
+ username: process.env.TURN_USERNAME,
3645
+ credential: process.env.TURN_CREDENTIAL
3646
+ });
3647
+ logger.info({ turnUrl: process.env.TURN_URL }, "TURN server configured");
3648
+ }
3649
+ const roomName = `shipyard-${planId}`;
3650
+ const provider = new WebrtcProvider(roomName, ydoc, {
3651
+ signaling: [SIGNALING_SERVER],
3652
+ peerOpts: {
3653
+ // @ts-expect-error - wrtc type definitions don't match runtime structure
3654
+ wrtc: wrtc.default || wrtc,
3655
+ config: {
3656
+ iceServers
3657
+ }
3658
+ }
3659
+ });
3660
+ const username = await getGitHubUsername().catch(() => void 0);
3661
+ const fallbackId = `mcp-anon-${crypto.randomUUID().slice(0, 8)}`;
3662
+ const userId = username ? `mcp-${username}` : fallbackId;
3663
+ const displayName = username ? `Claude Code (${username})` : "Claude Code";
3664
+ const awarenessState = {
3665
+ user: {
3666
+ id: userId,
3667
+ name: displayName,
3668
+ color: "#0066cc"
3669
+ },
3670
+ platform: "claude-code",
3671
+ status: "approved",
3672
+ isOwner: true,
3673
+ webrtcPeerId: crypto.randomUUID(),
3674
+ context: getEnvironmentContext()
3675
+ };
3676
+ provider.awareness.setLocalStateField("planStatus", awarenessState);
3677
+ logger.info(
3678
+ { planId, username: username ?? fallbackId, platform: "claude-code", hasContext: true },
3679
+ "MCP awareness state set"
3680
+ );
3681
+ sendApprovalStateToSignaling(provider, planId, username ?? fallbackId);
3682
+ setupProviderListeners(provider, planId);
3683
+ logger.info(
3684
+ {
3685
+ planId,
3686
+ roomName,
3687
+ signaling: SIGNALING_SERVER,
3688
+ hasTurn: iceServers.length > 2
3689
+ },
3690
+ "WebRTC provider created"
3691
+ );
3692
+ return provider;
3693
+ }
3694
+ function sendApprovalStateToSignaling(provider, planId, username) {
3695
+ const signalingConns = getSignalingConnections(provider);
3696
+ if (signalingConns.length === 0) {
3697
+ setTimeout(() => sendApprovalStateToSignaling(provider, planId, username), 1e3);
3698
+ return;
3699
+ }
3700
+ const identifyMessage = JSON.stringify({
3701
+ type: "subscribe",
3702
+ topics: [],
3703
+ userId: username
3704
+ });
3705
+ const approvalStateMessage = JSON.stringify({
3706
+ type: "approval_state",
3707
+ planId,
3708
+ ownerId: username,
3709
+ approvedUsers: [username],
3710
+ rejectedUsers: []
3711
+ });
3712
+ for (const conn of signalingConns) {
3713
+ const ws = conn.ws;
3714
+ if (ws && ws.readyState === WebSocket.OPEN) {
3715
+ ws.send(identifyMessage);
3716
+ ws.send(approvalStateMessage);
3717
+ logger.info({ planId, username }, "Pushed identity and approval state to signaling server");
3718
+ }
3719
+ }
3720
+ }
3721
+ function setupProviderListeners(provider, planId) {
3722
+ provider.on("peers", (event) => {
3723
+ const peerCount = event.webrtcPeers.length;
3724
+ if (event.added.length > 0) {
3725
+ logger.info(
3726
+ {
3727
+ planId,
3728
+ added: event.added,
3729
+ totalPeers: peerCount
3730
+ },
3731
+ "WebRTC peer connected"
3732
+ );
3733
+ }
3734
+ if (event.removed.length > 0) {
3735
+ logger.info(
3736
+ {
3737
+ planId,
3738
+ removed: event.removed,
3739
+ totalPeers: peerCount
3740
+ },
3741
+ "WebRTC peer disconnected"
3742
+ );
3743
+ }
3744
+ });
3745
+ provider.on("synced", (event) => {
3746
+ logger.info(
3747
+ {
3748
+ planId,
3749
+ synced: event.synced
3750
+ },
3751
+ "WebRTC sync status changed"
3752
+ );
3753
+ });
3754
+ provider.on("status", (event) => {
3755
+ logger.info(
3756
+ {
3757
+ planId,
3758
+ connected: event.connected
3759
+ },
3760
+ "WebRTC signaling status changed"
3761
+ );
3762
+ });
3763
+ }
3764
+
3765
+ // src/doc-store.ts
3766
+ var currentMode = "uninitialized";
3767
+ var webrtcProviders = /* @__PURE__ */ new Map();
3768
+ function initAsHub() {
3769
+ if (currentMode !== "uninitialized") {
3770
+ logger.warn({ currentMode }, "Doc store already initialized");
3771
+ return;
3772
+ }
3773
+ currentMode = "hub";
3774
+ logger.info("Doc store initialized as hub (registry server mode)");
3775
+ }
3776
+ async function initAsClient(registryPort) {
3777
+ if (currentMode !== "uninitialized") {
3778
+ logger.warn({ currentMode }, "Doc store already initialized");
3779
+ return;
3780
+ }
3781
+ await initHubClient(registryPort);
3782
+ currentMode = "client";
3783
+ logger.info({ registryPort }, "Doc store initialized as client (hub-client mode)");
3784
+ }
3785
+ async function getOrCreateDoc3(docName) {
3786
+ let doc;
3787
+ switch (currentMode) {
3788
+ case "hub":
3789
+ doc = await getOrCreateDoc2(docName);
3790
+ break;
3791
+ case "client":
3792
+ doc = await getOrCreateDoc(docName);
3793
+ break;
3794
+ case "uninitialized":
3795
+ if (isHubClientInitialized()) {
3796
+ currentMode = "client";
3797
+ doc = await getOrCreateDoc(docName);
3798
+ } else {
3799
+ logger.warn("Doc store not initialized, defaulting to registry server mode");
3800
+ currentMode = "hub";
3801
+ doc = await getOrCreateDoc2(docName);
3802
+ }
3803
+ }
3804
+ if (!webrtcProviders.has(docName)) {
3805
+ try {
3806
+ const provider = await createWebRtcProvider(doc, docName);
3807
+ webrtcProviders.set(docName, provider);
3808
+ logger.info({ docName }, "WebRTC P2P sync enabled for plan");
3809
+ } catch (error) {
3810
+ logger.error({ error, docName }, "Failed to create WebRTC provider - P2P sync unavailable");
3811
+ }
3812
+ }
3813
+ return doc;
3814
+ }
3815
+ async function hasActiveConnections3(planId) {
3816
+ switch (currentMode) {
3817
+ case "hub":
3818
+ return hasActiveConnections2(planId);
3819
+ case "client":
3820
+ return await hasActiveConnections(planId);
3821
+ case "uninitialized":
3822
+ return false;
3823
+ }
3824
+ }
3825
+
3826
+ // src/hook-handlers.ts
3827
+ async function parseMarkdownToBlocks(markdown) {
3828
+ const editor = ServerBlockNoteEditor.create();
3829
+ return await editor.tryParseMarkdownToBlocks(markdown);
3830
+ }
3831
+ function extractTitleFromBlocks(blocks) {
3832
+ const UNTITLED = "Untitled Plan";
3833
+ const firstBlock = blocks[0];
3834
+ if (!firstBlock) return UNTITLED;
3835
+ const content = firstBlock.content;
3836
+ if (!content || !Array.isArray(content) || content.length === 0) {
3837
+ return UNTITLED;
3838
+ }
3839
+ const firstContent = content[0];
3840
+ if (!firstContent || typeof firstContent !== "object" || !("text" in firstContent)) {
3841
+ return UNTITLED;
3842
+ }
3843
+ const record = Object.fromEntries(Object.entries(firstContent));
3844
+ const text = record.text;
3845
+ if (typeof text !== "string") {
3846
+ return UNTITLED;
3847
+ }
3848
+ if (firstBlock.type === "heading") {
3849
+ return text;
3850
+ }
3851
+ return text.slice(0, 50);
3852
+ }
3853
+ async function createSessionHandler(input, ctx) {
3854
+ const existingSession = getSessionState(input.sessionId);
3855
+ if (existingSession) {
3856
+ const url2 = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, existingSession.planId);
3857
+ ctx.logger.info(
3858
+ { planId: existingSession.planId, sessionId: input.sessionId },
3859
+ "Returning existing session (idempotent)"
3860
+ );
3861
+ return { planId: existingSession.planId, url: url2 };
3862
+ }
3863
+ const planId = nanoid4();
3864
+ const now = Date.now();
3865
+ ctx.logger.info(
3866
+ { planId, sessionId: input.sessionId, agentType: input.agentType },
3867
+ "Creating plan from hook"
3868
+ );
3869
+ const PLAN_IN_PROGRESS = "Plan in progress...";
3870
+ const ownerId = await getGitHubUsername();
3871
+ ctx.logger.info({ ownerId }, "GitHub username for plan ownership");
3872
+ const repo = getRepositoryFullName() || void 0;
3873
+ if (repo) {
3874
+ ctx.logger.info({ repo }, "Auto-detected repository from current directory");
3875
+ }
3876
+ const ydoc = await ctx.getOrCreateDoc(planId);
3877
+ const origin = parseClaudeCodeOrigin(input.metadata) || {
3878
+ platform: "claude-code",
3879
+ sessionId: input.sessionId,
3880
+ transcriptPath: ""
3881
+ };
3882
+ initPlanMetadata(ydoc, {
3883
+ id: planId,
3884
+ title: PLAN_IN_PROGRESS,
3885
+ ownerId,
3886
+ repo,
3887
+ origin
3888
+ });
3889
+ setAgentPresence(ydoc, {
3890
+ agentType: input.agentType ?? "claude-code",
3891
+ sessionId: input.sessionId,
3892
+ connectedAt: now,
3893
+ lastSeenAt: now
3894
+ });
3895
+ if (origin && origin.platform === "claude-code") {
3896
+ const creator = typeof input.metadata?.ownerId === "string" ? input.metadata.ownerId : "unknown";
3897
+ const initialVersion = createInitialConversationVersion({
3898
+ versionId: nanoid4(),
3899
+ creator,
3900
+ platform: origin.platform,
3901
+ sessionId: origin.sessionId,
3902
+ messageCount: 0,
3903
+ createdAt: now
3904
+ });
3905
+ addConversationVersion(ydoc, initialVersion);
3906
+ ctx.logger.info(
3907
+ { planId, versionId: initialVersion.versionId },
3908
+ "Added initial conversation version"
3909
+ );
3910
+ }
3911
+ const indexDoc = await ctx.getOrCreateDoc(PLAN_INDEX_DOC_NAME);
3912
+ setPlanIndexEntry(indexDoc, {
3913
+ id: planId,
3914
+ title: PLAN_IN_PROGRESS,
3915
+ status: "draft",
3916
+ createdAt: now,
3917
+ updatedAt: now,
3918
+ ownerId,
3919
+ deleted: false
3920
+ });
3921
+ const url = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, planId);
3922
+ ctx.logger.info({ url }, "Plan URL generated");
3923
+ setSessionState(input.sessionId, {
3924
+ lifecycle: "created",
3925
+ planId,
3926
+ createdAt: now,
3927
+ lastSyncedAt: now
3928
+ });
3929
+ ctx.logger.info({ sessionId: input.sessionId, planId }, "Session registered in registry");
3930
+ if (await hasActiveConnections3(PLAN_INDEX_DOC_NAME)) {
3931
+ indexDoc.getMap("navigation").set("target", planId);
3932
+ ctx.logger.info({ url, planId }, "Browser already connected, navigating via CRDT");
3933
+ } else {
3934
+ await open(url);
3935
+ ctx.logger.info({ url }, "Browser launched by server");
3936
+ }
3937
+ return { planId, url };
3938
+ }
3939
+ async function updateContentHandler(planId, input, ctx) {
3940
+ ctx.logger.info(
3941
+ { planId, contentLength: input.content.length },
3942
+ "Updating plan content from hook"
3943
+ );
3944
+ const ydoc = await ctx.getOrCreateDoc(planId);
3945
+ const metadata = getPlanMetadata(ydoc);
3946
+ if (!metadata) {
3947
+ throw new TRPCError2({
3948
+ code: "NOT_FOUND",
3949
+ message: "Plan not found"
3950
+ });
3951
+ }
3952
+ const blocks = await parseMarkdownToBlocks(input.content);
3953
+ const title = extractTitleFromBlocks(blocks);
3954
+ const now = Date.now();
3955
+ const editor = ServerBlockNoteEditor.create();
3956
+ ydoc.transact(() => {
3957
+ const fragment = ydoc.getXmlFragment("document");
3958
+ while (fragment.length > 0) {
3959
+ fragment.delete(0, 1);
3960
+ }
3961
+ editor.blocksToYXmlFragment(blocks, fragment);
3962
+ const deliverablesArray = ydoc.getArray(YDOC_KEYS.DELIVERABLES);
3963
+ deliverablesArray.delete(0, deliverablesArray.length);
3964
+ const deliverables = extractDeliverables(blocks);
3965
+ for (const deliverable of deliverables) {
3966
+ addDeliverable(ydoc, deliverable);
3967
+ }
3968
+ if (deliverables.length > 0) {
3969
+ ctx.logger.info({ count: deliverables.length }, "Deliverables extracted from hook content");
3970
+ }
3971
+ });
3972
+ setPlanMetadata(ydoc, {
3973
+ title
3974
+ });
3975
+ const indexDoc = await ctx.getOrCreateDoc(PLAN_INDEX_DOC_NAME);
3976
+ if (metadata.ownerId) {
3977
+ setPlanIndexEntry(indexDoc, {
3978
+ id: planId,
3979
+ title,
3980
+ status: metadata.status,
3981
+ createdAt: metadata.createdAt ?? now,
3982
+ updatedAt: now,
3983
+ ownerId: metadata.ownerId,
3984
+ deleted: false
3985
+ });
3986
+ } else {
3987
+ ctx.logger.warn({ planId }, "Cannot update plan index: missing ownerId");
3988
+ }
3989
+ const sessionId = getSessionIdByPlanId(planId);
3990
+ if (sessionId) {
3991
+ const session = getSessionStateByPlanId(planId);
3992
+ if (session) {
3993
+ const contentHash = computeHash(input.content);
3994
+ switch (session.lifecycle) {
3995
+ case "created":
3996
+ case "approved_awaiting_token":
3997
+ setSessionState(sessionId, {
3998
+ ...session,
3999
+ planFilePath: input.filePath
4000
+ });
4001
+ break;
4002
+ case "synced":
4003
+ case "approved":
4004
+ case "reviewed":
4005
+ setSessionState(sessionId, {
4006
+ ...session,
4007
+ contentHash,
4008
+ planFilePath: input.filePath
4009
+ });
4010
+ break;
4011
+ default:
4012
+ assertNever(session);
4013
+ }
4014
+ ctx.logger.info(
4015
+ { planId, sessionId, contentHash, lifecycle: session.lifecycle },
4016
+ "Updated session registry with content hash"
4017
+ );
4018
+ }
4019
+ }
4020
+ ctx.logger.info({ planId, title, blockCount: blocks.length }, "Plan content updated");
4021
+ return { success: true, updatedAt: now };
4022
+ }
4023
+ async function getReviewStatusHandler(planId, ctx) {
4024
+ const ydoc = await ctx.getOrCreateDoc(planId);
4025
+ const metadata = getPlanMetadata(ydoc);
4026
+ if (!metadata) {
4027
+ throw new TRPCError2({
4028
+ code: "NOT_FOUND",
4029
+ message: "Plan not found"
4030
+ });
4031
+ }
4032
+ switch (metadata.status) {
4033
+ case "draft":
4034
+ return { status: "draft" };
4035
+ case "pending_review":
4036
+ return {
4037
+ status: "pending_review",
4038
+ reviewRequestId: metadata.reviewRequestId
4039
+ };
4040
+ case "changes_requested": {
4041
+ const threadsMap = ydoc.getMap(YDOC_KEYS.THREADS);
4042
+ const threadsData = threadsMap.toJSON();
4043
+ const threads = parseThreads(threadsData);
4044
+ const feedback = threads.map((thread) => ({
4045
+ threadId: thread.id,
4046
+ blockId: thread.selectedText,
4047
+ comments: thread.comments.map((c) => ({
4048
+ author: c.userId ?? "Reviewer",
4049
+ content: typeof c.body === "string" ? c.body : JSON.stringify(c.body),
4050
+ createdAt: c.createdAt ?? Date.now()
4051
+ }))
4052
+ }));
4053
+ return {
4054
+ status: "changes_requested",
4055
+ reviewedAt: metadata.reviewedAt,
4056
+ reviewedBy: metadata.reviewedBy,
4057
+ reviewComment: metadata.reviewComment,
4058
+ feedback: feedback.length > 0 ? feedback : void 0
4059
+ };
4060
+ }
4061
+ case "in_progress":
4062
+ return {
4063
+ status: "in_progress",
4064
+ reviewedAt: metadata.reviewedAt,
4065
+ reviewedBy: metadata.reviewedBy
4066
+ };
4067
+ case "completed":
4068
+ return {
4069
+ status: "completed",
4070
+ completedAt: metadata.completedAt,
4071
+ completedBy: metadata.completedBy,
4072
+ snapshotUrl: metadata.snapshotUrl
4073
+ };
4074
+ default:
4075
+ assertNever(metadata);
4076
+ }
4077
+ }
4078
+ async function updatePresenceHandler(planId, input, ctx) {
4079
+ const ydoc = await ctx.getOrCreateDoc(planId);
4080
+ const now = Date.now();
4081
+ setAgentPresence(ydoc, {
4082
+ agentType: input.agentType,
4083
+ sessionId: input.sessionId,
4084
+ connectedAt: now,
4085
+ lastSeenAt: now
4086
+ });
4087
+ return { success: true };
4088
+ }
4089
+ async function setSessionTokenHandler(planId, sessionTokenHash, ctx) {
4090
+ ctx.logger.info({ planId }, "Setting session token from hook");
4091
+ const ydoc = await ctx.getOrCreateDoc(planId);
4092
+ const metadata = getPlanMetadata(ydoc);
4093
+ if (!metadata) {
4094
+ throw new TRPCError2({
4095
+ code: "NOT_FOUND",
4096
+ message: "Plan not found"
4097
+ });
4098
+ }
4099
+ setPlanMetadata(ydoc, {
4100
+ sessionTokenHash
4101
+ });
4102
+ const url = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, planId);
4103
+ const session = getSessionStateByPlanId(planId);
4104
+ const sessionId = getSessionIdByPlanId(planId);
4105
+ if (session && sessionId) {
4106
+ switch (session.lifecycle) {
4107
+ case "created":
4108
+ setSessionState(sessionId, {
4109
+ lifecycle: "synced",
4110
+ planId: session.planId,
4111
+ planFilePath: session.planFilePath,
4112
+ createdAt: session.createdAt,
4113
+ lastSyncedAt: session.lastSyncedAt,
4114
+ contentHash: "",
4115
+ sessionToken: sessionTokenHash,
4116
+ url
4117
+ });
4118
+ ctx.logger.info({ planId, sessionId }, "Transitioned session to synced state");
4119
+ break;
4120
+ case "approved_awaiting_token":
4121
+ setSessionState(sessionId, {
4122
+ lifecycle: "approved",
4123
+ planId: session.planId,
4124
+ planFilePath: session.planFilePath,
4125
+ createdAt: session.createdAt,
4126
+ lastSyncedAt: session.lastSyncedAt,
4127
+ contentHash: "",
4128
+ sessionToken: sessionTokenHash,
4129
+ url,
4130
+ approvedAt: session.approvedAt,
4131
+ deliverables: session.deliverables,
4132
+ reviewComment: session.reviewComment,
4133
+ reviewedBy: session.reviewedBy
4134
+ });
4135
+ ctx.logger.info(
4136
+ { planId, sessionId },
4137
+ "Transitioned session from approved_awaiting_token to approved"
4138
+ );
4139
+ break;
4140
+ case "synced":
4141
+ case "approved":
4142
+ case "reviewed":
4143
+ setSessionState(sessionId, {
4144
+ ...session,
4145
+ sessionToken: sessionTokenHash,
4146
+ url
4147
+ });
4148
+ ctx.logger.info({ planId, sessionId }, "Updated session token");
4149
+ break;
4150
+ default:
4151
+ assertNever(session);
4152
+ }
4153
+ }
4154
+ ctx.logger.info({ planId }, "Session token set successfully");
4155
+ return { url };
4156
+ }
4157
+ async function waitForApprovalHandler(planId, _reviewRequestIdParam, ctx) {
4158
+ let ydoc;
4159
+ try {
4160
+ ydoc = await ctx.getOrCreateDoc(planId);
4161
+ } catch (err) {
4162
+ ctx.logger.error({ err, planId }, "Failed to get or create doc for approval waiting");
4163
+ throw err;
4164
+ }
4165
+ const metadata = ydoc.getMap(YDOC_KEYS.METADATA);
4166
+ const planMetadata = getPlanMetadata(ydoc);
4167
+ const ownerId = planMetadata?.ownerId ?? "unknown";
4168
+ let reviewRequestId;
4169
+ if (planMetadata?.status === "pending_review" && planMetadata.reviewRequestId) {
4170
+ reviewRequestId = planMetadata.reviewRequestId;
4171
+ ctx.logger.info(
4172
+ { planId, currentStatus: planMetadata.status, reviewRequestId },
4173
+ "Status already pending_review, reusing existing reviewRequestId for observer"
4174
+ );
4175
+ } else {
4176
+ reviewRequestId = nanoid4();
4177
+ const result = transitionPlanStatus(
4178
+ ydoc,
4179
+ {
4180
+ status: "pending_review",
4181
+ reviewRequestId
4182
+ },
4183
+ ownerId
4184
+ );
4185
+ if (!result.success) {
4186
+ ctx.logger.error({ planId, error: result.error }, "Failed to transition to pending_review");
4187
+ }
4188
+ }
4189
+ ctx.logger.info(
4190
+ { planId, reviewRequestId },
4191
+ "[SERVER OBSERVER] Set reviewRequestId and status, starting observation"
4192
+ );
4193
+ const getReviewData = () => {
4194
+ const meta = getPlanMetadata(ydoc);
4195
+ if (meta?.status === "changes_requested" || meta?.status === "in_progress") {
4196
+ return {
4197
+ reviewComment: meta.reviewComment,
4198
+ reviewedBy: meta.reviewedBy
4199
+ };
4200
+ }
4201
+ return {
4202
+ reviewComment: void 0,
4203
+ reviewedBy: void 0
4204
+ };
4205
+ };
4206
+ const updateSessionRegistry = (status, extraData = {}) => {
4207
+ const sessionData = getSessionData();
4208
+ if (!sessionData) return;
4209
+ const { session, sessionId } = sessionData;
4210
+ validateSessionStateForTransition(session);
4211
+ const baseState = buildBaseState(session);
4212
+ const syncedFields = buildSyncedFields(session);
4213
+ const { reviewComment, reviewedBy } = getReviewData();
4214
+ if (status === "in_progress") {
4215
+ handleApprovedTransition(
4216
+ sessionId,
4217
+ baseState,
4218
+ syncedFields,
4219
+ extraData,
4220
+ reviewComment,
4221
+ reviewedBy
4222
+ );
4223
+ } else if (status === "changes_requested") {
4224
+ handleReviewedTransition(
4225
+ sessionId,
4226
+ baseState,
4227
+ syncedFields,
4228
+ session,
4229
+ extraData,
4230
+ reviewComment,
4231
+ reviewedBy
4232
+ );
4233
+ } else {
4234
+ throw new Error(
4235
+ `Invalid session state transition: missing required fields. status=${status}, hasApprovedAt=${!!extraData.approvedAt}, hasDeliverables=${!!extraData.deliverables}, hasReviewedBy=${!!reviewedBy}`
4236
+ );
4237
+ }
4238
+ logRegistryUpdate(status, extraData);
4239
+ };
4240
+ const getSessionData = () => {
4241
+ const session = getSessionStateByPlanId(planId);
4242
+ const sessionId = getSessionIdByPlanId(planId);
4243
+ if (!session || !sessionId) {
4244
+ ctx.logger.warn(
4245
+ { planId },
4246
+ "Session not found in registry during approval - post-exit injection will not work"
4247
+ );
4248
+ return null;
4249
+ }
4250
+ return { session, sessionId };
4251
+ };
4252
+ const validateSessionStateForTransition = (_session) => {
4253
+ };
4254
+ const buildBaseState = (session) => ({
4255
+ planId: session.planId,
4256
+ planFilePath: session.planFilePath,
4257
+ createdAt: session.createdAt,
4258
+ lastSyncedAt: session.lastSyncedAt
4259
+ });
4260
+ const buildSyncedFields = (session) => {
4261
+ if (isSessionStateSynced(session) || isSessionStateApproved(session) || isSessionStateReviewed(session)) {
4262
+ return {
4263
+ contentHash: session.contentHash,
4264
+ sessionToken: session.sessionToken,
4265
+ url: session.url
4266
+ };
4267
+ }
4268
+ return null;
4269
+ };
4270
+ const handleApprovedTransition = (sessionId, baseState, syncedFields, extraData, reviewComment, reviewedBy) => {
4271
+ if (!extraData.approvedAt || !extraData.deliverables) {
4272
+ throw new Error(
4273
+ `Invalid session state transition: missing required fields for approval. hasApprovedAt=${!!extraData.approvedAt}, hasDeliverables=${!!extraData.deliverables}`
4274
+ );
4275
+ }
4276
+ const webUrl = webConfig.SHIPYARD_WEB_URL;
4277
+ if (syncedFields) {
4278
+ setSessionState(sessionId, {
4279
+ lifecycle: "approved",
4280
+ ...baseState,
4281
+ ...syncedFields,
4282
+ approvedAt: extraData.approvedAt,
4283
+ deliverables: extraData.deliverables,
4284
+ reviewComment,
4285
+ reviewedBy
4286
+ });
4287
+ } else {
4288
+ setSessionState(sessionId, {
4289
+ lifecycle: "approved_awaiting_token",
4290
+ ...baseState,
4291
+ url: createPlanWebUrl(webUrl, baseState.planId),
4292
+ approvedAt: extraData.approvedAt,
4293
+ deliverables: extraData.deliverables,
4294
+ reviewComment,
4295
+ reviewedBy
4296
+ });
4297
+ }
4298
+ };
4299
+ const handleReviewedTransition = (sessionId, baseState, syncedFields, session, extraData, reviewComment, reviewedBy) => {
4300
+ if (!reviewedBy) {
4301
+ throw new Error(`Invalid session state transition: missing reviewedBy for changes_requested`);
4302
+ }
4303
+ const deliverables = extraData.deliverables || (isSessionStateApproved(session) || isSessionStateReviewed(session) || isSessionStateApprovedAwaitingToken(session) ? session.deliverables : []);
4304
+ const webUrl = webConfig.SHIPYARD_WEB_URL;
4305
+ setSessionState(sessionId, {
4306
+ lifecycle: "reviewed",
4307
+ ...baseState,
4308
+ contentHash: syncedFields?.contentHash ?? "",
4309
+ sessionToken: syncedFields?.sessionToken ?? "",
4310
+ url: syncedFields?.url ?? createPlanWebUrl(webUrl, baseState.planId),
4311
+ deliverables,
4312
+ reviewComment: reviewComment || "",
4313
+ reviewedBy,
4314
+ reviewStatus: "changes_requested"
4315
+ });
4316
+ };
4317
+ const logRegistryUpdate = (status, extraData) => {
4318
+ const sessionId = getSessionIdByPlanId(planId);
4319
+ ctx.logger.info(
4320
+ {
4321
+ planId,
4322
+ sessionId,
4323
+ ...extraData.deliverables && { deliverableCount: extraData.deliverables.length }
4324
+ },
4325
+ `Stored ${status === "in_progress" ? "approval" : "rejection"} data in session registry`
4326
+ );
4327
+ };
4328
+ const handleApproved = () => {
4329
+ const deliverables = getDeliverables(ydoc);
4330
+ const deliverableInfos = deliverables.map((d) => ({ id: d.id, text: d.text }));
4331
+ updateSessionRegistry("in_progress", {
4332
+ approvedAt: Date.now(),
4333
+ deliverables: deliverableInfos
4334
+ });
4335
+ const { reviewComment, reviewedBy } = getReviewData();
4336
+ ctx.logger.info(
4337
+ { planId, reviewRequestId, reviewedBy },
4338
+ "[SERVER OBSERVER] Plan approved via Y.Doc - resolving promise"
4339
+ );
4340
+ return {
4341
+ approved: true,
4342
+ deliverables,
4343
+ reviewComment,
4344
+ reviewedBy: reviewedBy || "unknown",
4345
+ status: "in_progress"
4346
+ };
4347
+ };
4348
+ const handleChangesRequested = () => {
4349
+ updateSessionRegistry("changes_requested");
4350
+ const feedback = extractFeedbackFromYDoc(ydoc, ctx);
4351
+ const { reviewComment, reviewedBy } = getReviewData();
4352
+ ctx.logger.info(
4353
+ { planId, reviewRequestId, feedback },
4354
+ "[SERVER OBSERVER] Changes requested via Y.Doc"
4355
+ );
4356
+ return {
4357
+ approved: false,
4358
+ feedback: feedback || "Changes requested",
4359
+ status: "changes_requested",
4360
+ reviewComment,
4361
+ reviewedBy
4362
+ };
4363
+ };
4364
+ return new Promise((resolve2, reject) => {
4365
+ const APPROVAL_TIMEOUT_MS = 30 * 60 * 1e3;
4366
+ let timeout = null;
4367
+ let checkStatus = null;
4368
+ const shouldProcessStatusChange = () => {
4369
+ const currentMeta = getPlanMetadata(ydoc);
4370
+ if (!currentMeta) return false;
4371
+ const currentReviewId = currentMeta.status === "pending_review" ? currentMeta.reviewRequestId : void 0;
4372
+ const status = currentMeta.status;
4373
+ if (currentReviewId !== reviewRequestId && currentMeta.status === "pending_review") {
4374
+ ctx.logger.warn(
4375
+ { planId, expected: reviewRequestId, actual: currentReviewId, status },
4376
+ "[SERVER OBSERVER] Review ID mismatch, ignoring status change"
4377
+ );
4378
+ return false;
4379
+ }
4380
+ const isTerminalState = status === "in_progress" || status === "changes_requested";
4381
+ return isTerminalState;
4382
+ };
4383
+ const cleanupObserver = () => {
4384
+ if (timeout) clearTimeout(timeout);
4385
+ if (checkStatus) metadata.unobserve(checkStatus);
4386
+ };
4387
+ try {
4388
+ timeout = setTimeout(() => {
4389
+ if (checkStatus) {
4390
+ metadata.unobserve(checkStatus);
4391
+ }
4392
+ resolve2({
4393
+ approved: false,
4394
+ feedback: "Review timeout - no decision received in 30 minutes",
4395
+ status: "timeout"
4396
+ });
4397
+ }, APPROVAL_TIMEOUT_MS);
4398
+ checkStatus = () => {
4399
+ const currentMeta = getPlanMetadata(ydoc);
4400
+ const status = currentMeta?.status;
4401
+ const currentReviewId = currentMeta?.status === "pending_review" ? currentMeta.reviewRequestId : void 0;
4402
+ ctx.logger.debug(
4403
+ { planId, status, currentReviewId, expectedReviewId: reviewRequestId },
4404
+ "[SERVER OBSERVER] Metadata changed, checking status"
4405
+ );
4406
+ if (!shouldProcessStatusChange()) return;
4407
+ cleanupObserver();
4408
+ resolve2(status === "in_progress" ? handleApproved() : handleChangesRequested());
4409
+ };
4410
+ ctx.logger.info(
4411
+ { planId, reviewRequestId },
4412
+ "[SERVER OBSERVER] Registering metadata observer"
4413
+ );
4414
+ metadata.observe((event) => {
4415
+ ctx.logger.info(
4416
+ {
4417
+ planId,
4418
+ reviewRequestId,
4419
+ keysChanged: Array.from(event.keysChanged),
4420
+ target: event.target.constructor.name
4421
+ },
4422
+ "[SERVER OBSERVER] *** METADATA MAP CHANGED *** (Raw Y.Map observer)"
4423
+ );
4424
+ });
4425
+ metadata.observe(checkStatus);
4426
+ checkStatus();
4427
+ } catch (err) {
4428
+ if (timeout) clearTimeout(timeout);
4429
+ if (checkStatus) {
4430
+ try {
4431
+ metadata.unobserve(checkStatus);
4432
+ } catch (unobserveErr) {
4433
+ ctx.logger.warn({ err: unobserveErr }, "Failed to unobserve during error cleanup");
4434
+ }
4435
+ }
4436
+ ctx.logger.error({ err, planId }, "Failed to setup approval observer");
4437
+ reject(err);
4438
+ }
4439
+ });
4440
+ }
4441
+ function extractFeedbackFromYDoc(ydoc, ctx) {
4442
+ try {
4443
+ const planMeta = getPlanMetadata(ydoc);
4444
+ const reviewComment = planMeta?.status === "changes_requested" || planMeta?.status === "in_progress" ? planMeta.reviewComment : void 0;
4445
+ const reviewedBy = planMeta?.status === "changes_requested" || planMeta?.status === "in_progress" ? planMeta.reviewedBy : void 0;
4446
+ const threadsMap = ydoc.getMap(YDOC_KEYS.THREADS);
4447
+ const threadsData = threadsMap.toJSON();
4448
+ const threads = parseThreads(threadsData);
4449
+ if (!reviewComment && threads.length === 0) {
4450
+ return "Changes requested. Check the plan for reviewer comments.";
4451
+ }
4452
+ const contentFragment = ydoc.getXmlFragment(YDOC_KEYS.DOCUMENT_FRAGMENT);
4453
+ const fragmentJson = contentFragment.toJSON();
4454
+ let planText = "";
4455
+ if (Array.isArray(fragmentJson)) {
4456
+ planText = fragmentJson.map((block) => {
4457
+ if (!block || typeof block !== "object") return "";
4458
+ const blockRecord = Object.fromEntries(Object.entries(block));
4459
+ const content = blockRecord.content;
4460
+ if (!Array.isArray(content)) return "";
4461
+ return content.map((item) => {
4462
+ if (!item || typeof item !== "object") return "";
4463
+ const itemRecord = Object.fromEntries(Object.entries(item));
4464
+ const text = itemRecord.text;
4465
+ return typeof text === "string" ? text : "";
4466
+ }).join("");
4467
+ }).filter(Boolean).join("\n");
4468
+ }
4469
+ const resolveUser = createUserResolver(ydoc);
4470
+ const feedbackText = formatThreadsForLLM(threads, {
4471
+ includeResolved: false,
4472
+ selectedTextMaxLength: 100,
4473
+ resolveUser
4474
+ });
4475
+ let output = "Changes requested:\n\n";
4476
+ if (planText) {
4477
+ output += "## Current Plan\n\n";
4478
+ output += planText;
4479
+ output += "\n\n---\n\n";
4480
+ }
4481
+ if (reviewComment) {
4482
+ output += "## Reviewer Comment\n\n";
4483
+ output += `> **${reviewedBy ?? "Reviewer"}:** ${reviewComment}
4484
+ `;
4485
+ output += "\n---\n\n";
4486
+ }
4487
+ if (feedbackText) {
4488
+ output += "## Inline Feedback\n\n";
4489
+ output += feedbackText;
4490
+ }
4491
+ const deliverables = getDeliverables(ydoc);
4492
+ const deliverablesText = formatDeliverablesForLLM(deliverables);
4493
+ if (deliverablesText) {
4494
+ output += "\n\n---\n\n";
4495
+ output += deliverablesText;
4496
+ }
4497
+ return output;
4498
+ } catch (err) {
4499
+ ctx.logger.warn({ err }, "Failed to extract feedback from Y.Doc");
4500
+ return "Changes requested. Check the plan for reviewer comments.";
4501
+ }
4502
+ }
4503
+ async function getDeliverableContextHandler(planId, sessionToken, ctx) {
4504
+ const ydoc = await ctx.getOrCreateDoc(planId);
4505
+ const metadata = getPlanMetadata(ydoc);
4506
+ if (!metadata) {
4507
+ throw new TRPCError2({
4508
+ code: "NOT_FOUND",
4509
+ message: "Plan not found"
4510
+ });
4511
+ }
4512
+ const deliverables = getDeliverables(ydoc);
4513
+ const url = createPlanWebUrl(webConfig.SHIPYARD_WEB_URL, planId);
4514
+ let deliverablesSection = "";
4515
+ if (deliverables.length > 0) {
4516
+ deliverablesSection = `
4517
+ ## Deliverables
4518
+
4519
+ Attach proof to each deliverable using add_artifact:
4520
+
4521
+ `;
4522
+ for (const d of deliverables) {
4523
+ deliverablesSection += `- ${d.text}
4524
+ deliverableId="${d.id}"
4525
+ `;
4526
+ }
4527
+ } else {
4528
+ deliverablesSection = `
4529
+ ## Deliverables
4530
+
4531
+ No deliverables marked in this plan. You can still upload artifacts without linking them.`;
4532
+ }
4533
+ let feedbackSection = "";
4534
+ if (metadata.status === "changes_requested" && metadata.reviewComment?.trim()) {
4535
+ feedbackSection = `
4536
+ ## Reviewer Feedback
4537
+
4538
+ ${metadata.reviewedBy ? `**From:** ${metadata.reviewedBy}
4539
+
4540
+ ` : ""}${metadata.reviewComment}
4541
+
4542
+ `;
4543
+ }
4544
+ const approvalMessage = metadata.status === "changes_requested" ? "[SHIPYARD] Changes requested on your plan \u26A0\uFE0F" : "[SHIPYARD] Plan approved! \u{1F389}";
4545
+ const context = `${approvalMessage}
4546
+ ${deliverablesSection}${feedbackSection}
4547
+ ## Session Info
4548
+
4549
+ planId="${planId}"
4550
+ sessionToken="${sessionToken}"
4551
+ url="${url}"
4552
+
4553
+ ## How to Attach Proof
4554
+
4555
+ For each deliverable above, call:
4556
+ \`\`\`
4557
+ add_artifact(
4558
+ planId="${planId}",
4559
+ sessionToken="${sessionToken}",
4560
+ type="image",
4561
+ filePath="/path/to/file.png",
4562
+ deliverableId="<id from above>"
4563
+ )
4564
+ \`\`\`
4565
+
4566
+ When the LAST deliverable gets an artifact, the task auto-completes and returns a snapshot URL for your PR.`;
4567
+ return { context };
4568
+ }
4569
+ async function getSessionContextHandler(sessionId, ctx) {
4570
+ ctx.logger.info({ sessionId }, "Getting session context for post-exit injection");
4571
+ const sessionState = getSessionState(sessionId);
4572
+ if (!sessionState) {
4573
+ ctx.logger.warn({ sessionId }, "Session not found in registry");
4574
+ return { found: false };
4575
+ }
4576
+ if (isSessionStateApproved(sessionState)) {
4577
+ ctx.logger.info(
4578
+ { sessionId, planId: sessionState.planId },
4579
+ "Session context retrieved (approved state, idempotent)"
4580
+ );
4581
+ return {
4582
+ found: true,
4583
+ planId: sessionState.planId,
4584
+ sessionToken: sessionState.sessionToken,
4585
+ url: sessionState.url,
4586
+ deliverables: sessionState.deliverables,
4587
+ reviewComment: sessionState.reviewComment,
4588
+ reviewedBy: sessionState.reviewedBy
4589
+ };
4590
+ }
4591
+ if (isSessionStateReviewed(sessionState)) {
4592
+ ctx.logger.info(
4593
+ { sessionId, planId: sessionState.planId },
4594
+ "Session context retrieved (reviewed state, idempotent)"
4595
+ );
4596
+ return {
4597
+ found: true,
4598
+ planId: sessionState.planId,
4599
+ sessionToken: sessionState.sessionToken,
4600
+ url: sessionState.url,
4601
+ deliverables: sessionState.deliverables,
4602
+ reviewComment: sessionState.reviewComment,
4603
+ reviewedBy: sessionState.reviewedBy,
4604
+ reviewStatus: sessionState.reviewStatus
4605
+ };
4606
+ }
4607
+ ctx.logger.warn(
4608
+ { sessionId, lifecycle: sessionState.lifecycle },
4609
+ "Session not ready for post-exit injection"
4610
+ );
4611
+ return { found: false };
4612
+ }
4613
+ function createHookHandlers() {
4614
+ return {
4615
+ createSession: (input, ctx) => createSessionHandler(input, ctx),
4616
+ updateContent: (planId, input, ctx) => updateContentHandler(planId, input, ctx),
4617
+ getReviewStatus: (planId, ctx) => getReviewStatusHandler(planId, ctx),
4618
+ updatePresence: (planId, input, ctx) => updatePresenceHandler(planId, input, ctx),
4619
+ setSessionToken: (planId, sessionTokenHash, ctx) => setSessionTokenHandler(planId, sessionTokenHash, ctx),
4620
+ waitForApproval: (planId, reviewRequestId, ctx) => waitForApprovalHandler(planId, reviewRequestId, ctx),
4621
+ getDeliverableContext: (planId, sessionToken, ctx) => getDeliverableContextHandler(planId, sessionToken, ctx),
4622
+ getSessionContext: (sessionId, ctx) => getSessionContextHandler(sessionId, ctx)
4623
+ };
4624
+ }
4625
+
4626
+ // src/subscriptions/manager.ts
4627
+ import { nanoid as nanoid5 } from "nanoid";
4628
+ var subscriptions = /* @__PURE__ */ new Map();
4629
+ var SUBSCRIPTION_TTL_MS = 5 * 60 * 1e3;
4630
+ function createSubscription(config) {
4631
+ const id = nanoid5();
4632
+ const now = Date.now();
4633
+ const subscription = {
4634
+ id,
4635
+ config,
4636
+ pendingChanges: [],
4637
+ windowStartedAt: null,
4638
+ lastFlushedAt: now,
4639
+ lastActivityAt: now,
4640
+ ready: false
4641
+ };
4642
+ let planSubs = subscriptions.get(config.planId);
4643
+ if (!planSubs) {
4644
+ planSubs = /* @__PURE__ */ new Map();
4645
+ subscriptions.set(config.planId, planSubs);
4646
+ }
4647
+ planSubs.set(id, subscription);
4648
+ logger.info(
4649
+ { planId: config.planId, subscriptionId: id, subscribe: config.subscribe },
4650
+ "Subscription created"
4651
+ );
4652
+ return id;
4653
+ }
4654
+ function deleteSubscription(planId, subscriptionId) {
4655
+ const deleted = subscriptions.get(planId)?.delete(subscriptionId) ?? false;
4656
+ if (deleted) {
4657
+ logger.info({ planId, subscriptionId }, "Subscription deleted");
4658
+ if (subscriptions.get(planId)?.size === 0) {
4659
+ subscriptions.delete(planId);
4660
+ }
4661
+ }
4662
+ return deleted;
4663
+ }
4664
+ function notifyChange(planId, change) {
4665
+ const planSubs = subscriptions.get(planId);
4666
+ if (!planSubs) return;
4667
+ const now = Date.now();
4668
+ for (const sub of planSubs.values()) {
4669
+ if (!sub.config.subscribe.includes(change.type)) continue;
4670
+ sub.pendingChanges.push(change);
4671
+ sub.lastActivityAt = now;
4672
+ if (sub.windowStartedAt === null) {
4673
+ sub.windowStartedAt = now;
4674
+ }
4675
+ checkFlushConditions(sub);
4676
+ }
4677
+ logger.debug(
4678
+ { planId, changeType: change.type, subscriberCount: planSubs.size },
4679
+ "Change notified"
4680
+ );
4681
+ }
4682
+ function getChanges(planId, subscriptionId) {
4683
+ const sub = subscriptions.get(planId)?.get(subscriptionId);
4684
+ if (!sub) return null;
4685
+ const now = Date.now();
4686
+ sub.lastActivityAt = now;
4687
+ checkFlushConditions(sub);
4688
+ if (!sub.ready) {
4689
+ return {
4690
+ ready: false,
4691
+ pending: sub.pendingChanges.length,
4692
+ windowExpiresIn: sub.windowStartedAt ? Math.max(0, sub.config.windowMs - (now - sub.windowStartedAt)) : sub.config.windowMs
4693
+ };
4694
+ }
4695
+ const changes = sub.pendingChanges;
4696
+ const summary = summarizeChanges(changes);
4697
+ sub.pendingChanges = [];
4698
+ sub.windowStartedAt = null;
4699
+ sub.lastFlushedAt = now;
4700
+ sub.ready = false;
4701
+ logger.debug({ planId, subscriptionId, changeCount: changes.length }, "Changes flushed");
4702
+ return {
4703
+ ready: true,
4704
+ changes: summary,
4705
+ details: changes
4706
+ };
4707
+ }
4708
+ function startCleanupInterval() {
4709
+ startPeriodicCleanup();
4710
+ setInterval(() => {
4711
+ const now = Date.now();
4712
+ let cleanedCount = 0;
4713
+ for (const [planId, planSubs] of subscriptions.entries()) {
4714
+ for (const [subId, sub] of planSubs.entries()) {
4715
+ if (now - sub.lastActivityAt > SUBSCRIPTION_TTL_MS) {
4716
+ planSubs.delete(subId);
4717
+ cleanedCount++;
4718
+ }
4719
+ }
4720
+ if (planSubs.size === 0) {
4721
+ subscriptions.delete(planId);
4722
+ }
4723
+ }
4724
+ if (cleanedCount > 0) {
4725
+ logger.info({ cleanedCount }, "Cleaned up stale subscriptions");
4726
+ }
4727
+ }, 6e4);
4728
+ }
4729
+ function checkFlushConditions(sub) {
4730
+ const now = Date.now();
4731
+ const { windowMs, maxWindowMs, threshold } = sub.config;
4732
+ if (sub.pendingChanges.length >= threshold) {
4733
+ sub.ready = true;
4734
+ return;
4735
+ }
4736
+ if (sub.windowStartedAt && now - sub.windowStartedAt >= windowMs) {
4737
+ sub.ready = true;
4738
+ return;
4739
+ }
4740
+ if (now - sub.lastFlushedAt >= maxWindowMs && sub.pendingChanges.length > 0) {
4741
+ sub.ready = true;
4742
+ }
4743
+ }
4744
+ function summarizeChanges(changes) {
4745
+ const parts = [];
4746
+ const statusChanges = changes.filter((c) => c.type === "status");
4747
+ if (statusChanges.length > 0) {
4748
+ const latest = statusChanges[statusChanges.length - 1];
4749
+ if (latest) {
4750
+ parts.push(`Status: ${latest.details?.newValue}`);
4751
+ }
4752
+ }
4753
+ const commentChanges = changes.filter((c) => c.type === "comments");
4754
+ if (commentChanges.length > 0) {
4755
+ const totalAdded = commentChanges.reduce((acc, c) => {
4756
+ const added = c.details?.added;
4757
+ return acc + (typeof added === "number" ? added : 1);
4758
+ }, 0);
4759
+ parts.push(`${totalAdded} new comment(s)`);
4760
+ }
4761
+ const resolvedChanges = changes.filter((c) => c.type === "resolved");
4762
+ if (resolvedChanges.length > 0) {
4763
+ const totalResolved = resolvedChanges.reduce((acc, c) => {
4764
+ const resolved = c.details?.resolved;
4765
+ return acc + (typeof resolved === "number" ? resolved : 1);
4766
+ }, 0);
4767
+ parts.push(`${totalResolved} resolved`);
4768
+ }
4769
+ const contentChanges = changes.filter((c) => c.type === "content");
4770
+ if (contentChanges.length > 0) {
4771
+ parts.push("Content updated");
4772
+ }
4773
+ const artifactChanges = changes.filter((c) => c.type === "artifacts");
4774
+ if (artifactChanges.length > 0) {
4775
+ const totalAdded = artifactChanges.reduce((acc, c) => {
4776
+ const added = c.details?.added;
4777
+ return acc + (typeof added === "number" ? added : 1);
4778
+ }, 0);
4779
+ parts.push(`${totalAdded} artifact(s) added`);
4780
+ }
4781
+ return parts.join(" | ") || "No changes";
4782
+ }
4783
+
4784
+ // src/subscriptions/observers.ts
4785
+ var previousState = /* @__PURE__ */ new Map();
4786
+ var lastContentEdit = /* @__PURE__ */ new Map();
4787
+ var CONTENT_EDIT_DEBOUNCE_MS = 5e3;
4788
+ function attachObservers(planId, doc) {
4789
+ const metadata = getPlanMetadata(doc);
4790
+ const threadsMap = doc.getMap(YDOC_KEYS.THREADS);
4791
+ const threads = parseThreads(threadsMap.toJSON());
4792
+ const deliverables = getDeliverables(doc);
4793
+ const allFulfilled = deliverables.length > 0 && deliverables.every((d) => d.linkedArtifactId);
4794
+ const initialCommentIds = /* @__PURE__ */ new Set();
4795
+ for (const thread of threads) {
4796
+ for (const comment of thread.comments) {
4797
+ initialCommentIds.add(comment.id);
4798
+ }
4799
+ }
4800
+ previousState.set(planId, {
4801
+ status: metadata?.status,
4802
+ commentCount: threads.reduce((acc, t2) => acc + t2.comments.length, 0),
4803
+ resolvedCount: threads.filter((t2) => t2.resolved).length,
4804
+ contentLength: doc.getXmlFragment("document").length,
4805
+ artifactCount: doc.getArray(YDOC_KEYS.ARTIFACTS).length,
4806
+ deliverablesFulfilled: allFulfilled,
4807
+ commentIds: initialCommentIds
4808
+ });
4809
+ logger.debug({ planId }, "Attached observers to plan");
4810
+ doc.getMap(YDOC_KEYS.METADATA).observe((event, transaction) => {
4811
+ if (event.keysChanged.has("status")) {
4812
+ let isValidStatus2 = function(s) {
4813
+ return typeof s === "string" && (s === "draft" || s === "pending_review" || s === "changes_requested" || s === "in_progress" || s === "completed");
4814
+ };
4815
+ var isValidStatus = isValidStatus2;
4816
+ const prev = previousState.get(planId);
4817
+ const rawStatus = doc.getMap(YDOC_KEYS.METADATA).get("status");
4818
+ const newStatus = isValidStatus2(rawStatus) ? rawStatus : void 0;
4819
+ if (prev?.status && prev.status !== newStatus && newStatus) {
4820
+ const actor = transaction.origin?.actor || "System";
4821
+ logPlanEvent(doc, "status_changed", actor, {
4822
+ fromStatus: prev.status,
4823
+ toStatus: newStatus
4824
+ });
4825
+ const change = {
4826
+ type: "status",
4827
+ timestamp: Date.now(),
4828
+ summary: `Status changed to ${newStatus}`,
4829
+ details: { oldValue: prev.status, newValue: newStatus }
4830
+ };
4831
+ notifyChange(planId, change);
4832
+ prev.status = newStatus;
4833
+ logger.debug({ planId, oldStatus: prev.status, newStatus }, "Status change detected");
4834
+ }
4835
+ }
4836
+ });
4837
+ doc.getMap(YDOC_KEYS.THREADS).observeDeep((_events, transaction) => {
4838
+ const prev = previousState.get(planId);
4839
+ if (!prev) return;
4840
+ const actor = transaction.origin?.actor || "System";
4841
+ const threadsMap2 = doc.getMap(YDOC_KEYS.THREADS);
4842
+ const threads2 = parseThreads(threadsMap2.toJSON());
4843
+ handleNewComments(doc, planId, threads2, prev, actor);
4844
+ handleResolvedComments(doc, planId, threads2, prev, actor);
4845
+ });
4846
+ doc.getXmlFragment("document").observeDeep((_events, transaction) => {
4847
+ const now = Date.now();
4848
+ const lastEdit = lastContentEdit.get(planId) || 0;
4849
+ if (now - lastEdit > CONTENT_EDIT_DEBOUNCE_MS) {
4850
+ const actor = transaction.origin?.actor || "System";
4851
+ logPlanEvent(doc, "content_edited", actor);
4852
+ lastContentEdit.set(planId, now);
4853
+ }
4854
+ notifyChange(planId, {
4855
+ type: "content",
4856
+ timestamp: Date.now(),
4857
+ summary: "Content updated"
4858
+ });
4859
+ logger.debug({ planId }, "Content change detected");
4860
+ });
4861
+ doc.getArray(YDOC_KEYS.ARTIFACTS).observe((_event, transaction) => {
4862
+ const prev = previousState.get(planId);
4863
+ if (!prev) return;
4864
+ const actor = transaction.origin?.actor || "System";
4865
+ const newCount = doc.getArray(YDOC_KEYS.ARTIFACTS).length;
4866
+ if (newCount > prev.artifactCount) {
4867
+ const diff = newCount - prev.artifactCount;
4868
+ const artifacts = doc.getArray(YDOC_KEYS.ARTIFACTS).toArray();
4869
+ const lastArtifact = artifacts[artifacts.length - 1];
4870
+ const artifactId = lastArtifact && typeof lastArtifact === "object" && "id" in lastArtifact ? String(lastArtifact.id) : "unknown";
4871
+ logPlanEvent(doc, "artifact_uploaded", actor, {
4872
+ artifactId
4873
+ });
4874
+ notifyChange(planId, {
4875
+ type: "artifacts",
4876
+ timestamp: Date.now(),
4877
+ summary: `${diff} artifact(s) added`,
4878
+ details: { added: diff }
4879
+ });
4880
+ prev.artifactCount = newCount;
4881
+ logger.debug({ planId, added: diff }, "Artifacts added detected");
4882
+ }
4883
+ });
4884
+ doc.getArray(YDOC_KEYS.DELIVERABLES).observeDeep((_events, transaction) => {
4885
+ const prev = previousState.get(planId);
4886
+ if (!prev) return;
4887
+ const deliverables2 = getDeliverables(doc);
4888
+ const allFulfilled2 = deliverables2.length > 0 && deliverables2.every((d) => d.linkedArtifactId);
4889
+ if (allFulfilled2 && !prev.deliverablesFulfilled) {
4890
+ const actor = transaction.origin?.actor || "System";
4891
+ logPlanEvent(
4892
+ doc,
4893
+ "deliverable_linked",
4894
+ actor,
4895
+ {
4896
+ allFulfilled: true
4897
+ },
4898
+ {
4899
+ inboxWorthy: true,
4900
+ inboxFor: "owner"
4901
+ }
4902
+ );
4903
+ prev.deliverablesFulfilled = true;
4904
+ logger.debug({ planId }, "All deliverables fulfilled - inbox-worthy event logged");
4905
+ }
4906
+ });
4907
+ }
4908
+ function detectNewComments(threads, prevCommentIds) {
4909
+ const newComments = [];
4910
+ for (const thread of threads) {
4911
+ for (const comment of thread.comments) {
4912
+ if (!prevCommentIds.has(comment.id)) {
4913
+ newComments.push(comment);
4914
+ }
4915
+ }
4916
+ }
4917
+ return newComments;
4918
+ }
4919
+ function logCommentWithMentions(doc, planId, comment, actor) {
4920
+ const mentions = extractMentions(comment.body);
4921
+ const hasMentions = mentions.length > 0;
4922
+ logPlanEvent(
4923
+ doc,
4924
+ "comment_added",
4925
+ actor,
4926
+ { commentId: comment.id, mentions: hasMentions },
4927
+ {
4928
+ inboxWorthy: hasMentions,
4929
+ inboxFor: hasMentions ? mentions : void 0
4930
+ }
4931
+ );
4932
+ if (hasMentions) {
4933
+ logger.debug(
4934
+ { planId, commentId: comment.id, mentions },
4935
+ "Comment with @mentions logged as inbox-worthy"
4936
+ );
4937
+ }
4938
+ }
4939
+ function handleNewComments(doc, planId, threads, prev, actor) {
4940
+ const newCommentCount = threads.reduce((acc, t2) => acc + t2.comments.length, 0);
4941
+ if (newCommentCount <= prev.commentCount) return;
4942
+ const diff = newCommentCount - prev.commentCount;
4943
+ const newComments = detectNewComments(threads, prev.commentIds);
4944
+ for (const comment of newComments) {
4945
+ prev.commentIds.add(comment.id);
4946
+ logCommentWithMentions(doc, planId, comment, actor);
4947
+ }
4948
+ notifyChange(planId, {
4949
+ type: "comments",
4950
+ timestamp: Date.now(),
4951
+ summary: `${diff} new comment(s)`,
4952
+ details: { added: diff }
4953
+ });
4954
+ prev.commentCount = newCommentCount;
4955
+ logger.debug({ planId, added: diff }, "New comments detected");
4956
+ }
4957
+ function handleResolvedComments(doc, planId, threads, prev, actor) {
4958
+ const newResolvedCount = threads.filter((t2) => t2.resolved).length;
4959
+ if (newResolvedCount <= prev.resolvedCount) return;
4960
+ const diff = newResolvedCount - prev.resolvedCount;
4961
+ logPlanEvent(doc, "comment_resolved", actor, { resolvedCount: diff });
4962
+ notifyChange(planId, {
4963
+ type: "resolved",
4964
+ timestamp: Date.now(),
4965
+ summary: `${diff} comment(s) resolved`,
4966
+ details: { resolved: diff }
4967
+ });
4968
+ prev.resolvedCount = newResolvedCount;
4969
+ logger.debug({ planId, resolved: diff }, "Comments resolved detected");
4970
+ }
4971
+
4972
+ // src/registry-server.ts
4973
+ function getParam(value) {
4974
+ if (Array.isArray(value)) return value[0];
4975
+ return value;
4976
+ }
4977
+ function getErrorStatus2(error) {
4978
+ if (!error || typeof error !== "object") return 500;
4979
+ const record = Object.fromEntries(Object.entries(error));
4980
+ const status = record.status;
4981
+ return typeof status === "number" ? status : 500;
4982
+ }
4983
+ var PERSISTENCE_DIR = join4(homedir3(), ".shipyard", "plans");
4984
+ var HUB_LOCK_FILE = join4(homedir3(), ".shipyard", "hub.lock");
4985
+ var SHIPYARD_DIR = join4(homedir3(), ".shipyard");
4986
+ var MAX_LOCK_RETRIES = 3;
4987
+ var messageSync = 0;
4988
+ var messageAwareness = 1;
4989
+ var docs2 = /* @__PURE__ */ new Map();
4990
+ var awarenessMap = /* @__PURE__ */ new Map();
4991
+ var conns = /* @__PURE__ */ new Map();
4992
+ var ldb = null;
4993
+ async function readLockHolderPid() {
4994
+ try {
4995
+ const content = await readFile(HUB_LOCK_FILE, "utf-8");
4996
+ const pidStr = content.split("\n")[0] ?? "";
4997
+ return Number.parseInt(pidStr, 10);
4998
+ } catch (readErr) {
4999
+ logger.error({ err: readErr }, "Failed to read hub lock file");
5000
+ return null;
5001
+ }
5002
+ }
5003
+ function isLockHolderAlive(pid) {
5004
+ try {
5005
+ process.kill(pid, 0);
5006
+ return true;
5007
+ } catch {
5008
+ return false;
5009
+ }
5010
+ }
5011
+ async function tryRemoveStaleLock(stalePid, retryCount) {
5012
+ logger.warn({ stalePid, retryCount }, "Removing stale hub lock");
5013
+ try {
5014
+ await unlink(HUB_LOCK_FILE);
5015
+ return true;
5016
+ } catch (unlinkErr) {
5017
+ logger.error({ err: unlinkErr, stalePid, retryCount }, "Failed to remove stale hub lock");
5018
+ return false;
5019
+ }
5020
+ }
5021
+ function registerLockCleanupHandler() {
5022
+ process.once("exit", () => {
5023
+ try {
5024
+ unlinkSync(HUB_LOCK_FILE);
5025
+ } catch {
5026
+ }
5027
+ });
5028
+ }
5029
+ async function handleExistingLock(retryCount) {
5030
+ const pid = await readLockHolderPid();
5031
+ if (pid === null) return false;
5032
+ if (isLockHolderAlive(pid)) {
5033
+ logger.debug({ holderPid: pid }, "Hub lock held by active process");
5034
+ return false;
5035
+ }
5036
+ if (retryCount >= MAX_LOCK_RETRIES) {
5037
+ logger.error(
5038
+ { stalePid: pid, retryCount },
5039
+ "Max retries exceeded while removing stale hub lock"
5040
+ );
5041
+ return false;
5042
+ }
5043
+ await tryRemoveStaleLock(pid, retryCount);
5044
+ return tryAcquireHubLock(retryCount + 1);
5045
+ }
5046
+ async function tryAcquireHubLock(retryCount = 0) {
5047
+ try {
5048
+ mkdirSync(SHIPYARD_DIR, { recursive: true });
5049
+ await writeFile2(HUB_LOCK_FILE, `${process.pid}
5050
+ ${Date.now()}`, { flag: "wx" });
5051
+ registerLockCleanupHandler();
5052
+ logger.info({ pid: process.pid }, "Acquired hub lock");
5053
+ return true;
5054
+ } catch (err) {
5055
+ if (hasErrorCode(err, "EEXIST")) {
5056
+ return handleExistingLock(retryCount);
5057
+ }
5058
+ logger.error({ err }, "Failed to acquire hub lock");
5059
+ return false;
5060
+ }
5061
+ }
5062
+ async function releaseHubLock() {
5063
+ try {
5064
+ await unlink(HUB_LOCK_FILE);
5065
+ logger.info("Released hub lock");
5066
+ } catch (err) {
5067
+ logger.debug({ err }, "Hub lock already released");
5068
+ }
5069
+ }
5070
+ function isLevelDbLockError(error) {
5071
+ return error.message?.includes("LOCK") || error.message?.includes("lock");
5072
+ }
5073
+ function isProcessAlive(pid) {
5074
+ try {
5075
+ process.kill(pid, 0);
5076
+ return true;
5077
+ } catch {
5078
+ return false;
5079
+ }
5080
+ }
5081
+ function tryRecoverStaleLock(originalError) {
5082
+ const lockFile = join4(PERSISTENCE_DIR, "LOCK");
5083
+ try {
5084
+ const hubLockContent = readFileSync2(HUB_LOCK_FILE, "utf-8");
5085
+ const pidStr = hubLockContent.split("\n")[0] ?? "";
5086
+ const pid = Number.parseInt(pidStr, 10);
5087
+ if (isProcessAlive(pid)) {
5088
+ logger.error({ holderPid: pid }, "LevelDB locked by active process, cannot recover");
5089
+ throw originalError;
5090
+ }
5091
+ logger.warn("Hub process dead, removing stale LevelDB lock");
5092
+ unlinkSync(lockFile);
5093
+ return true;
5094
+ } catch (hubLockErr) {
5095
+ if (hubLockErr === originalError) {
5096
+ throw hubLockErr;
5097
+ }
5098
+ logger.warn("No hub.lock found, assuming LevelDB lock is stale");
5099
+ unlinkSync(lockFile);
5100
+ return true;
5101
+ }
5102
+ }
5103
+ function initPersistence() {
5104
+ if (ldb) return;
5105
+ mkdirSync(PERSISTENCE_DIR, { recursive: true });
5106
+ try {
5107
+ ldb = new LeveldbPersistence(PERSISTENCE_DIR);
5108
+ logger.info({ dir: PERSISTENCE_DIR }, "LevelDB persistence initialized");
5109
+ return;
5110
+ } catch (err) {
5111
+ if (!(err instanceof Error)) {
5112
+ logger.error({ err }, "Failed to initialize LevelDB persistence with unknown error");
5113
+ throw new Error(String(err));
5114
+ }
5115
+ if (!isLevelDbLockError(err)) {
5116
+ logger.error({ err }, "Failed to initialize LevelDB persistence");
5117
+ throw err;
5118
+ }
5119
+ logger.warn({ err }, "LevelDB locked, checking for stale lock");
5120
+ tryRecoverStaleLock(err);
5121
+ ldb = new LeveldbPersistence(PERSISTENCE_DIR);
5122
+ logger.info("Recovered from stale LevelDB lock");
5123
+ }
5124
+ }
5125
+ async function getDoc(docName) {
5126
+ initPersistence();
5127
+ const persistence = ldb;
5128
+ if (!persistence) {
5129
+ throw new Error("LevelDB persistence failed to initialize");
5130
+ }
5131
+ let doc = docs2.get(docName);
5132
+ if (!doc) {
5133
+ doc = new Y4.Doc();
5134
+ const persistedDoc = await persistence.getYDoc(docName);
5135
+ const state = Y4.encodeStateAsUpdate(persistedDoc);
5136
+ Y4.applyUpdate(doc, state);
5137
+ doc.on("update", (update) => {
5138
+ persistence.storeUpdate(docName, update);
5139
+ });
5140
+ docs2.set(docName, doc);
5141
+ const awareness = new awarenessProtocol.Awareness(doc);
5142
+ awarenessMap.set(docName, awareness);
5143
+ attachObservers(docName, doc);
5144
+ attachCRDTValidation(docName, doc);
5145
+ }
5146
+ return doc;
5147
+ }
5148
+ async function getOrCreateDoc2(docName) {
5149
+ return getDoc(docName);
5150
+ }
5151
+ function hasActiveConnections2(planId) {
5152
+ const connections = conns.get(planId);
5153
+ return connections !== void 0 && connections.size > 0;
5154
+ }
5155
+ function send(ws, message) {
5156
+ if (ws.readyState === ws.OPEN) {
5157
+ ws.send(message);
5158
+ }
5159
+ }
5160
+ function broadcastUpdate(docName, update, origin) {
5161
+ const docConns = conns.get(docName);
5162
+ if (!docConns) return;
5163
+ const encoder = encoding.createEncoder();
5164
+ encoding.writeVarUint(encoder, messageSync);
5165
+ syncProtocol.writeUpdate(encoder, update);
5166
+ const message = encoding.toUint8Array(encoder);
5167
+ for (const conn of docConns) {
5168
+ if (conn !== origin) {
5169
+ send(conn, message);
5170
+ }
5171
+ }
5172
+ }
5173
+ function processMessage(message, doc, awareness, planId, ws) {
5174
+ try {
5175
+ const decoder = decoding.createDecoder(new Uint8Array(message));
5176
+ const messageType = decoding.readVarUint(decoder);
5177
+ switch (messageType) {
5178
+ case messageSync: {
5179
+ const encoder = encoding.createEncoder();
5180
+ encoding.writeVarUint(encoder, messageSync);
5181
+ syncProtocol.readSyncMessage(decoder, encoder, doc, ws);
5182
+ if (encoding.length(encoder) > 1) {
5183
+ send(ws, encoding.toUint8Array(encoder));
5184
+ }
5185
+ break;
5186
+ }
5187
+ case messageAwareness: {
5188
+ awarenessProtocol.applyAwarenessUpdate(awareness, decoding.readVarUint8Array(decoder), ws);
5189
+ break;
5190
+ }
5191
+ }
5192
+ } catch (err) {
5193
+ logger.error({ err, planId }, "Failed to process message");
5194
+ }
5195
+ }
5196
+ function handleWebSocketConnection(ws, req) {
5197
+ const planId = req.url?.slice(1) || "default";
5198
+ logger.info({ planId }, "WebSocket client connected to registry");
5199
+ const pendingMessages = [];
5200
+ let docReady = false;
5201
+ let doc;
5202
+ let awareness;
5203
+ ws.on("message", (message) => {
5204
+ if (!docReady) {
5205
+ pendingMessages.push(message);
5206
+ logger.debug(
5207
+ { planId, bufferedCount: pendingMessages.length },
5208
+ "Buffering message (doc not ready)"
5209
+ );
5210
+ return;
5211
+ }
5212
+ processMessage(message, doc, awareness, planId, ws);
5213
+ });
5214
+ ws.on("error", (err) => {
5215
+ logger.error({ err, planId }, "WebSocket error");
5216
+ });
5217
+ (async () => {
5218
+ try {
5219
+ doc = await getDoc(planId);
5220
+ const awarenessResult = awarenessMap.get(planId);
5221
+ if (!awarenessResult) {
5222
+ throw new Error(`Awareness not found for planId: ${planId}`);
5223
+ }
5224
+ awareness = awarenessResult;
5225
+ logger.debug({ planId }, "Got doc and awareness");
5226
+ if (!conns.has(planId)) {
5227
+ conns.set(planId, /* @__PURE__ */ new Set());
5228
+ }
5229
+ const planConns = conns.get(planId);
5230
+ planConns?.add(ws);
5231
+ const updateHandler = (update, origin) => {
5232
+ broadcastUpdate(planId, update, origin);
5233
+ };
5234
+ doc.on("update", updateHandler);
5235
+ const awarenessHandler = ({ added, updated, removed }, _origin) => {
5236
+ const changedClients = added.concat(updated, removed);
5237
+ const encoder2 = encoding.createEncoder();
5238
+ encoding.writeVarUint(encoder2, messageAwareness);
5239
+ encoding.writeVarUint8Array(
5240
+ encoder2,
5241
+ awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)
5242
+ );
5243
+ const message = encoding.toUint8Array(encoder2);
5244
+ for (const conn of conns.get(planId) || []) {
5245
+ send(conn, message);
5246
+ }
5247
+ };
5248
+ awareness.on("update", awarenessHandler);
5249
+ docReady = true;
5250
+ if (pendingMessages.length > 0) {
5251
+ logger.debug({ planId, count: pendingMessages.length }, "Processing buffered messages");
5252
+ for (const msg of pendingMessages) {
5253
+ processMessage(msg, doc, awareness, planId, ws);
5254
+ }
5255
+ pendingMessages.length = 0;
5256
+ }
5257
+ const encoder = encoding.createEncoder();
5258
+ encoding.writeVarUint(encoder, messageSync);
5259
+ syncProtocol.writeSyncStep1(encoder, doc);
5260
+ send(ws, encoding.toUint8Array(encoder));
5261
+ const awarenessStates = awareness.getStates();
5262
+ if (awarenessStates.size > 0) {
5263
+ const awarenessEncoder = encoding.createEncoder();
5264
+ encoding.writeVarUint(awarenessEncoder, messageAwareness);
5265
+ encoding.writeVarUint8Array(
5266
+ awarenessEncoder,
5267
+ awarenessProtocol.encodeAwarenessUpdate(awareness, Array.from(awarenessStates.keys()))
5268
+ );
5269
+ send(ws, encoding.toUint8Array(awarenessEncoder));
5270
+ }
5271
+ ws.on("close", () => {
5272
+ logger.info({ planId }, "WebSocket client disconnected from registry");
5273
+ doc.off("update", updateHandler);
5274
+ awareness.off("update", awarenessHandler);
5275
+ conns.get(planId)?.delete(ws);
5276
+ awarenessProtocol.removeAwarenessStates(awareness, [doc.clientID], null);
5277
+ });
5278
+ } catch (err) {
5279
+ logger.error({ err, planId }, "Error handling WebSocket connection");
5280
+ ws.close();
5281
+ }
5282
+ })();
5283
+ }
5284
+ async function handleHealthCheck(_req, res) {
5285
+ res.json({ status: "ok" });
5286
+ }
5287
+ async function handleGetPRDiff(req, res) {
5288
+ const planId = getParam(req.params.id);
5289
+ const prNumber = getParam(req.params.prNumber);
5290
+ if (!planId || !prNumber) {
5291
+ res.status(400).json({ error: "Missing plan ID or PR number" });
5292
+ return;
5293
+ }
5294
+ try {
5295
+ const doc = await getOrCreateDoc2(planId);
5296
+ const metadata = getPlanMetadata(doc);
5297
+ if (!metadata || !metadata.repo) {
5298
+ res.status(404).json({ error: "Plan not found or repo not set" });
5299
+ return;
5300
+ }
5301
+ const octokit = getOctokit();
5302
+ if (!octokit) {
5303
+ res.status(500).json({ error: "GitHub authentication not configured" });
5304
+ return;
5305
+ }
5306
+ const { owner, repoName } = parseRepoString(metadata.repo);
5307
+ const prNum = Number.parseInt(prNumber, 10);
5308
+ const response = await octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
5309
+ owner,
5310
+ repo: repoName,
5311
+ pull_number: prNum,
5312
+ headers: {
5313
+ accept: "application/vnd.github.diff"
5314
+ }
5315
+ });
5316
+ res.type("text/plain").send(response.data);
5317
+ logger.debug({ planId, prNumber: prNum, repo: metadata.repo }, "Served PR diff");
5318
+ } catch (error) {
5319
+ logger.error({ error, planId, prNumber }, "Failed to fetch PR diff");
5320
+ const status = getErrorStatus2(error);
5321
+ res.status(status).json({ error: "Failed to fetch PR diff" });
5322
+ }
5323
+ }
5324
+ async function handleGetPRFiles(req, res) {
5325
+ const planId = getParam(req.params.id);
5326
+ const prNumber = getParam(req.params.prNumber);
5327
+ if (!planId || !prNumber) {
5328
+ res.status(400).json({ error: "Missing plan ID or PR number" });
5329
+ return;
5330
+ }
5331
+ try {
5332
+ const doc = await getOrCreateDoc2(planId);
5333
+ const metadata = getPlanMetadata(doc);
5334
+ if (!metadata || !metadata.repo) {
5335
+ res.status(404).json({ error: "Plan not found or repo not set" });
5336
+ return;
5337
+ }
5338
+ const octokit = getOctokit();
5339
+ if (!octokit) {
5340
+ res.status(500).json({ error: "GitHub authentication not configured" });
5341
+ return;
5342
+ }
5343
+ const { owner, repoName } = parseRepoString(metadata.repo);
5344
+ const prNum = Number.parseInt(prNumber, 10);
5345
+ const { data: files } = await octokit.pulls.listFiles({
5346
+ owner,
5347
+ repo: repoName,
5348
+ pull_number: prNum
5349
+ });
5350
+ const fileList = files.map((file) => ({
5351
+ filename: file.filename,
5352
+ status: file.status,
5353
+ additions: file.additions,
5354
+ deletions: file.deletions,
5355
+ changes: file.changes,
5356
+ patch: file.patch
5357
+ }));
5358
+ res.json({ files: fileList });
5359
+ logger.debug({ planId, prNumber: prNum, fileCount: fileList.length }, "Served PR files");
5360
+ } catch (error) {
5361
+ logger.error({ error, planId, prNumber }, "Failed to fetch PR files");
5362
+ const status = getErrorStatus2(error);
5363
+ res.status(status).json({ error: "Failed to fetch PR files" });
5364
+ }
5365
+ }
5366
+ async function handleGetTranscript(req, res) {
5367
+ const planId = getParam(req.params.id);
5368
+ if (!planId) {
5369
+ res.status(400).json({ error: "Missing plan ID" });
5370
+ return;
5371
+ }
5372
+ try {
5373
+ const doc = await getOrCreateDoc2(planId);
5374
+ const metadata = getPlanMetadata(doc);
5375
+ if (!metadata?.origin) {
5376
+ res.status(404).json({ error: "Plan has no origin metadata" });
5377
+ return;
5378
+ }
5379
+ if (metadata.origin.platform !== "claude-code") {
5380
+ res.status(400).json({ error: "Transcript only available for Claude Code plans" });
5381
+ return;
5382
+ }
5383
+ const originRecord = Object.fromEntries(Object.entries(metadata.origin));
5384
+ const transcriptPath = originRecord.transcriptPath;
5385
+ if (typeof transcriptPath !== "string" || !transcriptPath) {
5386
+ res.status(404).json({ error: "No transcript path in origin metadata" });
5387
+ return;
5388
+ }
5389
+ const content = await readFile(transcriptPath, "utf-8");
5390
+ res.type("text/plain").send(content);
5391
+ logger.debug({ planId, transcriptPath, size: content.length }, "Served transcript for handoff");
5392
+ } catch (error) {
5393
+ if (hasErrorCode(error, "ENOENT")) {
5394
+ res.status(404).json({ error: "Transcript file not found" });
5395
+ } else {
5396
+ logger.error({ error, planId }, "Failed to read transcript");
5397
+ res.status(500).json({ error: "Failed to read transcript" });
5398
+ }
5399
+ }
5400
+ }
5401
+ function createPlanStore() {
5402
+ return {
5403
+ createSubscription: (params) => {
5404
+ const subscribe = params.subscribe.filter(
5405
+ (s) => s === "status" || s === "comments" || s === "resolved" || s === "content" || s === "artifacts"
5406
+ );
5407
+ return createSubscription({
5408
+ planId: params.planId,
5409
+ subscribe,
5410
+ windowMs: params.windowMs,
5411
+ maxWindowMs: params.maxWindowMs,
5412
+ threshold: params.threshold
5413
+ });
5414
+ },
5415
+ getChanges: (planId, clientId) => getChanges(planId, clientId),
5416
+ deleteSubscription: (planId, clientId) => deleteSubscription(planId, clientId),
5417
+ hasActiveConnections: async (planId) => hasActiveConnections2(planId)
5418
+ };
5419
+ }
5420
+ function createContext() {
5421
+ return {
5422
+ getOrCreateDoc: getOrCreateDoc2,
5423
+ getPlanStore: createPlanStore,
5424
+ logger,
5425
+ hookHandlers: createHookHandlers(),
5426
+ conversationHandlers: createConversationHandlers(),
5427
+ getLocalChanges,
5428
+ getFileContent
5429
+ };
5430
+ }
5431
+ function createApp() {
5432
+ const app = express();
5433
+ const httpServer = http.createServer(app);
5434
+ app.use((_req, res, next) => {
5435
+ res.header("Access-Control-Allow-Origin", "*");
5436
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
5437
+ res.header("Access-Control-Allow-Headers", "Content-Type");
5438
+ next();
5439
+ });
5440
+ app.options("{*splat}", (_req, res) => {
5441
+ res.sendStatus(204);
5442
+ });
5443
+ app.use(express.json({ limit: "10mb" }));
5444
+ app.use(
5445
+ "/trpc",
5446
+ createExpressMiddleware({
5447
+ router: appRouter,
5448
+ createContext
5449
+ })
5450
+ );
5451
+ app.get("/registry", handleHealthCheck);
5452
+ app.get("/api/plan/:planId/has-connections", (req, res) => {
5453
+ const planId = req.params.planId;
5454
+ if (!planId) {
5455
+ res.status(400).json({ error: "Missing plan ID" });
5456
+ return;
5457
+ }
5458
+ const hasConnections = hasActiveConnections2(planId);
5459
+ res.json({ hasConnections });
5460
+ });
5461
+ app.get("/api/plan/:id/transcript", handleGetTranscript);
5462
+ app.get("/api/plans/:id/pr-diff/:prNumber", handleGetPRDiff);
5463
+ app.get("/api/plans/:id/pr-files/:prNumber", handleGetPRFiles);
5464
+ app.get("/artifacts/:planId/:filename", async (req, res) => {
5465
+ const planId = getParam(req.params.planId);
5466
+ const filename = getParam(req.params.filename);
5467
+ if (!planId || !filename) {
5468
+ res.status(400).json({ error: "Missing planId or filename" });
5469
+ return;
5470
+ }
5471
+ const ARTIFACTS_DIR = join4(homedir3(), ".shipyard", "artifacts");
5472
+ const fullPath = resolve(ARTIFACTS_DIR, planId, filename);
5473
+ if (!fullPath.startsWith(ARTIFACTS_DIR + sep)) {
5474
+ res.status(400).json({ error: "Invalid artifact path" });
5475
+ return;
5476
+ }
5477
+ const buffer = await readFile(fullPath).catch(() => null);
5478
+ if (!buffer) {
5479
+ res.status(404).json({ error: "Artifact not found" });
5480
+ return;
5481
+ }
5482
+ const ext = filename.split(".").pop()?.toLowerCase();
5483
+ const mimeTypes = {
5484
+ png: "image/png",
5485
+ jpg: "image/jpeg",
5486
+ jpeg: "image/jpeg",
5487
+ mp4: "video/mp4",
5488
+ webm: "video/webm",
5489
+ json: "application/json",
5490
+ txt: "text/plain"
5491
+ };
5492
+ const contentType = mimeTypes[ext || ""] || "application/octet-stream";
5493
+ res.setHeader("Content-Type", contentType);
5494
+ res.setHeader("Content-Length", buffer.length);
5495
+ res.setHeader("Cache-Control", "public, max-age=31536000");
5496
+ res.send(buffer);
5497
+ });
5498
+ return { app, httpServer };
5499
+ }
5500
+ async function startRegistryServer() {
5501
+ const ports = registryConfig.REGISTRY_PORT;
5502
+ const { httpServer } = createApp();
5503
+ const wss = new WebSocketServer({ noServer: true });
5504
+ httpServer.on("upgrade", (request, socket, head) => {
5505
+ wss.handleUpgrade(request, socket, head, (ws) => {
5506
+ wss.emit("connection", ws, request);
5507
+ });
5508
+ });
5509
+ wss.on("connection", handleWebSocketConnection);
5510
+ process.once("SIGINT", async () => {
5511
+ logger.info("SIGINT received, shutting down gracefully");
5512
+ const { stopPeriodicCleanup } = await import("./session-registry-CBDXMXY3.js");
5513
+ stopPeriodicCleanup();
5514
+ await releaseHubLock();
5515
+ process.exit(0);
5516
+ });
5517
+ process.once("SIGTERM", async () => {
5518
+ logger.info("SIGTERM received, shutting down gracefully");
5519
+ const { stopPeriodicCleanup } = await import("./session-registry-CBDXMXY3.js");
5520
+ stopPeriodicCleanup();
5521
+ await releaseHubLock();
5522
+ process.exit(0);
5523
+ });
5524
+ for (const port of ports) {
5525
+ try {
5526
+ await new Promise((resolve2, reject) => {
5527
+ httpServer.listen(port, "127.0.0.1", () => {
5528
+ logger.info(
5529
+ { port, persistence: PERSISTENCE_DIR },
5530
+ "Registry server started with WebSocket and tRPC support"
5531
+ );
5532
+ startCleanupInterval();
5533
+ resolve2();
5534
+ });
5535
+ httpServer.on("error", (err) => {
5536
+ if (err.code === "EADDRINUSE") {
5537
+ reject(err);
5538
+ } else {
5539
+ logger.error({ err, port }, "Registry server error");
5540
+ }
5541
+ });
5542
+ });
5543
+ return port;
5544
+ } catch (err) {
5545
+ logger.debug({ err, port }, "Port unavailable or server failed to start");
5546
+ }
5547
+ }
5548
+ logger.warn({ ports }, "All registry ports in use");
5549
+ return null;
5550
+ }
5551
+ async function isRegistryRunning() {
5552
+ const ports = registryConfig.REGISTRY_PORT;
5553
+ for (const port of ports) {
5554
+ try {
5555
+ const res = await fetch(`http://localhost:${port}/registry`, {
5556
+ signal: AbortSignal.timeout(1e3)
5557
+ });
5558
+ if (res.ok) {
5559
+ return port;
5560
+ }
5561
+ } catch {
5562
+ }
5563
+ }
5564
+ return null;
5565
+ }
5566
+
5567
+ export {
5568
+ PlanStatusValues,
5569
+ createLinkedPR,
3743
5570
  InputRequestSchema,
3744
5571
  createInputRequest,
3745
- normalizeChoiceOptions,
3746
- CHOICE_DROPDOWN_THRESHOLD,
3747
- MAX_QUESTIONS_PER_REQUEST,
3748
- QuestionSchema,
3749
- MultiQuestionInputRequestSchema,
3750
- AnyInputRequestSchema,
3751
5572
  createMultiQuestionInputRequest,
3752
5573
  YDOC_KEYS,
3753
- isValidYDocKey,
3754
- ThreadCommentSchema,
3755
- ThreadSchema,
3756
- isThread,
3757
5574
  parseThreads,
3758
5575
  extractTextFromCommentBody,
3759
- extractMentions,
3760
- VALID_STATUS_TRANSITIONS,
3761
5576
  getPlanMetadata,
3762
- getPlanMetadataWithValidation,
3763
5577
  setPlanMetadata,
3764
5578
  resetPlanToDraft,
3765
5579
  transitionPlanStatus,
3766
5580
  initPlanMetadata,
3767
- getStepCompletions,
3768
- toggleStepCompletion,
3769
- isStepCompleted,
3770
5581
  getArtifacts,
3771
5582
  addArtifact,
3772
- removeArtifact,
3773
- getAgentPresences,
3774
- setAgentPresence,
3775
- clearAgentPresence,
3776
- getAgentPresence,
3777
5583
  getDeliverables,
3778
5584
  addDeliverable,
3779
5585
  linkArtifactToDeliverable,
3780
- getPlanOwnerId,
3781
- isApprovalRequired,
3782
- getApprovedUsers,
3783
- isUserApproved,
3784
- approveUser,
3785
- revokeUser,
3786
- getRejectedUsers,
3787
- isUserRejected,
3788
- rejectUser,
3789
- unrejectUser,
3790
5586
  getLinkedPRs,
3791
5587
  linkPR,
3792
- unlinkPR,
3793
- getLinkedPR,
3794
- updateLinkedPRStatus,
3795
5588
  getPRReviewComments,
3796
- getPRReviewCommentsForPR,
3797
- addPRReviewComment,
3798
- resolvePRReviewComment,
3799
- removePRReviewComment,
3800
5589
  getLocalDiffComments,
3801
- getLocalDiffCommentsForFile,
3802
- addLocalDiffComment,
3803
- resolveLocalDiffComment,
3804
- removeLocalDiffComment,
3805
- markPlanAsViewed,
3806
- getViewedBy,
3807
- isPlanUnread,
3808
- getConversationVersions,
3809
- addConversationVersion,
3810
- markVersionHandedOff,
3811
5590
  logPlanEvent,
3812
5591
  getPlanEvents,
3813
5592
  getSnapshots,
3814
5593
  addSnapshot,
3815
5594
  createPlanSnapshot,
3816
- getLatestSnapshot,
3817
- addPlanTag,
3818
- removePlanTag,
3819
- getAllTagsFromIndex,
3820
- archivePlan,
3821
- unarchivePlan,
3822
- answerInputRequest,
3823
- answerMultiQuestionInputRequest,
3824
- cancelInputRequest,
3825
- declineInputRequest,
3826
5595
  atomicRegenerateTokenIfOwner,
3827
- isUrlEncodedPlanV1,
3828
- isUrlEncodedPlanV2,
3829
- encodePlan,
3830
- decodePlan,
3831
- createPlanUrl,
3832
5596
  createPlanUrlWithHistory,
3833
- getPlanFromUrl,
3834
- A2ATextPartSchema,
3835
- A2ADataPartSchema,
3836
- A2AFilePartSchema,
3837
- A2APartSchema,
3838
- A2AMessageSchema,
3839
- ConversationExportMetaSchema,
3840
- ClaudeCodeMessageSchema,
3841
- parseClaudeCodeTranscriptString,
3842
- claudeCodeToA2A,
3843
- validateA2AMessages,
3844
- summarizeA2AConversation,
3845
- a2aToClaudeCode,
3846
- formatAsClaudeCodeJSONL,
3847
5597
  formatDeliverablesForLLM,
3848
5598
  extractDeliverables,
3849
- hashLineContent,
3850
- computeCommentStaleness,
3851
- isLineContentStale,
3852
- withStalenessInfo,
3853
- withStalenessInfoBatch,
3854
- buildLineContentMap,
3855
- formatStalenessMarker,
3856
5599
  formatDiffCommentsForLLM,
3857
- formatPRCommentsForLLM,
3858
- getPRCommentsSummary,
3859
- EnvironmentContextSchema,
3860
5600
  GitHubPRResponseSchema,
3861
- asPlanId,
3862
- asAwarenessClientId,
3863
- asWebRTCPeerId,
3864
- asGitHubUsername,
3865
- ROUTES,
3866
5601
  createPlanWebUrl,
3867
- InviteTokenSchema,
3868
- InviteRedemptionSchema,
3869
- parseInviteFromUrl,
3870
- buildInviteUrl,
3871
- getTokenTimeRemaining,
3872
- GitFileStatusSchema,
3873
- LocalFileChangeSchema,
3874
- LocalChangesResponseSchema,
3875
- LocalChangesUnavailableReasonSchema,
3876
- LocalChangesUnavailableSchema,
3877
- LocalChangesResultSchema,
3878
- P2PMessageType,
3879
- ConversationExportStartMetaSchema,
3880
- ChunkMessageSchema,
3881
- ConversationExportEndSchema,
3882
- isConversationExportStart,
3883
- isConversationChunk,
3884
- isConversationExportEnd,
3885
- isP2PConversationMessage,
3886
- encodeExportStartMessage,
3887
- decodeExportStartMessage,
3888
- encodeChunkMessage,
3889
- decodeChunkMessage,
3890
- encodeExportEndMessage,
3891
- decodeExportEndMessage,
3892
- decodeP2PMessage,
3893
- assertNeverP2PMessage,
3894
5602
  PLAN_INDEX_DOC_NAME,
3895
- PLAN_INDEX_VIEWED_BY_KEY,
3896
- NON_PLAN_DB_NAMES,
3897
- PlanIndexEntrySchema,
3898
- getPlanIndex,
3899
- getPlanIndexEntry,
3900
5603
  setPlanIndexEntry,
3901
- removePlanIndexEntry,
3902
5604
  touchPlanIndexEntry,
3903
- getViewedByFromIndex,
3904
- updatePlanIndexViewedBy,
3905
- clearPlanIndexViewedBy,
3906
- getAllViewedByFromIndex,
3907
- removeViewedByFromIndex,
3908
- PLAN_INDEX_EVENT_VIEWED_BY_KEY,
3909
- markEventAsViewed,
3910
- clearEventViewedBy,
3911
- isEventUnread,
3912
- getAllEventViewedByForPlan,
3913
- formatThreadsForLLM,
3914
5605
  TOOL_NAMES,
3915
- PlanIdSchema,
3916
- PlanStatusResponseSchema,
3917
- HasConnectionsResponseSchema,
3918
- SubscriptionClientIdSchema,
3919
- ChangeSchema,
3920
- ChangesResponseSchema,
3921
- DeleteSubscriptionResponseSchema,
3922
- SetSessionTokenRequestSchema,
3923
- SetSessionTokenResponseSchema,
3924
- ImportConversationRequestSchema,
3925
- ImportConversationResponseSchema,
3926
- conversationRouter,
3927
- hookRouter,
3928
- planRouter,
3929
- subscriptionRouter,
3930
- appRouter,
3931
- isErrnoException,
3932
5606
  hasErrorCode,
3933
- isBuffer,
3934
5607
  createUserResolver,
3935
- getSignalingConnections,
3936
- getWebrtcPeerId,
3937
- getWebrtcRoom
5608
+ registryConfig,
5609
+ getLocalChanges,
5610
+ parseRepoString,
5611
+ isArtifactsEnabled,
5612
+ GitHubAuthError,
5613
+ getOctokit,
5614
+ isGitHubConfigured,
5615
+ uploadArtifact,
5616
+ generateSessionToken,
5617
+ hashSessionToken,
5618
+ webConfig,
5619
+ getRepositoryFullName,
5620
+ getGitHubUsername,
5621
+ getVerifiedGitHubUsername,
5622
+ tryAcquireHubLock,
5623
+ releaseHubLock,
5624
+ startRegistryServer,
5625
+ isRegistryRunning,
5626
+ initAsHub,
5627
+ initAsClient,
5628
+ getOrCreateDoc3 as getOrCreateDoc,
5629
+ hasActiveConnections3 as hasActiveConnections
3938
5630
  };