replicas-engine 0.1.117 → 0.1.119

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.
Files changed (2) hide show
  1. package/dist/src/index.js +176 -122
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -362,7 +362,7 @@ var codexTokenManager = new CodexTokenManager();
362
362
  import { readdir, stat } from "fs/promises";
363
363
  import { existsSync as existsSync2 } from "fs";
364
364
  import { execFileSync as execFileSync2 } from "child_process";
365
- import { join as join3 } from "path";
365
+ import { join as join4 } from "path";
366
366
 
367
367
  // src/utils/state.ts
368
368
  import { readFile, writeFile, mkdir, rename, unlink } from "fs/promises";
@@ -413,7 +413,12 @@ function coerceRepoState(value) {
413
413
  if (typeof value.path !== "string") return null;
414
414
  if (typeof value.defaultBranch !== "string") return null;
415
415
  if (typeof value.currentBranch !== "string") return null;
416
- if (!(value.prUrl === null || typeof value.prUrl === "string")) return null;
416
+ if (!Array.isArray(value.prUrls)) return null;
417
+ const prUrls = [];
418
+ for (const entry of value.prUrls) {
419
+ if (typeof entry !== "string") return null;
420
+ if (!prUrls.includes(entry)) prUrls.push(entry);
421
+ }
417
422
  if (!(value.gitDiff === null || isEngineRepoDiff(value.gitDiff))) return null;
418
423
  if (typeof value.startHooksCompleted !== "boolean") return null;
419
424
  return {
@@ -421,7 +426,7 @@ function coerceRepoState(value) {
421
426
  path: value.path,
422
427
  defaultBranch: value.defaultBranch,
423
428
  currentBranch: value.currentBranch,
424
- prUrl: value.prUrl,
429
+ prUrls,
425
430
  gitDiff: value.gitDiff,
426
431
  startHooksCompleted: value.startHooksCompleted
427
432
  };
@@ -482,6 +487,8 @@ async function saveRepoState(repoName, state, fallbackState) {
482
487
 
483
488
  // src/git/commands.ts
484
489
  import { execFileSync } from "child_process";
490
+ import { readFileSync } from "fs";
491
+ import { join as join3 } from "path";
485
492
  function runGitCommand(args, cwd) {
486
493
  return execFileSync("git", args, {
487
494
  cwd,
@@ -489,6 +496,15 @@ function runGitCommand(args, cwd) {
489
496
  stdio: ["pipe", "pipe", "pipe"]
490
497
  }).trim();
491
498
  }
499
+ function readRepoHeadBranch(repoPath) {
500
+ try {
501
+ const contents = readFileSync(join3(repoPath, ".git", "HEAD"), "utf-8").trim();
502
+ const match = contents.match(/^ref:\s+refs\/heads\/(.+)$/);
503
+ return match ? match[1] : null;
504
+ } catch {
505
+ return null;
506
+ }
507
+ }
492
508
  function branchExists(branchName, cwd) {
493
509
  try {
494
510
  runGitCommand(["rev-parse", "--verify", branchName], cwd);
@@ -507,6 +523,9 @@ function getCurrentBranch(cwd) {
507
523
  }
508
524
 
509
525
  // src/git/service.ts
526
+ function appendUniqueUrl(urls, url) {
527
+ return urls.includes(url) ? urls : [...urls, url];
528
+ }
510
529
  var GitService = class {
511
530
  defaultBranchCache = /* @__PURE__ */ new Map();
512
531
  cachedPrByRepo = /* @__PURE__ */ new Map();
@@ -522,13 +541,13 @@ var GitService = class {
522
541
  const entries = await readdir(root);
523
542
  const repos = [];
524
543
  for (const entry of entries) {
525
- const fullPath = join3(root, entry);
544
+ const fullPath = join4(root, entry);
526
545
  try {
527
546
  const entryStat = await stat(fullPath);
528
547
  if (!entryStat.isDirectory()) {
529
548
  continue;
530
549
  }
531
- const hasGit = Boolean(await this.safeStat(join3(fullPath, ".git")));
550
+ const hasGit = Boolean(await this.safeStat(join4(fullPath, ".git")));
532
551
  if (!hasGit) {
533
552
  continue;
534
553
  }
@@ -563,7 +582,7 @@ var GitService = class {
563
582
  path: repo.path,
564
583
  defaultBranch: repo.defaultBranch,
565
584
  currentBranch,
566
- prUrl: persistedMatchesCurrentBranch ? persistedState.prUrl : null,
585
+ prUrls: persistedState?.prUrls ?? [],
567
586
  gitDiff: persistedMatchesCurrentBranch ? persistedState.gitDiff : null,
568
587
  startHooksCompleted: persistedState?.startHooksCompleted ?? false
569
588
  });
@@ -572,7 +591,7 @@ var GitService = class {
572
591
  }
573
592
  return states;
574
593
  }
575
- async refreshRepos() {
594
+ async refreshRepos(observedBranchesByRepo) {
576
595
  const repos = await this.listRepositories();
577
596
  const states = [];
578
597
  for (const repo of repos) {
@@ -580,12 +599,29 @@ var GitService = class {
580
599
  const persistedState = await loadRepoState(repo.name);
581
600
  const currentBranch = getCurrentBranch(repo.path) ?? repo.defaultBranch;
582
601
  const startHooksCompleted = persistedState?.startHooksCompleted ?? false;
583
- states.push(await this.refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState));
602
+ const observed = observedBranchesByRepo?.get(repo.name);
603
+ states.push(
604
+ await this.refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState, observed)
605
+ );
584
606
  } catch {
585
607
  }
586
608
  }
587
609
  return states;
588
610
  }
611
+ // Fast branch snapshot for per-event observation during a turn. Reads
612
+ // .git/HEAD directly for each repo (no subprocess) so callers can safely
613
+ // invoke this on every agent event without measurable overhead.
614
+ async snapshotCurrentBranches() {
615
+ const repos = await this.listRepositories();
616
+ const branches = /* @__PURE__ */ new Map();
617
+ for (const repo of repos) {
618
+ const branch = readRepoHeadBranch(repo.path);
619
+ if (branch) {
620
+ branches.set(repo.name, branch);
621
+ }
622
+ }
623
+ return branches;
624
+ }
589
625
  async setBranchNameAndCheckout(name) {
590
626
  ENGINE_ENV.WORKSPACE_BRANCH_NAME = name;
591
627
  return this.initializeGitRepository();
@@ -618,7 +654,7 @@ var GitService = class {
618
654
  path: repo.path,
619
655
  defaultBranch: repo.defaultBranch,
620
656
  currentBranch: repo.defaultBranch,
621
- prUrl: null,
657
+ prUrls: [],
622
658
  gitDiff: null,
623
659
  startHooksCompleted: false
624
660
  };
@@ -645,7 +681,7 @@ var GitService = class {
645
681
  }
646
682
  const branchName = this.findAvailableBranchName(workspaceName, repo.path);
647
683
  runGitCommand(["checkout", "-b", branchName], repo.path);
648
- await saveRepoState(repo.name, { currentBranch: branchName, prUrl: null }, baselineState);
684
+ await saveRepoState(repo.name, { currentBranch: branchName }, baselineState);
649
685
  results.push({
650
686
  name: repo.name,
651
687
  success: true,
@@ -723,56 +759,50 @@ var GitService = class {
723
759
  return { status: "found", url: cachedPr.prUrl };
724
760
  }
725
761
  const persistedRepoState = persistedRepoStateArg ?? await loadRepoState(repoName);
726
- if (persistedRepoState?.prUrl && persistedRepoState.currentBranch === currentBranch) {
727
- this.cachedPrByRepo.set(repoName, {
728
- prUrl: persistedRepoState.prUrl,
729
- currentBranch
730
- });
731
- return { status: "found", url: persistedRepoState.prUrl };
732
- }
733
762
  this.cachedPrByRepo.delete(repoName);
734
- if (persistedRepoState?.prUrl && persistedRepoState.currentBranch !== currentBranch) {
735
- await saveRepoState(repoName, { prUrl: null }, persistedRepoState);
736
- }
737
- try {
738
- const remoteRef = execFileSync2("git", ["ls-remote", "--heads", "origin", currentBranch], {
739
- cwd: repoPath,
740
- encoding: "utf-8",
741
- stdio: ["pipe", "pipe", "pipe"]
742
- }).trim();
743
- if (!remoteRef) {
744
- return { status: "not_found" };
763
+ const result = this.lookupPrOnRemote(repoName, repoPath, currentBranch);
764
+ if (result.status === "found") {
765
+ this.cachedPrByRepo.set(repoName, { prUrl: result.url, currentBranch });
766
+ if (persistedRepoState && !persistedRepoState.prUrls.includes(result.url)) {
767
+ await saveRepoState(
768
+ repoName,
769
+ { prUrls: appendUniqueUrl(persistedRepoState.prUrls, result.url) },
770
+ persistedRepoState
771
+ );
745
772
  }
746
- } catch {
747
- return { status: "error" };
748
- }
749
- try {
750
- const prInfo = execFileSync2("gh", ["pr", "view", "--json", "url", "--jq", ".url"], {
751
- cwd: repoPath,
752
- encoding: "utf-8",
753
- stdio: ["pipe", "pipe", "pipe"]
754
- }).trim();
755
- if (prInfo) {
756
- this.cachedPrByRepo.set(repoName, {
757
- prUrl: prInfo,
758
- currentBranch
759
- });
760
- if (persistedRepoState) {
761
- await saveRepoState(repoName, { prUrl: prInfo }, persistedRepoState);
762
- }
763
- return { status: "found", url: prInfo };
764
- }
765
- } catch (error) {
766
- const message = error instanceof Error ? error.message : String(error);
767
- console.warn(`[GitService] gh pr view failed for ${repoName}: ${message}`);
768
- return { status: "error" };
769
773
  }
770
- return { status: "not_found" };
774
+ return result;
771
775
  } catch (error) {
772
776
  console.error("Error checking for pull request:", error);
773
777
  return { status: "error" };
774
778
  }
775
779
  }
780
+ lookupPrOnRemote(repoName, repoPath, branch) {
781
+ try {
782
+ const remoteRef = execFileSync2("git", ["ls-remote", "--heads", "origin", branch], {
783
+ cwd: repoPath,
784
+ encoding: "utf-8",
785
+ stdio: ["pipe", "pipe", "pipe"]
786
+ }).trim();
787
+ if (!remoteRef) {
788
+ return { status: "not_found" };
789
+ }
790
+ } catch {
791
+ return { status: "error" };
792
+ }
793
+ try {
794
+ const prInfo = execFileSync2("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
795
+ cwd: repoPath,
796
+ encoding: "utf-8",
797
+ stdio: ["pipe", "pipe", "pipe"]
798
+ }).trim();
799
+ return prInfo ? { status: "found", url: prInfo } : { status: "not_found" };
800
+ } catch (error) {
801
+ const message = error instanceof Error ? error.message : String(error);
802
+ console.warn(`[GitService] gh pr view ${branch} failed for ${repoName}: ${message}`);
803
+ return { status: "error" };
804
+ }
805
+ }
776
806
  resolveDefaultBranch(repoPath) {
777
807
  const cached = this.defaultBranchCache.get(repoPath);
778
808
  if (cached) {
@@ -807,22 +837,27 @@ var GitService = class {
807
837
  const normalized = name.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^-+|-+$/g, "");
808
838
  return normalized || "replicas";
809
839
  }
810
- async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState) {
840
+ async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState, observedBranches) {
811
841
  const prResult = await this.getPullRequestUrl(repo.name, repo.path, currentBranch, persistedState);
812
- let prUrl;
842
+ let prUrls = persistedState?.prUrls ?? [];
813
843
  if (prResult.status === "found") {
814
- prUrl = prResult.url;
815
- } else if (prResult.status === "not_found") {
816
- prUrl = null;
817
- } else {
818
- prUrl = persistedState?.prUrl ?? null;
844
+ prUrls = appendUniqueUrl(prUrls, prResult.url);
845
+ }
846
+ if (observedBranches) {
847
+ for (const branch of observedBranches) {
848
+ if (branch === currentBranch) continue;
849
+ const branchResult = this.lookupPrOnRemote(repo.name, repo.path, branch);
850
+ if (branchResult.status === "found") {
851
+ prUrls = appendUniqueUrl(prUrls, branchResult.url);
852
+ }
853
+ }
819
854
  }
820
855
  const state = {
821
856
  name: repo.name,
822
857
  path: repo.path,
823
858
  defaultBranch: repo.defaultBranch,
824
859
  currentBranch,
825
- prUrl,
860
+ prUrls,
826
861
  gitDiff: this.getGitDiffStats(repo.path, repo.defaultBranch),
827
862
  startHooksCompleted
828
863
  };
@@ -845,10 +880,10 @@ var gitService = new GitService();
845
880
  // src/utils/logger.ts
846
881
  import { appendFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
847
882
  import { homedir as homedir3 } from "os";
848
- import { join as join4 } from "path";
883
+ import { join as join5 } from "path";
849
884
  import { format } from "util";
850
885
  import { randomBytes } from "crypto";
851
- var LOG_DIR = join4(homedir3(), ".replicas", "logs");
886
+ var LOG_DIR = join5(homedir3(), ".replicas", "logs");
852
887
  var EngineLogger = class {
853
888
  _sessionId = null;
854
889
  filePath = null;
@@ -860,7 +895,7 @@ var EngineLogger = class {
860
895
  async initialize() {
861
896
  await mkdir2(LOG_DIR, { recursive: true });
862
897
  this._sessionId = this.createSessionId();
863
- this.filePath = join4(LOG_DIR, `${this._sessionId}.log`);
898
+ this.filePath = join5(LOG_DIR, `${this._sessionId}.log`);
864
899
  await writeFile2(this.filePath, `=== Replicas Engine Session ${this._sessionId} ===
865
900
  `, "utf-8");
866
901
  this.patchConsole();
@@ -913,7 +948,7 @@ var engineLogger = new EngineLogger();
913
948
  // src/services/replicas-config-service.ts
914
949
  import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
915
950
  import { existsSync as existsSync4 } from "fs";
916
- import { join as join7 } from "path";
951
+ import { join as join8 } from "path";
917
952
  import { homedir as homedir5 } from "os";
918
953
  import { exec } from "child_process";
919
954
  import { promisify as promisify2 } from "util";
@@ -1102,7 +1137,7 @@ function parseReplicasConfigString(content, filename) {
1102
1137
  }
1103
1138
 
1104
1139
  // ../shared/src/engine/environment.ts
1105
- var DAYTONA_SNAPSHOT_ID = "21-04-2026-islington-v2";
1140
+ var DAYTONA_SNAPSHOT_ID = "22-04-2026-islington-v2";
1106
1141
 
1107
1142
  // ../shared/src/engine/types.ts
1108
1143
  var DEFAULT_CHAT_TITLES = {
@@ -1120,14 +1155,14 @@ var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
1120
1155
  import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
1121
1156
  import { existsSync as existsSync3 } from "fs";
1122
1157
  import { homedir as homedir4 } from "os";
1123
- import { join as join5 } from "path";
1158
+ import { join as join6 } from "path";
1124
1159
  import { execFile } from "child_process";
1125
1160
  import { promisify } from "util";
1126
1161
  var execFileAsync = promisify(execFile);
1127
1162
  var REPLICAS_DIR = SANDBOX_PATHS.REPLICAS_DIR;
1128
- var DETAILS_FILE = join5(REPLICAS_DIR, "environment-details.json");
1129
- var CLAUDE_CREDENTIALS_PATH = join5(homedir4(), ".claude", ".credentials.json");
1130
- var CODEX_AUTH_PATH = join5(homedir4(), ".codex", "auth.json");
1163
+ var DETAILS_FILE = join6(REPLICAS_DIR, "environment-details.json");
1164
+ var CLAUDE_CREDENTIALS_PATH = join6(homedir4(), ".claude", ".credentials.json");
1165
+ var CODEX_AUTH_PATH = join6(homedir4(), ".codex", "auth.json");
1131
1166
  function detectClaudeAuthMethod() {
1132
1167
  if (existsSync3(CLAUDE_CREDENTIALS_PATH)) {
1133
1168
  return "oauth";
@@ -1273,8 +1308,8 @@ var environmentDetailsService = new EnvironmentDetailsService();
1273
1308
  // src/services/start-hook-logs-service.ts
1274
1309
  import { createHash } from "crypto";
1275
1310
  import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, readdir as readdir2 } from "fs/promises";
1276
- import { join as join6 } from "path";
1277
- var LOGS_DIR = join6(SANDBOX_PATHS.REPLICAS_DIR, "start-hook-logs");
1311
+ import { join as join7 } from "path";
1312
+ var LOGS_DIR = join7(SANDBOX_PATHS.REPLICAS_DIR, "start-hook-logs");
1278
1313
  function sanitizeFilename(name) {
1279
1314
  const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
1280
1315
  const hash = createHash("sha256").update(name).digest("hex").slice(0, 8);
@@ -1294,7 +1329,7 @@ var StartHookLogsService = class {
1294
1329
  async saveRepoLog(repoName, entry) {
1295
1330
  await this.ensureDir();
1296
1331
  const log = { repoName, ...entry };
1297
- await writeFile4(join6(LOGS_DIR, repoFilename(repoName)), `${JSON.stringify(log, null, 2)}
1332
+ await writeFile4(join7(LOGS_DIR, repoFilename(repoName)), `${JSON.stringify(log, null, 2)}
1298
1333
  `, "utf-8");
1299
1334
  }
1300
1335
  async getAllLogs() {
@@ -1313,7 +1348,7 @@ var StartHookLogsService = class {
1313
1348
  continue;
1314
1349
  }
1315
1350
  try {
1316
- const raw = await readFile3(join6(LOGS_DIR, file), "utf-8");
1351
+ const raw = await readFile3(join7(LOGS_DIR, file), "utf-8");
1317
1352
  const stored = JSON.parse(raw);
1318
1353
  logs.push(withPreview(stored));
1319
1354
  } catch {
@@ -1324,7 +1359,7 @@ var StartHookLogsService = class {
1324
1359
  }
1325
1360
  async getFullOutput(repoName) {
1326
1361
  try {
1327
- const raw = await readFile3(join6(LOGS_DIR, repoFilename(repoName)), "utf-8");
1362
+ const raw = await readFile3(join7(LOGS_DIR, repoFilename(repoName)), "utf-8");
1328
1363
  const stored = JSON.parse(raw);
1329
1364
  if (stored.repoName !== repoName) {
1330
1365
  return null;
@@ -1342,7 +1377,7 @@ var startHookLogsService = new StartHookLogsService();
1342
1377
 
1343
1378
  // src/services/replicas-config-service.ts
1344
1379
  var execAsync = promisify2(exec);
1345
- var START_HOOKS_LOG = join7(homedir5(), ".replicas", "startHooks.log");
1380
+ var START_HOOKS_LOG = join8(homedir5(), ".replicas", "startHooks.log");
1346
1381
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
1347
1382
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
1348
1383
  These hooks are currently executing in the background. You can:
@@ -1353,7 +1388,7 @@ The start hooks may install dependencies, build projects, or perform other setup
1353
1388
  If your task depends on setup being complete, check the log file before proceeding.`;
1354
1389
  async function readReplicasConfigFromDir(dirPath) {
1355
1390
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
1356
- const configPath = join7(dirPath, filename);
1391
+ const configPath = join8(dirPath, filename);
1357
1392
  if (!existsSync4(configPath)) {
1358
1393
  continue;
1359
1394
  }
@@ -1417,7 +1452,7 @@ var ReplicasConfigService = class {
1417
1452
  const logLine = `[${timestamp}] ${message}
1418
1453
  `;
1419
1454
  try {
1420
- await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1455
+ await mkdir5(join8(homedir5(), ".replicas"), { recursive: true });
1421
1456
  await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
1422
1457
  } catch (error) {
1423
1458
  console.error("Failed to write to start hooks log:", error);
@@ -1441,7 +1476,7 @@ var ReplicasConfigService = class {
1441
1476
  this.hooksRunning = true;
1442
1477
  this.hooksCompleted = false;
1443
1478
  try {
1444
- await mkdir5(join7(homedir5(), ".replicas"), { recursive: true });
1479
+ await mkdir5(join8(homedir5(), ".replicas"), { recursive: true });
1445
1480
  await writeFile5(
1446
1481
  START_HOOKS_LOG,
1447
1482
  `=== Start Hooks Execution Log ===
@@ -1528,7 +1563,7 @@ Repositories: ${hookEntries.length}
1528
1563
  path: entry.workingDirectory,
1529
1564
  defaultBranch: entry.defaultBranch,
1530
1565
  currentBranch: entry.defaultBranch,
1531
- prUrl: null,
1566
+ prUrls: [],
1532
1567
  gitDiff: null,
1533
1568
  startHooksCompleted: false
1534
1569
  };
@@ -1593,10 +1628,10 @@ var replicasConfigService = new ReplicasConfigService();
1593
1628
  // src/services/event-service.ts
1594
1629
  import { appendFile as appendFile3, mkdir as mkdir6 } from "fs/promises";
1595
1630
  import { homedir as homedir6 } from "os";
1596
- import { join as join8 } from "path";
1631
+ import { join as join9 } from "path";
1597
1632
  import { randomUUID } from "crypto";
1598
- var ENGINE_DIR = join8(homedir6(), ".replicas", "engine");
1599
- var EVENTS_FILE = join8(ENGINE_DIR, "events.jsonl");
1633
+ var ENGINE_DIR = join9(homedir6(), ".replicas", "engine");
1634
+ var EVENTS_FILE = join9(ENGINE_DIR, "events.jsonl");
1600
1635
  var EventService = class {
1601
1636
  subscribers = /* @__PURE__ */ new Map();
1602
1637
  writeChain = Promise.resolve();
@@ -1689,14 +1724,14 @@ var previewService = new PreviewService();
1689
1724
  import { existsSync as existsSync7 } from "fs";
1690
1725
  import { mkdir as mkdir10, readFile as readFile8, rm, writeFile as writeFile8 } from "fs/promises";
1691
1726
  import { homedir as homedir9 } from "os";
1692
- import { join as join11 } from "path";
1727
+ import { join as join12 } from "path";
1693
1728
  import { randomUUID as randomUUID4 } from "crypto";
1694
1729
 
1695
1730
  // src/managers/claude-manager.ts
1696
1731
  import {
1697
1732
  query
1698
1733
  } from "@anthropic-ai/claude-agent-sdk";
1699
- import { join as join9 } from "path";
1734
+ import { join as join10 } from "path";
1700
1735
  import { mkdir as mkdir8, appendFile as appendFile4 } from "fs/promises";
1701
1736
  import { homedir as homedir7 } from "os";
1702
1737
 
@@ -2467,7 +2502,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2467
2502
  disallowedToolsOverride;
2468
2503
  constructor(options) {
2469
2504
  super(options);
2470
- this.historyFile = options.historyFilePath ?? join9(homedir7(), ".replicas", "claude", "history.jsonl");
2505
+ this.historyFile = options.historyFilePath ?? join10(homedir7(), ".replicas", "claude", "history.jsonl");
2471
2506
  this.systemPromptOverride = options.systemPromptOverride;
2472
2507
  this.toolsOverride = options.tools;
2473
2508
  this.mcpServersConfig = options.mcpServers;
@@ -2650,7 +2685,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
2650
2685
  };
2651
2686
  }
2652
2687
  async initialize() {
2653
- const historyDir = join9(homedir7(), ".replicas", "claude");
2688
+ const historyDir = join10(homedir7(), ".replicas", "claude");
2654
2689
  await mkdir8(historyDir, { recursive: true });
2655
2690
  if (this.initialSessionId) {
2656
2691
  this.sessionId = this.initialSessionId;
@@ -2704,11 +2739,11 @@ import { Codex } from "@openai/codex-sdk";
2704
2739
  import { randomUUID as randomUUID3 } from "crypto";
2705
2740
  import { readdir as readdir3, stat as stat2, writeFile as writeFile7, mkdir as mkdir9, readFile as readFile7 } from "fs/promises";
2706
2741
  import { existsSync as existsSync6 } from "fs";
2707
- import { join as join10 } from "path";
2742
+ import { join as join11 } from "path";
2708
2743
  import { homedir as homedir8 } from "os";
2709
2744
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2710
2745
  var DEFAULT_MODEL = "gpt-5.4";
2711
- var CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
2746
+ var CODEX_CONFIG_PATH = join11(homedir8(), ".codex", "config.toml");
2712
2747
  function isLinearThoughtEvent2(event) {
2713
2748
  return event.content.type === "thought";
2714
2749
  }
@@ -2732,7 +2767,7 @@ var CodexManager = class extends CodingAgentManager {
2732
2767
  this.codex = new Codex(
2733
2768
  ENGINE_ENV.OPENAI_API_KEY ? { apiKey: ENGINE_ENV.OPENAI_API_KEY } : void 0
2734
2769
  );
2735
- this.tempImageDir = join10(homedir8(), ".replicas", "codex", "temp-images");
2770
+ this.tempImageDir = join11(homedir8(), ".replicas", "codex", "temp-images");
2736
2771
  this.initializeManager(this.processMessageInternal.bind(this));
2737
2772
  }
2738
2773
  async initialize() {
@@ -2752,7 +2787,7 @@ var CodexManager = class extends CodingAgentManager {
2752
2787
  */
2753
2788
  async updateCodexConfig(developerInstructions) {
2754
2789
  try {
2755
- const codexDir = join10(homedir8(), ".codex");
2790
+ const codexDir = join11(homedir8(), ".codex");
2756
2791
  await mkdir9(codexDir, { recursive: true });
2757
2792
  let config = {};
2758
2793
  if (existsSync6(CODEX_CONFIG_PATH)) {
@@ -2788,7 +2823,7 @@ var CodexManager = class extends CodingAgentManager {
2788
2823
  for (const image of images) {
2789
2824
  const ext = image.source.media_type.split("/")[1] || "png";
2790
2825
  const filename = `img_${randomUUID3()}.${ext}`;
2791
- const filepath = join10(this.tempImageDir, filename);
2826
+ const filepath = join11(this.tempImageDir, filename);
2792
2827
  const buffer = Buffer.from(image.source.data, "base64");
2793
2828
  await writeFile7(filepath, buffer);
2794
2829
  tempPaths.push(filepath);
@@ -2927,13 +2962,13 @@ var CodexManager = class extends CodingAgentManager {
2927
2962
  }
2928
2963
  // Helper methods for finding session files
2929
2964
  async findSessionFile(threadId) {
2930
- const sessionsDir = join10(homedir8(), ".codex", "sessions");
2965
+ const sessionsDir = join11(homedir8(), ".codex", "sessions");
2931
2966
  try {
2932
2967
  const now = /* @__PURE__ */ new Date();
2933
2968
  const year = now.getFullYear();
2934
2969
  const month = String(now.getMonth() + 1).padStart(2, "0");
2935
2970
  const day = String(now.getDate()).padStart(2, "0");
2936
- const todayDir = join10(sessionsDir, String(year), month, day);
2971
+ const todayDir = join11(sessionsDir, String(year), month, day);
2937
2972
  const file = await this.findFileInDirectory(todayDir, threadId);
2938
2973
  if (file) return file;
2939
2974
  for (let daysAgo = 1; daysAgo <= 7; daysAgo++) {
@@ -2942,7 +2977,7 @@ var CodexManager = class extends CodingAgentManager {
2942
2977
  const searchYear = date.getFullYear();
2943
2978
  const searchMonth = String(date.getMonth() + 1).padStart(2, "0");
2944
2979
  const searchDay = String(date.getDate()).padStart(2, "0");
2945
- const searchDir = join10(sessionsDir, String(searchYear), searchMonth, searchDay);
2980
+ const searchDir = join11(sessionsDir, String(searchYear), searchMonth, searchDay);
2946
2981
  const file2 = await this.findFileInDirectory(searchDir, threadId);
2947
2982
  if (file2) return file2;
2948
2983
  }
@@ -2956,7 +2991,7 @@ var CodexManager = class extends CodingAgentManager {
2956
2991
  const files = await readdir3(directory);
2957
2992
  for (const file of files) {
2958
2993
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
2959
- const fullPath = join10(directory, file);
2994
+ const fullPath = join11(directory, file);
2960
2995
  const stats = await stat2(fullPath);
2961
2996
  if (stats.isFile()) {
2962
2997
  return fullPath;
@@ -3639,11 +3674,11 @@ var DuplicateDefaultChatError = class extends Error {
3639
3674
  };
3640
3675
 
3641
3676
  // src/services/chat/chat-service.ts
3642
- var ENGINE_DIR2 = join11(homedir9(), ".replicas", "engine");
3643
- var CHATS_FILE = join11(ENGINE_DIR2, "chats.json");
3644
- var CLAUDE_HISTORY_DIR = join11(ENGINE_DIR2, "claude-histories");
3645
- var RELAY_HISTORY_DIR = join11(ENGINE_DIR2, "relay-histories");
3646
- var CODEX_AUTH_PATH2 = join11(homedir9(), ".codex", "auth.json");
3677
+ var ENGINE_DIR2 = join12(homedir9(), ".replicas", "engine");
3678
+ var CHATS_FILE = join12(ENGINE_DIR2, "chats.json");
3679
+ var CLAUDE_HISTORY_DIR = join12(ENGINE_DIR2, "claude-histories");
3680
+ var RELAY_HISTORY_DIR = join12(ENGINE_DIR2, "relay-histories");
3681
+ var CODEX_AUTH_PATH2 = join12(homedir9(), ".codex", "auth.json");
3647
3682
  function isCodexAvailable() {
3648
3683
  return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
3649
3684
  }
@@ -3801,7 +3836,7 @@ var ChatService = class {
3801
3836
  async deleteHistoryFile(persisted) {
3802
3837
  if (persisted.provider === "claude" || persisted.provider === "relay") {
3803
3838
  const dir = persisted.provider === "claude" ? CLAUDE_HISTORY_DIR : RELAY_HISTORY_DIR;
3804
- await rm(join11(dir, `${persisted.id}.jsonl`), { force: true });
3839
+ await rm(join12(dir, `${persisted.id}.jsonl`), { force: true });
3805
3840
  }
3806
3841
  }
3807
3842
  async getChatHistory(chatId) {
@@ -3844,7 +3879,7 @@ var ChatService = class {
3844
3879
  if (persisted.provider === "claude") {
3845
3880
  provider = new ClaudeManager({
3846
3881
  workingDirectory: this.workingDirectory,
3847
- historyFilePath: join11(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
3882
+ historyFilePath: join12(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
3848
3883
  initialSessionId: persisted.providerSessionId,
3849
3884
  onSaveSessionId: saveSession,
3850
3885
  onTurnComplete: onProviderTurnComplete,
@@ -3853,7 +3888,7 @@ var ChatService = class {
3853
3888
  } else if (persisted.provider === "relay") {
3854
3889
  provider = new RelayManager({
3855
3890
  workingDirectory: this.workingDirectory,
3856
- historyFilePath: join11(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
3891
+ historyFilePath: join12(RELAY_HISTORY_DIR, `${persisted.id}.jsonl`),
3857
3892
  initialSessionId: persisted.providerSessionId,
3858
3893
  onSaveSessionId: saveSession,
3859
3894
  onTurnComplete: onProviderTurnComplete,
@@ -3874,7 +3909,8 @@ var ChatService = class {
3874
3909
  persisted,
3875
3910
  provider,
3876
3911
  pendingMessageIds: [],
3877
- hasActiveTurn: false
3912
+ hasActiveTurn: false,
3913
+ observedBranchesByRepo: /* @__PURE__ */ new Map()
3878
3914
  };
3879
3915
  }
3880
3916
  touch(chat) {
@@ -3922,6 +3958,8 @@ var ChatService = class {
3922
3958
  });
3923
3959
  }
3924
3960
  this.touch(chat);
3961
+ this.observeCurrentBranches(chat).catch(() => {
3962
+ });
3925
3963
  this.publish({
3926
3964
  type: "chat.turn.delta",
3927
3965
  payload: {
@@ -3937,7 +3975,7 @@ var ChatService = class {
3937
3975
  return;
3938
3976
  }
3939
3977
  if (!chat.hasActiveTurn) {
3940
- await this.publishAgentTurnCompleteWebhook();
3978
+ await this.publishAgentTurnCompleteWebhook(chat);
3941
3979
  return;
3942
3980
  }
3943
3981
  chat.hasActiveTurn = false;
@@ -3950,7 +3988,7 @@ var ChatService = class {
3950
3988
  }
3951
3989
  }).catch(() => {
3952
3990
  });
3953
- await this.publishAgentTurnCompleteWebhook();
3991
+ await this.publishAgentTurnCompleteWebhook(chat);
3954
3992
  }
3955
3993
  getRuntimeChat(chatId) {
3956
3994
  return this.chats.get(chatId) ?? null;
@@ -3986,15 +4024,31 @@ var ChatService = class {
3986
4024
  };
3987
4025
  await eventService.publish(event);
3988
4026
  }
3989
- async publishAgentTurnCompleteWebhook() {
4027
+ async observeCurrentBranches(chat) {
4028
+ try {
4029
+ const snapshot = await gitService.snapshotCurrentBranches();
4030
+ for (const [repoName, branch] of snapshot) {
4031
+ let observed = chat.observedBranchesByRepo.get(repoName);
4032
+ if (!observed) {
4033
+ observed = /* @__PURE__ */ new Set();
4034
+ chat.observedBranchesByRepo.set(repoName, observed);
4035
+ }
4036
+ observed.add(branch);
4037
+ }
4038
+ } catch {
4039
+ }
4040
+ }
4041
+ async publishAgentTurnCompleteWebhook(chat) {
3990
4042
  try {
3991
4043
  await githubTokenManager.refreshOnce();
3992
4044
  } catch {
3993
4045
  }
3994
4046
  const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
4047
+ const observedBranches = chat.observedBranchesByRepo;
4048
+ chat.observedBranchesByRepo = /* @__PURE__ */ new Map();
3995
4049
  let repoStatuses;
3996
4050
  try {
3997
- repoStatuses = await gitService.refreshRepos();
4051
+ repoStatuses = await gitService.refreshRepos(observedBranches);
3998
4052
  console.log(`Repository Statuses Refreshed: `, repoStatuses);
3999
4053
  } catch (error) {
4000
4054
  console.error("[ChatService] Failed to refresh repo statuses:", error);
@@ -4012,15 +4066,15 @@ var ChatService = class {
4012
4066
  import { Hono } from "hono";
4013
4067
  import { z as z2 } from "zod";
4014
4068
  import { readdir as readdir6, stat as stat3, readFile as readFile12 } from "fs/promises";
4015
- import { join as join15, resolve } from "path";
4069
+ import { join as join16, resolve } from "path";
4016
4070
 
4017
4071
  // src/services/plan-service.ts
4018
4072
  import { readdir as readdir4, readFile as readFile9 } from "fs/promises";
4019
4073
  import { homedir as homedir10 } from "os";
4020
- import { basename, join as join12 } from "path";
4074
+ import { basename, join as join13 } from "path";
4021
4075
  var PLAN_DIRECTORIES = [
4022
- join12(homedir10(), ".claude", "plans"),
4023
- join12(homedir10(), ".replicas", "plans")
4076
+ join13(homedir10(), ".claude", "plans"),
4077
+ join13(homedir10(), ".replicas", "plans")
4024
4078
  ];
4025
4079
  function isMarkdownFile(filename) {
4026
4080
  return filename.toLowerCase().endsWith(".md");
@@ -4054,7 +4108,7 @@ var PlanService = class {
4054
4108
  return null;
4055
4109
  }
4056
4110
  for (const directory of PLAN_DIRECTORIES) {
4057
- const filePath = join12(directory, safeFilename);
4111
+ const filePath = join13(directory, safeFilename);
4058
4112
  try {
4059
4113
  const content = await readFile9(filePath, "utf-8");
4060
4114
  return { filename: safeFilename, content };
@@ -4071,13 +4125,13 @@ import { execFile as execFile2 } from "child_process";
4071
4125
  import { promisify as promisify3 } from "util";
4072
4126
  import { readFile as readFile11 } from "fs/promises";
4073
4127
  import { existsSync as existsSync8 } from "fs";
4074
- import { join as join14 } from "path";
4128
+ import { join as join15 } from "path";
4075
4129
 
4076
4130
  // src/services/warm-hook-logs-service.ts
4077
4131
  import { createHash as createHash2 } from "crypto";
4078
4132
  import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile9, readdir as readdir5 } from "fs/promises";
4079
- import { join as join13 } from "path";
4080
- var LOGS_DIR2 = join13(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
4133
+ import { join as join14 } from "path";
4134
+ var LOGS_DIR2 = join14(SANDBOX_PATHS.REPLICAS_DIR, "warm-hook-logs");
4081
4135
  function sanitizeFilename2(name) {
4082
4136
  const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
4083
4137
  const hash = createHash2("sha256").update(name).digest("hex").slice(0, 8);
@@ -4104,7 +4158,7 @@ var WarmHookLogsService = class {
4104
4158
  hookName: "organization",
4105
4159
  ...entry
4106
4160
  };
4107
- await writeFile9(join13(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
4161
+ await writeFile9(join14(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
4108
4162
  `, "utf-8");
4109
4163
  }
4110
4164
  async saveRepoHookLog(repoName, entry) {
@@ -4114,7 +4168,7 @@ var WarmHookLogsService = class {
4114
4168
  hookName: repoName,
4115
4169
  ...entry
4116
4170
  };
4117
- await writeFile9(join13(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
4171
+ await writeFile9(join14(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
4118
4172
  `, "utf-8");
4119
4173
  }
4120
4174
  async getAllLogs() {
@@ -4133,7 +4187,7 @@ var WarmHookLogsService = class {
4133
4187
  continue;
4134
4188
  }
4135
4189
  try {
4136
- const raw = await readFile10(join13(LOGS_DIR2, file), "utf-8");
4190
+ const raw = await readFile10(join14(LOGS_DIR2, file), "utf-8");
4137
4191
  const stored = JSON.parse(raw);
4138
4192
  logs.push(withPreview2(stored));
4139
4193
  } catch {
@@ -4150,7 +4204,7 @@ var WarmHookLogsService = class {
4150
4204
  async getFullOutput(hookType, hookName) {
4151
4205
  const filename = hookType === "global" ? globalFilename() : repoFilename2(hookName);
4152
4206
  try {
4153
- const raw = await readFile10(join13(LOGS_DIR2, filename), "utf-8");
4207
+ const raw = await readFile10(join14(LOGS_DIR2, filename), "utf-8");
4154
4208
  const stored = JSON.parse(raw);
4155
4209
  if (stored.hookType !== hookType || stored.hookName !== hookName) {
4156
4210
  return null;
@@ -4197,7 +4251,7 @@ async function executeHookScript(params) {
4197
4251
  }
4198
4252
  async function readRepoWarmHook(repoPath) {
4199
4253
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
4200
- const configPath = join14(repoPath, filename);
4254
+ const configPath = join15(repoPath, filename);
4201
4255
  if (!existsSync8(configPath)) {
4202
4256
  continue;
4203
4257
  }
@@ -4724,7 +4778,7 @@ function createV1Routes(deps) {
4724
4778
  const logFiles = files.filter((f) => f.endsWith(".log"));
4725
4779
  const sessions = await Promise.all(
4726
4780
  logFiles.map(async (filename) => {
4727
- const filePath = join15(LOG_DIR, filename);
4781
+ const filePath = join16(LOG_DIR, filename);
4728
4782
  const fileStat = await stat3(filePath);
4729
4783
  const sessionId = filename.replace(/\.log$/, "");
4730
4784
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.117",
3
+ "version": "0.1.119",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",