replicas-engine 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -5,14 +5,76 @@ import "./chunk-ZXMDA7VB.js";
5
5
  import "dotenv/config";
6
6
  import { serve } from "@hono/node-server";
7
7
  import { Hono as Hono2 } from "hono";
8
- import { readFile as readFile8 } from "fs/promises";
9
- import { execSync as execSync2 } from "child_process";
10
- import { randomUUID as randomUUID5 } from "crypto";
8
+ import { readFile as readFile7 } from "fs/promises";
9
+ import { execSync } from "child_process";
10
+ import { randomUUID as randomUUID4 } from "crypto";
11
11
 
12
12
  // src/managers/github-token-manager.ts
13
13
  import { promises as fs } from "fs";
14
14
  import path from "path";
15
15
 
16
+ // src/engine-env.ts
17
+ import { homedir } from "os";
18
+ import { join } from "path";
19
+ function readEnv(name) {
20
+ const value = process.env[name]?.trim();
21
+ return value ? value : void 0;
22
+ }
23
+ function parsePort(value) {
24
+ if (!value) {
25
+ return 3737;
26
+ }
27
+ const parsed = Number(value);
28
+ if (!Number.isInteger(parsed) || parsed <= 0) {
29
+ throw new Error("Invalid engine environment: REPLICAS_ENGINE_PORT must be a positive integer");
30
+ }
31
+ return parsed;
32
+ }
33
+ function requireDefined(value, name) {
34
+ if (value === void 0 || value === null) {
35
+ throw new Error(`Invalid engine environment: ${name} is required`);
36
+ }
37
+ return value;
38
+ }
39
+ function requireValidURL(value, name) {
40
+ try {
41
+ new URL(value);
42
+ return value;
43
+ } catch {
44
+ throw new Error(`Invalid engine environment: ${name} must be a valid URL`);
45
+ }
46
+ }
47
+ function loadEngineEnv() {
48
+ const HOME_DIR = readEnv("HOME") ?? homedir();
49
+ return {
50
+ // Required boot env.
51
+ REPLICAS_ENGINE_SECRET: requireDefined(readEnv("REPLICAS_ENGINE_SECRET"), "REPLICAS_ENGINE_SECRET"),
52
+ WORKSPACE_ID: requireDefined(readEnv("WORKSPACE_ID"), "WORKSPACE_ID"),
53
+ MONOLITH_URL: requireValidURL(requireDefined(readEnv("MONOLITH_URL"), "MONOLITH_URL"), "MONOLITH_URL"),
54
+ // Engine config defaults.
55
+ REPLICAS_ENGINE_PORT: parsePort(readEnv("REPLICAS_ENGINE_PORT")),
56
+ HOME_DIR,
57
+ WORKSPACE_ROOT: join(HOME_DIR, "workspaces"),
58
+ // Engine-consumed optional values.
59
+ WORKSPACE_NAME: readEnv("WORKSPACE_NAME"),
60
+ LINEAR_SESSION_ID: readEnv("LINEAR_SESSION_ID"),
61
+ LINEAR_ACCESS_TOKEN: readEnv("LINEAR_ACCESS_TOKEN"),
62
+ SLACK_BOT_TOKEN: readEnv("SLACK_BOT_TOKEN"),
63
+ GH_TOKEN: readEnv("GH_TOKEN"),
64
+ // Ambient runtime values
65
+ // not directly used by the engine code, but are required in the VM
66
+ // for use by the agent or SDKs (e.g claude/codex)
67
+ ANTHROPIC_API_KEY: readEnv("ANTHROPIC_API_KEY"),
68
+ CLAUDE_CODE_USE_BEDROCK: readEnv("CLAUDE_CODE_USE_BEDROCK"),
69
+ AWS_ACCESS_KEY_ID: readEnv("AWS_ACCESS_KEY_ID"),
70
+ AWS_SECRET_ACCESS_KEY: readEnv("AWS_SECRET_ACCESS_KEY"),
71
+ AWS_REGION: readEnv("AWS_REGION"),
72
+ SLACK_CHANNEL_ID: readEnv("SLACK_CHANNEL_ID"),
73
+ SLACK_THREAD_TS: readEnv("SLACK_THREAD_TS")
74
+ };
75
+ }
76
+ var ENGINE_ENV = loadEngineEnv();
77
+
16
78
  // src/managers/base-refresh-manager.ts
17
79
  var BaseRefreshManager = class {
18
80
  constructor(managerName, intervalMs = 45 * 60 * 1e3) {
@@ -64,22 +126,17 @@ var BaseRefreshManager = class {
64
126
  return null;
65
127
  }
66
128
  getRuntimeConfig() {
67
- const monolithUrl = process.env.MONOLITH_URL;
68
- const workspaceId = process.env.WORKSPACE_ID;
69
- const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
70
- if (!monolithUrl || !workspaceId || !engineSecret) {
71
- return null;
72
- }
73
- return { monolithUrl, workspaceId, engineSecret };
129
+ return {
130
+ monolithUrl: ENGINE_ENV.MONOLITH_URL,
131
+ workspaceId: ENGINE_ENV.WORKSPACE_ID,
132
+ engineSecret: ENGINE_ENV.REPLICAS_ENGINE_SECRET
133
+ };
74
134
  }
75
135
  getSkipReasonForRun() {
76
136
  const skipReason = this.getSkipReason();
77
137
  if (skipReason) {
78
138
  return skipReason;
79
139
  }
80
- if (!this.getRuntimeConfig()) {
81
- return "missing MONOLITH_URL, WORKSPACE_ID, or REPLICAS_ENGINE_SECRET";
82
- }
83
140
  return null;
84
141
  }
85
142
  async refreshOnce() {
@@ -124,15 +181,12 @@ var GitHubTokenManager = class extends BaseRefreshManager {
124
181
  }
125
182
  const data = await response.json();
126
183
  await this.updateGitCredentials(data.token);
184
+ ENGINE_ENV.GH_TOKEN = data.token;
127
185
  process.env.GH_TOKEN = data.token;
128
186
  console.log(`[GitHubTokenManager] Token refreshed successfully, expires at ${data.expiresAt}`);
129
187
  }
130
188
  async updateGitCredentials(token) {
131
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME;
132
- if (!workspaceHome) {
133
- console.warn("[GitHubTokenManager] No WORKSPACE_HOME or HOME set, skipping git credentials update");
134
- return;
135
- }
189
+ const workspaceHome = ENGINE_ENV.HOME_DIR;
136
190
  const credentialsPath = path.join(workspaceHome, ".git-credentials");
137
191
  const credentialsContent = `https://x-access-token:${token}@github.com
138
192
  `;
@@ -154,7 +208,7 @@ var ClaudeTokenManager = class extends BaseRefreshManager {
154
208
  super("ClaudeTokenManager");
155
209
  }
156
210
  getSkipReason() {
157
- if (process.env.ANTHROPIC_API_KEY) {
211
+ if (ENGINE_ENV.ANTHROPIC_API_KEY) {
158
212
  return "ANTHROPIC_API_KEY is set";
159
213
  }
160
214
  return null;
@@ -178,11 +232,7 @@ var ClaudeTokenManager = class extends BaseRefreshManager {
178
232
  console.log(`[ClaudeTokenManager] Credentials refreshed successfully, expires at ${data.expiresAt}`);
179
233
  }
180
234
  async updateClaudeCredentials(credentials) {
181
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME;
182
- if (!workspaceHome) {
183
- console.warn("[ClaudeTokenManager] No WORKSPACE_HOME or HOME set, skipping credentials update");
184
- return;
185
- }
235
+ const workspaceHome = ENGINE_ENV.HOME_DIR;
186
236
  const claudeDir = path2.join(workspaceHome, ".claude");
187
237
  const credentialsPath = path2.join(claudeDir, ".credentials.json");
188
238
  const claudeCliConfig = {
@@ -231,11 +281,7 @@ var CodexTokenManager = class extends BaseRefreshManager {
231
281
  console.log(`[CodexTokenManager] Credentials refreshed successfully, expires at ${data.expiresAt}`);
232
282
  }
233
283
  async updateCodexCredentials(credentials) {
234
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME;
235
- if (!workspaceHome) {
236
- console.warn("[CodexTokenManager] No WORKSPACE_HOME or HOME set, skipping credentials update");
237
- return;
238
- }
284
+ const workspaceHome = ENGINE_ENV.HOME_DIR;
239
285
  const codexDir = path3.join(workspaceHome, ".codex");
240
286
  const authPath = path3.join(codexDir, "auth.json");
241
287
  const codexAuthConfig = {
@@ -259,49 +305,81 @@ var CodexTokenManager = class extends BaseRefreshManager {
259
305
  };
260
306
  var codexTokenManager = new CodexTokenManager();
261
307
 
262
- // src/services/git-init.ts
308
+ // src/git/service.ts
309
+ import { readdir, stat } from "fs/promises";
263
310
  import { existsSync as existsSync2 } from "fs";
264
- import path4 from "path";
265
-
266
- // src/utils/git.ts
267
- import { execSync } from "child_process";
311
+ import { execFileSync as execFileSync2 } from "child_process";
312
+ import { join as join3 } from "path";
268
313
 
269
- // src/services/engine-state.ts
314
+ // src/utils/state.ts
270
315
  import { readFile, writeFile, mkdir } from "fs/promises";
271
316
  import { existsSync } from "fs";
272
- import { join } from "path";
273
- import { homedir } from "os";
274
- var STATE_DIR = join(homedir(), ".replicas");
275
- var STATE_FILE = join(STATE_DIR, "engine-state.json");
276
- var DEFAULT_STATE = {
277
- branch: null,
278
- prUrl: null,
279
- claudeSessionId: null,
280
- codexThreadId: null,
281
- startHooksCompleted: false
282
- };
317
+ import { join as join2 } from "path";
318
+ import { homedir as homedir2 } from "os";
319
+
320
+ // src/utils/type-guards.ts
283
321
  function isRecord(value) {
284
322
  return typeof value === "object" && value !== null;
285
323
  }
324
+
325
+ // src/utils/state.ts
326
+ var STATE_DIR = join2(homedir2(), ".replicas");
327
+ var STATE_FILE = join2(STATE_DIR, "engine-state.json");
328
+ var DEFAULT_STATE = {
329
+ repos: {}
330
+ };
331
+ var stateWriteChain = Promise.resolve();
332
+ function enqueueStateWrite(operation) {
333
+ const result = stateWriteChain.then(operation);
334
+ stateWriteChain = result.then(() => void 0, () => void 0);
335
+ return result;
336
+ }
337
+ async function updateEngineState(updater) {
338
+ await enqueueStateWrite(async () => {
339
+ await mkdir(STATE_DIR, { recursive: true });
340
+ const currentState = await loadEngineState();
341
+ const nextState = updater(currentState);
342
+ await writeFile(STATE_FILE, JSON.stringify(nextState, null, 2), "utf-8");
343
+ });
344
+ }
345
+ function isEngineRepoDiff(value) {
346
+ return isRecord(value) && typeof value.added === "number" && typeof value.removed === "number";
347
+ }
348
+ function coerceRepoState(value) {
349
+ if (!isRecord(value)) {
350
+ return null;
351
+ }
352
+ if (typeof value.name !== "string") return null;
353
+ if (typeof value.path !== "string") return null;
354
+ if (typeof value.defaultBranch !== "string") return null;
355
+ if (typeof value.currentBranch !== "string") return null;
356
+ if (!(value.prUrl === null || typeof value.prUrl === "string")) return null;
357
+ if (!(value.gitDiff === null || isEngineRepoDiff(value.gitDiff))) return null;
358
+ if (typeof value.startHooksCompleted !== "boolean") return null;
359
+ return {
360
+ name: value.name,
361
+ path: value.path,
362
+ defaultBranch: value.defaultBranch,
363
+ currentBranch: value.currentBranch,
364
+ prUrl: value.prUrl,
365
+ gitDiff: value.gitDiff,
366
+ startHooksCompleted: value.startHooksCompleted
367
+ };
368
+ }
286
369
  function coerceEngineState(value) {
287
370
  if (!isRecord(value)) {
288
371
  return {};
289
372
  }
290
373
  const partial = {};
291
- if (value.branch === null || typeof value.branch === "string") {
292
- partial.branch = value.branch;
293
- }
294
- if (value.prUrl === null || typeof value.prUrl === "string") {
295
- partial.prUrl = value.prUrl;
296
- }
297
- if (value.claudeSessionId === null || typeof value.claudeSessionId === "string") {
298
- partial.claudeSessionId = value.claudeSessionId;
299
- }
300
- if (value.codexThreadId === null || typeof value.codexThreadId === "string") {
301
- partial.codexThreadId = value.codexThreadId;
302
- }
303
- if (typeof value.startHooksCompleted === "boolean") {
304
- partial.startHooksCompleted = value.startHooksCompleted;
374
+ if (isRecord(value.repos)) {
375
+ const repos = {};
376
+ for (const [repoName, repoState] of Object.entries(value.repos)) {
377
+ const coerced = coerceRepoState(repoState);
378
+ if (coerced) {
379
+ repos[repoName] = coerced;
380
+ }
381
+ }
382
+ partial.repos = repos;
305
383
  }
306
384
  return partial;
307
385
  }
@@ -314,33 +392,38 @@ async function loadEngineState() {
314
392
  const state = coerceEngineState(JSON.parse(content));
315
393
  return {
316
394
  ...DEFAULT_STATE,
317
- ...state
395
+ ...state,
396
+ repos: state.repos ?? {}
318
397
  };
319
398
  } catch (error) {
320
399
  console.error("[EngineState] Failed to load state, using defaults:", error);
321
400
  return { ...DEFAULT_STATE };
322
401
  }
323
402
  }
324
- async function saveEngineState(state) {
325
- try {
326
- await mkdir(STATE_DIR, { recursive: true });
327
- const currentState = await loadEngineState();
328
- const newState = {
403
+ async function loadRepoState(repoName) {
404
+ const state = await loadEngineState();
405
+ return state.repos[repoName] ?? null;
406
+ }
407
+ async function saveRepoState(repoName, state, fallbackState) {
408
+ await updateEngineState((currentState) => {
409
+ const currentRepoState = currentState.repos[repoName] ?? fallbackState;
410
+ return {
329
411
  ...currentState,
330
- ...state
412
+ repos: {
413
+ ...currentState.repos,
414
+ [repoName]: {
415
+ ...currentRepoState,
416
+ ...state
417
+ }
418
+ }
331
419
  };
332
- await writeFile(STATE_FILE, JSON.stringify(newState, null, 2), "utf-8");
333
- console.log("[EngineState] State saved:", newState);
334
- } catch (error) {
335
- console.error("[EngineState] Failed to save state:", error);
336
- throw error;
337
- }
420
+ });
338
421
  }
339
422
 
340
- // src/utils/git.ts
341
- var cachedPr = null;
342
- function runGitCommand(command, cwd) {
343
- return execSync(command, {
423
+ // src/git/commands.ts
424
+ import { execFileSync } from "child_process";
425
+ function runGitCommand(args, cwd) {
426
+ return execFileSync("git", args, {
344
427
  cwd,
345
428
  encoding: "utf-8",
346
429
  stdio: ["pipe", "pipe", "pipe"]
@@ -348,7 +431,7 @@ function runGitCommand(command, cwd) {
348
431
  }
349
432
  function branchExists(branchName, cwd) {
350
433
  try {
351
- runGitCommand(`git rev-parse --verify ${branchName}`, cwd);
434
+ runGitCommand(["rev-parse", "--verify", branchName], cwd);
352
435
  return true;
353
436
  } catch {
354
437
  return false;
@@ -356,229 +439,341 @@ function branchExists(branchName, cwd) {
356
439
  }
357
440
  function getCurrentBranch(cwd) {
358
441
  try {
359
- return runGitCommand("git rev-parse --abbrev-ref HEAD", cwd);
442
+ return runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
360
443
  } catch (error) {
361
444
  console.error("Error getting current branch:", error);
362
445
  return null;
363
446
  }
364
447
  }
365
- function getGitDiff(cwd) {
366
- try {
367
- const defaultBranch = process.env.REPLICAS_DEFAULT_BRANCH || "main";
368
- const baseBranch = `origin/${defaultBranch}`;
369
- const shortstat = execSync(`git diff ${baseBranch}...HEAD --shortstat -M`, {
370
- cwd,
371
- encoding: "utf-8",
372
- stdio: ["pipe", "pipe", "pipe"]
373
- }).trim();
374
- let added = 0;
375
- let removed = 0;
376
- const addedMatch = shortstat.match(/(\d+) insertion/);
377
- const removedMatch = shortstat.match(/(\d+) deletion/);
378
- if (addedMatch) {
379
- added = parseInt(addedMatch[1], 10);
380
- }
381
- if (removedMatch) {
382
- removed = parseInt(removedMatch[1], 10);
383
- }
384
- const fullDiff = execSync(`git diff ${baseBranch}...HEAD -M -C`, {
385
- cwd,
386
- encoding: "utf-8",
387
- stdio: ["pipe", "pipe", "pipe"]
388
- });
389
- return {
390
- added,
391
- removed,
392
- fullDiff
393
- };
394
- } catch (error) {
395
- console.error("Error getting git diff:", error);
396
- return null;
448
+
449
+ // src/git/service.ts
450
+ var GitService = class {
451
+ defaultBranchCache = /* @__PURE__ */ new Map();
452
+ cachedPrByRepo = /* @__PURE__ */ new Map();
453
+ getWorkspaceRoot() {
454
+ return ENGINE_ENV.WORKSPACE_ROOT;
397
455
  }
398
- }
399
- async function getPullRequestUrl(cwd) {
400
- try {
401
- const currentBranch = getCurrentBranch(cwd);
402
- if (!currentBranch) {
403
- return null;
456
+ async listRepositories() {
457
+ const root = this.getWorkspaceRoot();
458
+ const rootStat = await this.safeStat(root);
459
+ if (!rootStat?.isDirectory()) {
460
+ return [];
404
461
  }
405
- if (cachedPr && cachedPr.branch === currentBranch) {
406
- return cachedPr.prUrl;
462
+ const entries = await readdir(root);
463
+ const repos = [];
464
+ for (const entry of entries) {
465
+ const fullPath = join3(root, entry);
466
+ try {
467
+ const entryStat = await stat(fullPath);
468
+ if (!entryStat.isDirectory()) {
469
+ continue;
470
+ }
471
+ const hasGit = Boolean(await this.safeStat(join3(fullPath, ".git")));
472
+ if (!hasGit) {
473
+ continue;
474
+ }
475
+ repos.push({
476
+ name: entry,
477
+ path: fullPath,
478
+ defaultBranch: this.resolveDefaultBranch(fullPath)
479
+ });
480
+ } catch {
481
+ }
407
482
  }
408
- const persistedState = await loadEngineState();
409
- if (persistedState.prUrl && persistedState.branch === currentBranch) {
410
- cachedPr = {
411
- prUrl: persistedState.prUrl,
412
- branch: currentBranch
483
+ return repos.sort((a, b) => a.name.localeCompare(b.name));
484
+ }
485
+ async listRepos(options) {
486
+ const includeDiffs = options?.includeDiffs === true;
487
+ if (includeDiffs) {
488
+ const repos2 = await this.refreshRepos();
489
+ return repos2.map((repo) => ({
490
+ ...repo,
491
+ gitDiff: repo.gitDiff ? { ...repo.gitDiff, fullDiff: this.getFullGitDiff(repo.path, repo.defaultBranch) } : null
492
+ }));
493
+ }
494
+ const repos = await this.listRepositories();
495
+ const states = [];
496
+ for (const repo of repos) {
497
+ try {
498
+ const persistedState = await loadRepoState(repo.name);
499
+ const currentBranch = getCurrentBranch(repo.path) ?? repo.defaultBranch;
500
+ const persistedMatchesCurrentBranch = persistedState?.currentBranch === currentBranch;
501
+ states.push({
502
+ name: repo.name,
503
+ path: repo.path,
504
+ defaultBranch: repo.defaultBranch,
505
+ currentBranch,
506
+ prUrl: persistedMatchesCurrentBranch ? persistedState.prUrl : null,
507
+ gitDiff: persistedMatchesCurrentBranch ? persistedState.gitDiff : null,
508
+ startHooksCompleted: persistedState?.startHooksCompleted ?? false
509
+ });
510
+ } catch {
511
+ }
512
+ }
513
+ return states;
514
+ }
515
+ async refreshRepos() {
516
+ const repos = await this.listRepositories();
517
+ const states = [];
518
+ for (const repo of repos) {
519
+ try {
520
+ const persistedState = await loadRepoState(repo.name);
521
+ const currentBranch = getCurrentBranch(repo.path) ?? repo.defaultBranch;
522
+ const startHooksCompleted = persistedState?.startHooksCompleted ?? false;
523
+ states.push(await this.refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState));
524
+ } catch {
525
+ }
526
+ }
527
+ return states;
528
+ }
529
+ async initializeGitRepository() {
530
+ const workspaceName = ENGINE_ENV.WORKSPACE_NAME;
531
+ if (!workspaceName) {
532
+ return {
533
+ success: false,
534
+ repos: [],
535
+ error: "No WORKSPACE_NAME environment variable set"
413
536
  };
414
- return cachedPr.prUrl;
415
537
  }
416
- cachedPr = null;
417
- if (persistedState.prUrl && persistedState.branch !== currentBranch) {
418
- await saveEngineState({ prUrl: null });
538
+ const repos = await this.listRepositories();
539
+ if (repos.length === 0) {
540
+ return {
541
+ success: true,
542
+ repos: []
543
+ };
419
544
  }
545
+ const results = [];
546
+ for (const repo of repos) {
547
+ if (!this.pathExists(repo.path)) {
548
+ results.push({ name: repo.name, success: true, currentBranch: repo.defaultBranch });
549
+ continue;
550
+ }
551
+ try {
552
+ const baselineState = {
553
+ name: repo.name,
554
+ path: repo.path,
555
+ defaultBranch: repo.defaultBranch,
556
+ currentBranch: repo.defaultBranch,
557
+ prUrl: null,
558
+ gitDiff: null,
559
+ startHooksCompleted: false
560
+ };
561
+ const persistedState = await loadRepoState(repo.name);
562
+ const persistedBranch = persistedState?.currentBranch;
563
+ runGitCommand(["fetch", "--all", "--prune"], repo.path);
564
+ if (persistedBranch && branchExists(persistedBranch, repo.path)) {
565
+ const currentBranch = getCurrentBranch(repo.path);
566
+ if (currentBranch !== persistedBranch) {
567
+ runGitCommand(["checkout", persistedBranch], repo.path);
568
+ }
569
+ results.push({
570
+ name: repo.name,
571
+ success: true,
572
+ currentBranch: persistedBranch,
573
+ resumed: true
574
+ });
575
+ continue;
576
+ }
577
+ runGitCommand(["checkout", repo.defaultBranch], repo.path);
578
+ try {
579
+ runGitCommand(["pull", "--rebase", "--autostash"], repo.path);
580
+ } catch {
581
+ }
582
+ const branchName = this.findAvailableBranchName(workspaceName, repo.path);
583
+ runGitCommand(["checkout", "-b", branchName], repo.path);
584
+ await saveRepoState(repo.name, { currentBranch: branchName, prUrl: null }, baselineState);
585
+ results.push({
586
+ name: repo.name,
587
+ success: true,
588
+ currentBranch: branchName,
589
+ resumed: false
590
+ });
591
+ } catch (error) {
592
+ const errorMessage = error instanceof Error ? error.message : String(error);
593
+ results.push({
594
+ name: repo.name,
595
+ success: false,
596
+ currentBranch: repo.defaultBranch,
597
+ error: errorMessage
598
+ });
599
+ }
600
+ }
601
+ const failed = results.filter((result) => !result.success);
602
+ return {
603
+ success: failed.length === 0,
604
+ repos: results,
605
+ error: failed.length > 0 ? `Git init failed for ${failed.length} repo(s)` : void 0
606
+ };
607
+ }
608
+ getGitDiffStats(repoPath, defaultBranch) {
420
609
  try {
421
- const remoteRef = execSync(`git ls-remote --heads origin ${currentBranch}`, {
422
- cwd,
423
- encoding: "utf-8",
424
- stdio: ["pipe", "pipe", "pipe"]
425
- }).trim();
426
- if (!remoteRef) {
427
- return null;
610
+ const diffBase = this.getDiffBase(repoPath, defaultBranch);
611
+ const shortstat = runGitCommand(["diff", diffBase, "--shortstat", "-M"], repoPath);
612
+ let added = 0;
613
+ let removed = 0;
614
+ const addedMatch = shortstat.match(/(\d+) insertion/);
615
+ const removedMatch = shortstat.match(/(\d+) deletion/);
616
+ if (addedMatch) {
617
+ added = parseInt(addedMatch[1], 10);
618
+ }
619
+ if (removedMatch) {
620
+ removed = parseInt(removedMatch[1], 10);
428
621
  }
429
- } catch {
622
+ return {
623
+ added,
624
+ removed
625
+ };
626
+ } catch (error) {
627
+ console.error("Error getting git diff:", error);
430
628
  return null;
431
629
  }
630
+ }
631
+ getFullGitDiff(repoPath, defaultBranch) {
432
632
  try {
433
- const prInfo = execSync("gh pr view --json url --jq .url", {
434
- cwd,
633
+ const diffBase = this.getDiffBase(repoPath, defaultBranch);
634
+ return execFileSync2("git", ["diff", diffBase, "-M", "-C"], {
635
+ cwd: repoPath,
435
636
  encoding: "utf-8",
436
637
  stdio: ["pipe", "pipe", "pipe"]
437
- }).trim();
438
- if (prInfo) {
439
- cachedPr = {
440
- prUrl: prInfo,
441
- branch: currentBranch
442
- };
443
- await saveEngineState({ prUrl: prInfo });
638
+ });
639
+ } catch {
640
+ return "";
641
+ }
642
+ }
643
+ getDiffBase(repoPath, defaultBranch) {
644
+ const baseBranch = `origin/${defaultBranch}`;
645
+ try {
646
+ return runGitCommand(["merge-base", baseBranch, "HEAD"], repoPath);
647
+ } catch {
648
+ return baseBranch;
649
+ }
650
+ }
651
+ async getPullRequestUrl(repoName, repoPath, currentBranchArg, persistedRepoStateArg) {
652
+ try {
653
+ const currentBranch = currentBranchArg ?? getCurrentBranch(repoPath);
654
+ if (!currentBranch) {
655
+ return null;
656
+ }
657
+ const cachedPr = this.cachedPrByRepo.get(repoName);
658
+ if (cachedPr && cachedPr.currentBranch === currentBranch) {
444
659
  return cachedPr.prUrl;
445
660
  }
446
- } catch {
661
+ const persistedRepoState = persistedRepoStateArg ?? await loadRepoState(repoName);
662
+ if (persistedRepoState?.prUrl && persistedRepoState.currentBranch === currentBranch) {
663
+ this.cachedPrByRepo.set(repoName, {
664
+ prUrl: persistedRepoState.prUrl,
665
+ currentBranch
666
+ });
667
+ return persistedRepoState.prUrl;
668
+ }
669
+ this.cachedPrByRepo.delete(repoName);
670
+ if (persistedRepoState?.prUrl && persistedRepoState.currentBranch !== currentBranch) {
671
+ await saveRepoState(repoName, { prUrl: null }, persistedRepoState);
672
+ }
673
+ try {
674
+ const remoteRef = execFileSync2("git", ["ls-remote", "--heads", "origin", currentBranch], {
675
+ cwd: repoPath,
676
+ encoding: "utf-8",
677
+ stdio: ["pipe", "pipe", "pipe"]
678
+ }).trim();
679
+ if (!remoteRef) {
680
+ return null;
681
+ }
682
+ } catch {
683
+ return null;
684
+ }
685
+ try {
686
+ const prInfo = execFileSync2("gh", ["pr", "view", "--json", "url", "--jq", ".url"], {
687
+ cwd: repoPath,
688
+ encoding: "utf-8",
689
+ stdio: ["pipe", "pipe", "pipe"]
690
+ }).trim();
691
+ if (prInfo) {
692
+ this.cachedPrByRepo.set(repoName, {
693
+ prUrl: prInfo,
694
+ currentBranch
695
+ });
696
+ if (persistedRepoState) {
697
+ await saveRepoState(repoName, { prUrl: prInfo }, persistedRepoState);
698
+ }
699
+ return prInfo;
700
+ }
701
+ } catch {
702
+ return null;
703
+ }
704
+ return null;
705
+ } catch (error) {
706
+ console.error("Error checking for pull request:", error);
447
707
  return null;
448
708
  }
449
- return null;
450
- } catch (error) {
451
- console.error("Error checking for pull request:", error);
452
- return null;
453
709
  }
454
- }
455
- async function getGitStatus(workingDirectory) {
456
- return {
457
- branch: getCurrentBranch(workingDirectory),
458
- gitDiff: getGitDiff(workingDirectory),
459
- prUrl: await getPullRequestUrl(workingDirectory)
460
- };
461
- }
462
-
463
- // src/services/git-init.ts
464
- var initializedBranch = null;
465
- function findAvailableBranchName(baseName, cwd) {
466
- if (!branchExists(baseName, cwd)) {
467
- return baseName;
710
+ resolveDefaultBranch(repoPath) {
711
+ const cached = this.defaultBranchCache.get(repoPath);
712
+ if (cached) {
713
+ return cached;
714
+ }
715
+ const fromSymbolicRef = this.resolveDefaultBranchFromSymbolicRef(repoPath);
716
+ if (fromSymbolicRef) {
717
+ this.defaultBranchCache.set(repoPath, fromSymbolicRef);
718
+ return fromSymbolicRef;
719
+ }
720
+ const fallback = "main";
721
+ this.defaultBranchCache.set(repoPath, fallback);
722
+ return fallback;
468
723
  }
469
- return `${baseName}-${Date.now()}`;
470
- }
471
- async function initializeGitRepository() {
472
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME;
473
- const repoName = process.env.REPLICAS_REPO_NAME;
474
- const workspaceName = process.env.WORKSPACE_NAME;
475
- const defaultBranch = process.env.REPLICAS_DEFAULT_BRANCH || "main";
476
- if (!workspaceHome) {
477
- return {
478
- success: false,
479
- branch: null,
480
- error: "No WORKSPACE_HOME or HOME environment variable set"
481
- };
724
+ resolveDefaultBranchFromSymbolicRef(repoPath) {
725
+ try {
726
+ const output = runGitCommand(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoPath);
727
+ const match = output.match(/^origin\/(.+)$/);
728
+ return match ? match[1] : null;
729
+ } catch {
730
+ return null;
731
+ }
482
732
  }
483
- if (!repoName) {
484
- console.log("[GitInit] No REPLICAS_REPO_NAME set, skipping git initialization");
485
- return {
486
- success: true,
487
- branch: null
488
- };
733
+ findAvailableBranchName(baseName, cwd) {
734
+ const sanitizedBaseName = this.sanitizeBranchName(baseName);
735
+ if (!branchExists(sanitizedBaseName, cwd)) {
736
+ return sanitizedBaseName;
737
+ }
738
+ return `${sanitizedBaseName}-${Date.now()}`;
489
739
  }
490
- if (!workspaceName) {
491
- return {
492
- success: false,
493
- branch: null,
494
- error: "No WORKSPACE_NAME environment variable set"
495
- };
740
+ sanitizeBranchName(name) {
741
+ const normalized = name.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/\/{2,}/g, "/").replace(/^-+|-+$/g, "");
742
+ return normalized || "replicas";
496
743
  }
497
- const repoPath = path4.join(workspaceHome, "workspaces", repoName);
498
- if (!existsSync2(repoPath)) {
499
- console.log(`[GitInit] Repository directory does not exist: ${repoPath}`);
500
- console.log("[GitInit] Waiting for initializer to clone the repository...");
501
- return {
502
- success: true,
503
- branch: null
744
+ async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState) {
745
+ const state = {
746
+ name: repo.name,
747
+ path: repo.path,
748
+ defaultBranch: repo.defaultBranch,
749
+ currentBranch,
750
+ prUrl: await this.getPullRequestUrl(repo.name, repo.path, currentBranch, persistedState),
751
+ gitDiff: this.getGitDiffStats(repo.path, repo.defaultBranch),
752
+ startHooksCompleted
504
753
  };
754
+ await saveRepoState(repo.name, state, state);
755
+ return state;
505
756
  }
506
- if (!existsSync2(path4.join(repoPath, ".git"))) {
507
- return {
508
- success: false,
509
- branch: null,
510
- error: `Directory exists but is not a git repository: ${repoPath}`
511
- };
757
+ pathExists(path4) {
758
+ return existsSync2(path4);
512
759
  }
513
- console.log(`[GitInit] Initializing repository at ${repoPath}`);
514
- try {
515
- const persistedState = await loadEngineState();
516
- const persistedBranch = persistedState.branch;
517
- console.log("[GitInit] Fetching all remotes...");
518
- runGitCommand("git fetch --all --prune", repoPath);
519
- if (persistedBranch && branchExists(persistedBranch, repoPath)) {
520
- console.log(`[GitInit] Found persisted branch: ${persistedBranch}`);
521
- const currentBranch = getCurrentBranch(repoPath);
522
- if (currentBranch === persistedBranch) {
523
- console.log(`[GitInit] Already on persisted branch: ${persistedBranch}`);
524
- initializedBranch = persistedBranch;
525
- return {
526
- success: true,
527
- branch: persistedBranch,
528
- resumed: true
529
- };
530
- }
531
- console.log(`[GitInit] Resuming on persisted branch: ${persistedBranch}`);
532
- runGitCommand(`git checkout ${persistedBranch}`, repoPath);
533
- initializedBranch = persistedBranch;
534
- console.log(`[GitInit] Successfully resumed on branch: ${persistedBranch}`);
535
- return {
536
- success: true,
537
- branch: persistedBranch,
538
- resumed: true
539
- };
540
- }
541
- console.log(`[GitInit] Checking out default branch: ${defaultBranch}`);
542
- runGitCommand(`git checkout ${defaultBranch}`, repoPath);
543
- console.log("[GitInit] Pulling latest changes...");
760
+ async safeStat(path4) {
544
761
  try {
545
- runGitCommand("git pull --rebase --autostash", repoPath);
546
- } catch (pullError) {
547
- console.warn("[GitInit] Pull had issues, continuing anyway:", pullError);
548
- }
549
- const branchName = findAvailableBranchName(workspaceName, repoPath);
550
- if (branchName !== workspaceName) {
551
- console.log(`[GitInit] Branch "${workspaceName}" already exists, using "${branchName}" instead`);
552
- }
553
- console.log(`[GitInit] Creating workspace branch: ${branchName}`);
554
- runGitCommand(`git checkout -b ${branchName}`, repoPath);
555
- initializedBranch = branchName;
556
- await saveEngineState({ branch: branchName, prUrl: null });
557
- console.log(`[GitInit] Successfully initialized on branch: ${branchName}`);
558
- return {
559
- success: true,
560
- branch: branchName,
561
- resumed: false
562
- };
563
- } catch (error) {
564
- const errorMessage = error instanceof Error ? error.message : String(error);
565
- console.error("[GitInit] Failed to initialize repository:", errorMessage);
566
- return {
567
- success: false,
568
- branch: null,
569
- error: errorMessage
570
- };
762
+ return await stat(path4);
763
+ } catch {
764
+ return null;
765
+ }
571
766
  }
572
- }
767
+ };
768
+ var gitService = new GitService();
573
769
 
574
770
  // src/utils/logger.ts
575
- import { appendFile, mkdir as mkdir2, readdir, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
576
- import { homedir as homedir2 } from "os";
577
- import { basename, join as join2 } from "path";
771
+ import { appendFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
772
+ import { homedir as homedir3 } from "os";
773
+ import { join as join4 } from "path";
578
774
  import { format } from "util";
579
775
  import { randomBytes } from "crypto";
580
- var LOG_DIR = join2(homedir2(), ".replicas", "logs");
581
- var MAX_READ_LIMIT = 5e3;
776
+ var LOG_DIR = join4(homedir3(), ".replicas", "logs");
582
777
  var EngineLogger = class {
583
778
  sessionId = null;
584
779
  filePath = null;
@@ -587,69 +782,12 @@ var EngineLogger = class {
587
782
  async initialize() {
588
783
  await mkdir2(LOG_DIR, { recursive: true });
589
784
  this.sessionId = this.createSessionId();
590
- this.filePath = join2(LOG_DIR, `${this.sessionId}.log`);
785
+ this.filePath = join4(LOG_DIR, `${this.sessionId}.log`);
591
786
  await writeFile2(this.filePath, `=== Replicas Engine Session ${this.sessionId} ===
592
787
  `, "utf-8");
593
788
  this.patchConsole();
594
789
  this.log("INFO", `Engine logging initialized at ${this.filePath}`);
595
790
  }
596
- getCurrentSessionId() {
597
- return this.sessionId;
598
- }
599
- async listSessions() {
600
- await mkdir2(LOG_DIR, { recursive: true });
601
- const files = await readdir(LOG_DIR);
602
- const sessions = [];
603
- for (const file of files) {
604
- if (!file.endsWith(".log")) continue;
605
- const fullPath = join2(LOG_DIR, file);
606
- const fileStat = await stat(fullPath);
607
- if (!fileStat.isFile()) continue;
608
- sessions.push({
609
- sessionId: file.replace(/\.log$/, ""),
610
- filename: file,
611
- sizeBytes: fileStat.size,
612
- updatedAt: fileStat.mtime.toISOString()
613
- });
614
- }
615
- sessions.sort((a, b) => b.filename.localeCompare(a.filename));
616
- return sessions;
617
- }
618
- async readSession(sessionId, options = {}) {
619
- const normalized = this.normalizeSessionId(sessionId);
620
- const filePath = join2(LOG_DIR, `${normalized}.log`);
621
- const content = await readFile2(filePath, "utf-8");
622
- const allLines = content.split("\n");
623
- if (allLines[allLines.length - 1] === "") {
624
- allLines.pop();
625
- }
626
- const totalLines = allLines.length;
627
- const limit = this.normalizeLimit(options.limit);
628
- if (options.tail !== void 0) {
629
- const tail = Math.max(0, Math.min(MAX_READ_LIMIT, options.tail));
630
- const start = Math.max(0, totalLines - tail);
631
- const lines2 = allLines.slice(start, start + tail);
632
- return {
633
- sessionId: normalized,
634
- totalLines,
635
- offset: start,
636
- limit: tail,
637
- hasMore: start > 0,
638
- lines: lines2
639
- };
640
- }
641
- const offset = Math.max(0, options.offset ?? 0);
642
- const lines = allLines.slice(offset, offset + limit);
643
- const hasMore = offset + lines.length < totalLines;
644
- return {
645
- sessionId: normalized,
646
- totalLines,
647
- offset,
648
- limit,
649
- hasMore,
650
- lines
651
- };
652
- }
653
791
  patchConsole() {
654
792
  if (this.patched) return;
655
793
  this.patched = true;
@@ -687,69 +825,28 @@ var EngineLogger = class {
687
825
  const suffix = randomBytes(3).toString("hex");
688
826
  return `${timestamp}-${suffix}`;
689
827
  }
690
- normalizeLimit(limit) {
691
- if (limit === void 0) return 200;
692
- if (!Number.isFinite(limit)) return 200;
693
- if (limit < 1) return 1;
694
- return Math.min(MAX_READ_LIMIT, limit);
695
- }
696
- normalizeSessionId(sessionId) {
697
- const trimmed = sessionId.trim();
698
- if (!trimmed || basename(trimmed) !== trimmed || !/^[A-Za-z0-9._-]+$/.test(trimmed)) {
699
- throw new Error("Invalid sessionId");
700
- }
701
- return trimmed;
702
- }
703
828
  };
704
829
  var engineLogger = new EngineLogger();
705
830
 
706
- // src/services/replicas-config.ts
707
- import { readFile as readFile3, appendFile as appendFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
831
+ // src/services/replicas-config-service.ts
832
+ import { readFile as readFile2, appendFile as appendFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
708
833
  import { existsSync as existsSync3 } from "fs";
709
- import { join as join3 } from "path";
710
- import { homedir as homedir3 } from "os";
834
+ import { join as join5 } from "path";
835
+ import { homedir as homedir4 } from "os";
711
836
  import { exec } from "child_process";
712
837
  import { promisify } from "util";
713
-
714
- // ../shared/src/sandbox.ts
715
- var SANDBOX_LIFECYCLE = {
716
- AUTO_STOP_MINUTES: 60,
717
- AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
718
- AUTO_DELETE_MINUTES: -1,
719
- SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
720
- };
721
-
722
- // ../shared/src/prompts.ts
723
- var GENERAL_INSTRUCTIONS_TAG = "general_instructions";
724
- function wrapInTag(content, tag) {
725
- return `<${tag}>
726
- ${content}
727
- </${tag}>`;
728
- }
729
-
730
- // ../shared/src/engine/types.ts
731
- var DEFAULT_CHAT_TITLES = {
732
- claude: "Claude Code",
733
- codex: "Codex"
734
- };
735
- var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
736
-
737
- // src/services/replicas-config.ts
738
838
  var execAsync = promisify(exec);
739
- var START_HOOKS_LOG = join3(homedir3(), ".replicas", "startHooks.log");
839
+ var START_HOOKS_LOG = join5(homedir4(), ".replicas", "startHooks.log");
740
840
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
741
- Start hooks are shell commands/scripts set by the repository owners that run on workspace startup.
841
+ Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
742
842
  These hooks are currently executing in the background. You can:
743
843
  - Check the status and output by reading ~/.replicas/startHooks.log
744
- - View the hook commands in replicas.json in the repository root (under the "startHook" field)
844
+ - View hook commands in each repository's replicas.json (under the "startHook" field)
745
845
 
746
846
  The start hooks may install dependencies, build projects, or perform other setup tasks.
747
847
  If your task depends on setup being complete, check the log file before proceeding.`;
748
- function isRecord2(value) {
749
- return typeof value === "object" && value !== null;
750
- }
751
848
  function parseReplicasConfig(value) {
752
- if (!isRecord2(value)) {
849
+ if (!isRecord(value)) {
753
850
  throw new Error("Invalid replicas.json: expected an object");
754
851
  }
755
852
  const config = {};
@@ -778,7 +875,7 @@ function parseReplicasConfig(value) {
778
875
  config.systemPrompt = value.systemPrompt;
779
876
  }
780
877
  if ("startHook" in value) {
781
- if (!isRecord2(value.startHook)) {
878
+ if (!isRecord(value.startHook)) {
782
879
  throw new Error('Invalid replicas.json: "startHook" must be an object with "commands" array');
783
880
  }
784
881
  const { commands, timeout } = value.startHook;
@@ -793,151 +890,149 @@ function parseReplicasConfig(value) {
793
890
  return config;
794
891
  }
795
892
  var ReplicasConfigService = class {
796
- config = null;
797
- workingDirectory;
893
+ configs = [];
798
894
  hooksRunning = false;
799
895
  hooksCompleted = false;
800
896
  hooksFailed = false;
801
- startHooksPromise = null;
802
- constructor() {
803
- const repoName = process.env.REPLICAS_REPO_NAME;
804
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir3();
805
- if (repoName) {
806
- this.workingDirectory = join3(workspaceHome, "workspaces", repoName);
807
- } else {
808
- this.workingDirectory = workspaceHome;
809
- }
810
- }
811
897
  /**
812
- * Initialize the service by reading replicas.json and starting start hooks asynchronously
813
- * Start hooks run in the background and don't block engine startup
898
+ * Initialize by reading all replicas.json files and running start hooks.
814
899
  */
815
900
  async initialize() {
816
- await this.loadConfig();
817
- this.startHooksPromise = this.executeStartHooks();
901
+ await this.loadConfigs();
902
+ void this.executeStartHooks().catch((error) => {
903
+ const errorMessage = error instanceof Error ? error.message : String(error);
904
+ this.hooksFailed = true;
905
+ this.hooksRunning = false;
906
+ console.error("[ReplicasConfig] Start hooks execution failed:", errorMessage);
907
+ });
818
908
  }
819
909
  /**
820
- * Load and parse the replicas.json config file
910
+ * Load and parse replicas.json from each discovered repository root.
821
911
  */
822
- async loadConfig() {
823
- const configPath = join3(this.workingDirectory, "replicas.json");
824
- if (!existsSync3(configPath)) {
825
- console.log("No replicas.json found in workspace directory");
826
- this.config = null;
827
- return;
828
- }
829
- try {
830
- const data = await readFile3(configPath, "utf-8");
831
- const config = parseReplicasConfig(JSON.parse(data));
832
- this.config = config;
833
- console.log("Loaded replicas.json config:", {
834
- hasSystemPrompt: !!config.systemPrompt,
835
- startHookCount: config.startHook?.commands.length ?? 0
836
- });
837
- } catch (error) {
838
- if (error instanceof SyntaxError) {
839
- console.error("Failed to parse replicas.json:", error.message);
840
- } else if (error instanceof Error) {
841
- console.error("Error loading replicas.json:", error.message);
912
+ async loadConfigs() {
913
+ const repos = await gitService.listRepositories();
914
+ const configs = [];
915
+ for (const repo of repos) {
916
+ const configPath = join5(repo.path, "replicas.json");
917
+ if (!existsSync3(configPath)) {
918
+ continue;
919
+ }
920
+ try {
921
+ const data = await readFile2(configPath, "utf-8");
922
+ const config = parseReplicasConfig(JSON.parse(data));
923
+ configs.push({
924
+ repoName: repo.name,
925
+ workingDirectory: repo.path,
926
+ defaultBranch: repo.defaultBranch,
927
+ config
928
+ });
929
+ } catch (error) {
930
+ if (error instanceof SyntaxError) {
931
+ console.error(`[ReplicasConfig] Failed to parse ${repo.name}/replicas.json:`, error.message);
932
+ } else if (error instanceof Error) {
933
+ console.error(`[ReplicasConfig] Error loading ${repo.name}/replicas.json:`, error.message);
934
+ }
842
935
  }
843
- this.config = null;
844
936
  }
937
+ this.configs = configs;
845
938
  }
846
939
  /**
847
- * Write a message to the start hooks log file
940
+ * Write a message to the start hooks log file.
848
941
  */
849
942
  async logToFile(message) {
850
943
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
851
944
  const logLine = `[${timestamp}] ${message}
852
945
  `;
853
946
  try {
854
- await mkdir3(join3(homedir3(), ".replicas"), { recursive: true });
947
+ await mkdir3(join5(homedir4(), ".replicas"), { recursive: true });
855
948
  await appendFile2(START_HOOKS_LOG, logLine, "utf-8");
856
949
  } catch (error) {
857
950
  console.error("Failed to write to start hooks log:", error);
858
951
  }
859
952
  }
860
953
  /**
861
- * Execute all start hooks defined in replicas.json
862
- * Writes output to ~/.replicas/startHooks.log
863
- * Only runs once per workspace lifecycle (persisted across sleep/wake cycles)
954
+ * Execute start hooks from all repositories sequentially.
864
955
  */
865
956
  async executeStartHooks() {
866
- const persistedState = await loadEngineState();
867
- if (persistedState.startHooksCompleted) {
868
- console.log("Start hooks already completed in previous session, skipping");
957
+ const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
958
+ if (hookEntries.length === 0) {
959
+ this.hooksRunning = false;
869
960
  this.hooksCompleted = true;
870
961
  this.hooksFailed = false;
871
- await this.logToFile("Start hooks already completed in previous session, skipping");
872
962
  return;
873
963
  }
874
- const startHookConfig = this.config?.startHook;
875
- if (!startHookConfig || startHookConfig.commands.length === 0) {
876
- this.hooksCompleted = true;
877
- this.hooksFailed = false;
878
- await saveEngineState({ startHooksCompleted: true });
879
- return;
880
- }
881
- const timeout = startHookConfig.timeout ?? 3e5;
882
- const hooks = startHookConfig.commands;
883
964
  this.hooksRunning = true;
884
- await mkdir3(join3(homedir3(), ".replicas"), { recursive: true });
885
- await writeFile3(START_HOOKS_LOG, `=== Start Hooks Execution Log ===
965
+ this.hooksCompleted = false;
966
+ try {
967
+ await mkdir3(join5(homedir4(), ".replicas"), { recursive: true });
968
+ await writeFile3(
969
+ START_HOOKS_LOG,
970
+ `=== Start Hooks Execution Log ===
886
971
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
887
- Commands: ${hooks.length}
972
+ Repositories: ${hookEntries.length}
888
973
 
889
- `, "utf-8");
890
- console.log(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
891
- await this.logToFile(`Executing ${hooks.length} start hook(s) with timeout ${timeout}ms...`);
892
- for (const hook of hooks) {
893
- try {
894
- console.log(`Running start hook: ${hook}`);
895
- await this.logToFile(`
896
- --- Running: ${hook} ---`);
897
- const { stdout, stderr } = await execAsync(hook, {
898
- cwd: this.workingDirectory,
899
- timeout,
900
- env: process.env
901
- });
902
- if (stdout) {
903
- console.log(`[${hook}] stdout:`, stdout);
904
- await this.logToFile(`[stdout] ${stdout}`);
974
+ `,
975
+ "utf-8"
976
+ );
977
+ for (const entry of hookEntries) {
978
+ const startHookConfig = entry.config.startHook;
979
+ if (!startHookConfig) {
980
+ continue;
905
981
  }
906
- if (stderr) {
907
- console.warn(`[${hook}] stderr:`, stderr);
908
- await this.logToFile(`[stderr] ${stderr}`);
982
+ const persistedRepoState = await loadRepoState(entry.repoName);
983
+ if (persistedRepoState?.startHooksCompleted) {
984
+ await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
985
+ continue;
909
986
  }
910
- console.log(`Start hook completed: ${hook}`);
911
- await this.logToFile(`--- Completed: ${hook} ---`);
912
- } catch (error) {
913
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
914
- this.hooksFailed = true;
915
- console.error(`Start hook failed: ${hook}`, errorMessage);
916
- await this.logToFile(`[ERROR] ${hook} failed: ${errorMessage}`);
987
+ const timeout = startHookConfig.timeout ?? 3e5;
988
+ await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
989
+ for (const hook of startHookConfig.commands) {
990
+ try {
991
+ await this.logToFile(`[${entry.repoName}] --- Running: ${hook} ---`);
992
+ const { stdout, stderr } = await execAsync(hook, {
993
+ cwd: entry.workingDirectory,
994
+ timeout,
995
+ env: process.env
996
+ });
997
+ if (stdout) {
998
+ await this.logToFile(`[${entry.repoName}] [stdout] ${stdout}`);
999
+ }
1000
+ if (stderr) {
1001
+ await this.logToFile(`[${entry.repoName}] [stderr] ${stderr}`);
1002
+ }
1003
+ await this.logToFile(`[${entry.repoName}] --- Completed: ${hook} ---`);
1004
+ } catch (error) {
1005
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1006
+ this.hooksFailed = true;
1007
+ await this.logToFile(`[${entry.repoName}] [ERROR] ${hook} failed: ${errorMessage}`);
1008
+ }
1009
+ }
1010
+ const fallbackRepoState = persistedRepoState ?? {
1011
+ name: entry.repoName,
1012
+ path: entry.workingDirectory,
1013
+ defaultBranch: entry.defaultBranch,
1014
+ currentBranch: entry.defaultBranch,
1015
+ prUrl: null,
1016
+ gitDiff: null,
1017
+ startHooksCompleted: false
1018
+ };
1019
+ await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
917
1020
  }
1021
+ this.hooksCompleted = true;
1022
+ await this.logToFile(`=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
1023
+ } catch (error) {
1024
+ const errorMessage = error instanceof Error ? error.message : String(error);
1025
+ this.hooksFailed = true;
1026
+ this.hooksCompleted = false;
1027
+ console.error("[ReplicasConfig] Start hooks execution failed:", errorMessage);
1028
+ await this.logToFile(`[SYSTEM] [ERROR] Start hooks execution failed: ${errorMessage}`);
1029
+ throw error;
1030
+ } finally {
1031
+ this.hooksRunning = false;
918
1032
  }
919
- this.hooksRunning = false;
920
- this.hooksCompleted = true;
921
- await saveEngineState({ startHooksCompleted: true });
922
- console.log("All start hooks completed");
923
- await this.logToFile(`
924
- === All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
925
- }
926
- /**
927
- * Get the system prompt from replicas.json
928
- */
929
- getSystemPrompt() {
930
- if (!this.config?.systemPrompt) return void 0;
931
- return wrapInTag(this.config.systemPrompt, GENERAL_INSTRUCTIONS_TAG);
932
- }
933
- /**
934
- * Get the full config object
935
- */
936
- getConfig() {
937
- return this.config;
938
1033
  }
939
1034
  /**
940
- * Check if start hooks are currently running
1035
+ * Check if start hooks are currently running.
941
1036
  */
942
1037
  areHooksRunning() {
943
1038
  return this.hooksRunning;
@@ -949,21 +1044,42 @@ Commands: ${hooks.length}
949
1044
  return this.hooksFailed;
950
1045
  }
951
1046
  /**
952
- * Get start hook configuration (for system prompt)
1047
+ * Get aggregated start hook metadata.
953
1048
  */
954
1049
  getStartHookConfig() {
955
- return this.config?.startHook;
1050
+ const commands = this.configs.flatMap((entry) => {
1051
+ const repoCommands = entry.config.startHook?.commands ?? [];
1052
+ return repoCommands.map((command) => `[${entry.repoName}] ${command}`);
1053
+ });
1054
+ if (commands.length === 0) {
1055
+ return void 0;
1056
+ }
1057
+ return { commands };
1058
+ }
1059
+ getSystemPrompts() {
1060
+ const prompts = [];
1061
+ for (const entry of this.configs) {
1062
+ if (typeof entry.config.systemPrompt !== "string") {
1063
+ continue;
1064
+ }
1065
+ const prompt = entry.config.systemPrompt.trim();
1066
+ if (!prompt) {
1067
+ continue;
1068
+ }
1069
+ prompts.push(`[${entry.repoName}] ${prompt}`);
1070
+ }
1071
+ return prompts;
956
1072
  }
957
1073
  };
958
1074
  var replicasConfigService = new ReplicasConfigService();
959
1075
 
960
- // src/event-service.ts
1076
+ // src/services/event-service.ts
961
1077
  import { appendFile as appendFile3, mkdir as mkdir4 } from "fs/promises";
962
- import { homedir as homedir4 } from "os";
963
- import { join as join4 } from "path";
1078
+ import { homedir as homedir5 } from "os";
1079
+ import { join as join6 } from "path";
964
1080
  import { randomUUID } from "crypto";
965
- var ENGINE_DIR = join4(homedir4(), ".replicas", "engine");
966
- var EVENTS_FILE = join4(ENGINE_DIR, "events.jsonl");
1081
+ var ENGINE_DIR = join6(homedir5(), ".replicas", "engine");
1082
+ var EVENTS_FILE = join6(ENGINE_DIR, "events.jsonl");
967
1083
  var EventService = class {
968
1084
  subscribers = /* @__PURE__ */ new Map();
969
1085
  writeChain = Promise.resolve();
@@ -997,98 +1113,46 @@ var EventService = class {
997
1113
  };
998
1114
  var eventService = new EventService();
999
1115
 
1000
- // src/repo-service.ts
1001
- import { readdir as readdir2, stat as stat2 } from "fs/promises";
1002
- import { join as join5 } from "path";
1003
- import { homedir as homedir5 } from "os";
1004
- var RepoService = class {
1005
- getWorkspaceRoot() {
1006
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir5();
1007
- return join5(workspaceHome, "workspaces");
1008
- }
1009
- async listRepos() {
1010
- const root = this.getWorkspaceRoot();
1011
- const rootStat = await this.safeStat(root);
1012
- if (!rootStat?.isDirectory()) {
1013
- return [];
1014
- }
1015
- const entries = await readdir2(root);
1016
- const repos = [];
1017
- for (const entry of entries) {
1018
- const fullPath = join5(root, entry);
1019
- try {
1020
- const entryStat = await stat2(fullPath);
1021
- if (!entryStat.isDirectory()) {
1022
- continue;
1023
- }
1024
- const hasGit = Boolean(await this.safeStat(join5(fullPath, ".git")));
1025
- repos.push({
1026
- name: entry,
1027
- path: fullPath,
1028
- hasGit
1029
- });
1030
- } catch {
1031
- }
1032
- }
1033
- return repos.sort((a, b) => a.name.localeCompare(b.name));
1034
- }
1035
- async listRepoStatuses() {
1036
- const repos = await this.listRepos();
1037
- const statuses = [];
1038
- for (const repo of repos) {
1039
- if (!repo.hasGit) {
1040
- statuses.push({
1041
- repo,
1042
- git: {
1043
- branch: null,
1044
- prUrl: null,
1045
- gitDiff: null
1046
- }
1047
- });
1048
- continue;
1049
- }
1050
- try {
1051
- const git = await getGitStatus(repo.path);
1052
- statuses.push({ repo, git });
1053
- } catch {
1054
- }
1055
- }
1056
- return statuses;
1057
- }
1058
- async safeStat(path5) {
1059
- try {
1060
- return await stat2(path5);
1061
- } catch {
1062
- return null;
1063
- }
1064
- }
1065
- };
1066
- var repoService = new RepoService();
1067
-
1068
- // src/chat-service.ts
1069
- import { mkdir as mkdir7, readFile as readFile6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
1070
- import { homedir as homedir9 } from "os";
1116
+ // src/services/chat/chat-service.ts
1117
+ import { mkdir as mkdir7, readFile as readFile5, rm, writeFile as writeFile5 } from "fs/promises";
1118
+ import { homedir as homedir8 } from "os";
1071
1119
  import { join as join9 } from "path";
1072
1120
  import { randomUUID as randomUUID3 } from "crypto";
1073
1121
 
1122
+ // ../shared/src/sandbox.ts
1123
+ var SANDBOX_LIFECYCLE = {
1124
+ AUTO_STOP_MINUTES: 60,
1125
+ AUTO_ARCHIVE_MINUTES: 60 * 24 * 7,
1126
+ AUTO_DELETE_MINUTES: -1,
1127
+ SSH_TOKEN_EXPIRATION_MINUTES: 3 * 60
1128
+ };
1129
+
1130
+ // ../shared/src/engine/types.ts
1131
+ var DEFAULT_CHAT_TITLES = {
1132
+ claude: "Claude Code",
1133
+ codex: "Codex"
1134
+ };
1135
+ var IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1136
+
1137
+ // ../shared/src/routes/workspaces.ts
1138
+ var WORKSPACE_FILE_UPLOAD_MAX_SIZE_BYTES = 20 * 1024 * 1024;
1139
+ var WORKSPACE_FILE_CONTENT_MAX_SIZE_BYTES = 1 * 1024 * 1024;
1140
+
1074
1141
  // src/managers/claude-manager.ts
1075
1142
  import {
1076
1143
  query
1077
1144
  } from "@anthropic-ai/claude-agent-sdk";
1078
1145
  import { join as join7 } from "path";
1079
- import { mkdir as mkdir5, appendFile as appendFile4, rm } from "fs/promises";
1080
- import { homedir as homedir7 } from "os";
1146
+ import { mkdir as mkdir5, appendFile as appendFile4 } from "fs/promises";
1147
+ import { homedir as homedir6 } from "os";
1081
1148
 
1082
1149
  // src/utils/jsonl-reader.ts
1083
- import { readFile as readFile4 } from "fs/promises";
1084
- function isRecord3(value) {
1085
- return typeof value === "object" && value !== null;
1086
- }
1150
+ import { readFile as readFile3 } from "fs/promises";
1087
1151
  function isJsonlEvent(value) {
1088
- if (!isRecord3(value)) {
1152
+ if (!isRecord(value)) {
1089
1153
  return false;
1090
1154
  }
1091
- return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord3(value.payload);
1155
+ return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord(value.payload);
1092
1156
  }
1093
1157
  function parseJsonlEvents(lines) {
1094
1158
  const events = [];
@@ -1105,52 +1169,23 @@ function parseJsonlEvents(lines) {
1105
1169
  }
1106
1170
  async function readJSONL(filePath) {
1107
1171
  try {
1108
- const content = await readFile4(filePath, "utf-8");
1172
+ const content = await readFile3(filePath, "utf-8");
1109
1173
  const lines = content.split("\n").filter((line) => line.trim());
1110
1174
  return parseJsonlEvents(lines);
1111
1175
  } catch (error) {
1112
1176
  return [];
1113
1177
  }
1114
1178
  }
1115
- async function readJSONLPaginated(filePath, limit, offset = 0) {
1116
- try {
1117
- const content = await readFile4(filePath, "utf-8");
1118
- const lines = content.split("\n").filter((line) => line.trim());
1119
- const allEvents = parseJsonlEvents(lines);
1120
- const total = allEvents.length;
1121
- const startIndex = Math.max(0, total - offset - (limit ?? total));
1122
- const endIndex = Math.max(0, total - offset);
1123
- const events = allEvents.slice(startIndex, endIndex);
1124
- const hasMore = startIndex > 0;
1125
- return {
1126
- events,
1127
- total,
1128
- hasMore
1129
- };
1130
- } catch (error) {
1131
- return {
1132
- events: [],
1133
- total: 0,
1134
- hasMore: false
1135
- };
1136
- }
1137
- }
1138
1179
 
1139
1180
  // src/services/monolith-service.ts
1140
1181
  var MonolithService = class {
1141
1182
  async sendEvent(event) {
1142
- const monolithUrl = process.env.MONOLITH_URL;
1143
- const workspaceId = process.env.WORKSPACE_ID;
1144
- const engineSecret = process.env.REPLICAS_ENGINE_SECRET;
1145
- if (!monolithUrl || !workspaceId || !engineSecret) {
1146
- return;
1147
- }
1148
1183
  try {
1149
- const response = await fetch(`${monolithUrl}/v1/engine/webhook`, {
1184
+ const response = await fetch(`${ENGINE_ENV.MONOLITH_URL}/v1/engine/webhook`, {
1150
1185
  method: "POST",
1151
1186
  headers: {
1152
- Authorization: `Bearer ${engineSecret}`,
1153
- "X-Workspace-Id": workspaceId,
1187
+ Authorization: `Bearer ${ENGINE_ENV.REPLICAS_ENGINE_SECRET}`,
1188
+ "X-Workspace-Id": ENGINE_ENV.WORKSPACE_ID,
1154
1189
  "Content-Type": "application/json"
1155
1190
  },
1156
1191
  body: JSON.stringify(event)
@@ -1166,7 +1201,7 @@ var MonolithService = class {
1166
1201
  };
1167
1202
  var monolithService = new MonolithService();
1168
1203
 
1169
- // src/services/linear-event-converter.ts
1204
+ // src/utils/linear-converter.ts
1170
1205
  function linearThoughtToResponse(thought) {
1171
1206
  return {
1172
1207
  linearSessionId: thought.linearSessionId,
@@ -1299,6 +1334,46 @@ function convertClaudeEvent(event, linearSessionId) {
1299
1334
  }
1300
1335
  return null;
1301
1336
  }
1337
+ function mapTodoStatus(status) {
1338
+ if (status === "in_progress") {
1339
+ return "inProgress";
1340
+ }
1341
+ if (status === "completed") {
1342
+ return "completed";
1343
+ }
1344
+ if (status === "cancelled" || status === "canceled") {
1345
+ return "canceled";
1346
+ }
1347
+ return "pending";
1348
+ }
1349
+ function extractPlanFromClaudeEvent(event) {
1350
+ if (event.type !== "assistant") {
1351
+ return null;
1352
+ }
1353
+ const message = event;
1354
+ const contentBlocks = message.message?.content || [];
1355
+ for (const block of contentBlocks) {
1356
+ if (block.type !== "tool_use" || block.name !== "TodoWrite") {
1357
+ continue;
1358
+ }
1359
+ const input = typeof block.input === "string" ? (() => {
1360
+ try {
1361
+ return JSON.parse(block.input);
1362
+ } catch {
1363
+ return null;
1364
+ }
1365
+ })() : typeof block.input === "object" && block.input !== null ? block.input : null;
1366
+ const todos = input?.todos;
1367
+ if (!todos || todos.length === 0) {
1368
+ return null;
1369
+ }
1370
+ return todos.filter((todo) => Boolean(todo.content && todo.content.trim().length > 0)).map((todo) => ({
1371
+ content: todo.content.trim(),
1372
+ status: mapTodoStatus(todo.status)
1373
+ }));
1374
+ }
1375
+ return null;
1376
+ }
1302
1377
  function convertCodexEvent(event, linearSessionId) {
1303
1378
  if (event.type === "turn.started") {
1304
1379
  return {
@@ -1485,13 +1560,30 @@ function convertCodexEvent(event, linearSessionId) {
1485
1560
  }
1486
1561
  }
1487
1562
  }
1488
- return null;
1563
+ return null;
1564
+ }
1565
+ function extractPlanFromCodexEvent(event) {
1566
+ if (event.type !== "item.started" && event.type !== "item.completed") {
1567
+ return null;
1568
+ }
1569
+ const item = event.item;
1570
+ if (!item || item.type !== "todo_list") {
1571
+ return null;
1572
+ }
1573
+ const items = "items" in item && Array.isArray(item.items) ? item.items : [];
1574
+ if (items.length === 0) {
1575
+ return null;
1576
+ }
1577
+ const hasIncomplete = items.some((entry) => !entry.completed);
1578
+ return items.filter((entry) => Boolean(entry.text && entry.text.trim().length > 0)).map((entry) => ({
1579
+ content: entry.text.trim(),
1580
+ status: entry.completed ? "completed" : hasIncomplete ? "inProgress" : "pending"
1581
+ }));
1489
1582
  }
1490
1583
 
1491
1584
  // src/utils/image-utils.ts
1492
- var IMAGE_MEDIA_TYPES2 = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
1493
1585
  function isImageMediaType(value) {
1494
- return IMAGE_MEDIA_TYPES2.has(value);
1586
+ return IMAGE_MEDIA_TYPES.includes(value);
1495
1587
  }
1496
1588
  function inferMediaType(url, contentType) {
1497
1589
  if (contentType) {
@@ -1514,13 +1606,13 @@ async function fetchImageAsBase64(url) {
1514
1606
  const headers = {};
1515
1607
  const hostname = new URL(url).hostname;
1516
1608
  if (hostname === "uploads.linear.app") {
1517
- const token = process.env.LINEAR_TOKEN;
1609
+ const token = ENGINE_ENV.LINEAR_ACCESS_TOKEN;
1518
1610
  if (token) {
1519
1611
  headers["Authorization"] = `Bearer ${token}`;
1520
1612
  }
1521
1613
  }
1522
1614
  if (hostname === "files.slack.com") {
1523
- const token = process.env.SLACK_BOT_TOKEN;
1615
+ const token = ENGINE_ENV.SLACK_BOT_TOKEN;
1524
1616
  if (token) {
1525
1617
  headers["Authorization"] = `Bearer ${token}`;
1526
1618
  }
@@ -1559,15 +1651,10 @@ async function normalizeImages(images) {
1559
1651
  return normalized;
1560
1652
  }
1561
1653
 
1562
- // src/managers/coding-agent-manager.ts
1563
- import { join as join6 } from "path";
1564
- import { homedir as homedir6 } from "os";
1565
-
1566
- // src/services/message-queue.ts
1567
- var MessageQueue = class {
1654
+ // src/services/message-queue-service.ts
1655
+ var MessageQueueService = class {
1568
1656
  queue = [];
1569
1657
  processing = false;
1570
- currentMessageId = null;
1571
1658
  messageIdCounter = 0;
1572
1659
  processMessage;
1573
1660
  constructor(processMessage) {
@@ -1580,15 +1667,11 @@ var MessageQueue = class {
1580
1667
  * Add a message to the queue or start processing immediately if not busy
1581
1668
  * @returns Object indicating whether the message was queued or started processing
1582
1669
  */
1583
- async enqueue(message, model, customInstructions, images, permissionMode) {
1670
+ async enqueue(request) {
1584
1671
  const messageId = this.generateMessageId();
1585
1672
  const queuedMessage = {
1586
1673
  id: messageId,
1587
- message,
1588
- model,
1589
- customInstructions,
1590
- images,
1591
- permissionMode,
1674
+ ...request,
1592
1675
  queuedAt: (/* @__PURE__ */ new Date()).toISOString()
1593
1676
  };
1594
1677
  if (this.processing) {
@@ -1608,20 +1691,12 @@ var MessageQueue = class {
1608
1691
  }
1609
1692
  async startProcessing(queuedMessage) {
1610
1693
  this.processing = true;
1611
- this.currentMessageId = queuedMessage.id;
1612
1694
  try {
1613
- await this.processMessage(
1614
- queuedMessage.message,
1615
- queuedMessage.model,
1616
- queuedMessage.customInstructions,
1617
- queuedMessage.images,
1618
- queuedMessage.permissionMode
1619
- );
1695
+ await this.processMessage(queuedMessage);
1620
1696
  } catch (error) {
1621
1697
  console.error("[MessageQueue] Error processing message:", error);
1622
1698
  } finally {
1623
1699
  this.processing = false;
1624
- this.currentMessageId = null;
1625
1700
  await this.processNextInQueue();
1626
1701
  }
1627
1702
  }
@@ -1637,33 +1712,6 @@ var MessageQueue = class {
1637
1712
  isProcessing() {
1638
1713
  return this.processing;
1639
1714
  }
1640
- /**
1641
- * Get the current queue length
1642
- */
1643
- getQueueLength() {
1644
- return this.queue.length;
1645
- }
1646
- /**
1647
- * Get full queue status
1648
- */
1649
- getStatus() {
1650
- return {
1651
- isProcessing: this.processing,
1652
- queueLength: this.queue.length,
1653
- currentMessageId: this.currentMessageId,
1654
- queuedMessages: this.queue.map((msg, index) => ({
1655
- id: msg.id,
1656
- queuedAt: msg.queuedAt,
1657
- position: index + 1
1658
- }))
1659
- };
1660
- }
1661
- /**
1662
- * Clear the queue (does not stop current processing)
1663
- */
1664
- clearQueue() {
1665
- this.queue = [];
1666
- }
1667
1715
  drainQueue(options) {
1668
1716
  const maxItems = options?.maxItems ?? this.queue.length;
1669
1717
  const maxChars = options?.maxChars ?? Number.POSITIVE_INFINITY;
@@ -1685,7 +1733,6 @@ var MessageQueue = class {
1685
1733
  reset() {
1686
1734
  this.queue = [];
1687
1735
  this.processing = false;
1688
- this.currentMessageId = null;
1689
1736
  }
1690
1737
  };
1691
1738
 
@@ -1696,18 +1743,19 @@ var CodingAgentManager = class {
1696
1743
  workingDirectory;
1697
1744
  messageQueue;
1698
1745
  initialized;
1699
- baseSystemPrompt;
1700
- constructor(workingDirectory) {
1701
- if (workingDirectory) {
1702
- this.workingDirectory = workingDirectory;
1703
- return;
1704
- }
1705
- const repoName = process.env.REPLICAS_REPO_NAME;
1706
- const workspaceHome = process.env.WORKSPACE_HOME || process.env.HOME || homedir6();
1707
- this.workingDirectory = repoName ? join6(workspaceHome, "workspaces", repoName) : workspaceHome;
1746
+ initialSessionId;
1747
+ onSaveSessionId;
1748
+ onEvent;
1749
+ onTurnComplete;
1750
+ constructor(options) {
1751
+ this.workingDirectory = options.workingDirectory ?? ENGINE_ENV.WORKSPACE_ROOT;
1752
+ this.initialSessionId = options.initialSessionId;
1753
+ this.onSaveSessionId = options.onSaveSessionId;
1754
+ this.onEvent = options.onEvent;
1755
+ this.onTurnComplete = options.onTurnComplete;
1708
1756
  }
1709
1757
  initializeManager(processMessage) {
1710
- this.messageQueue = new MessageQueue(processMessage);
1758
+ this.messageQueue = new MessageQueueService(processMessage);
1711
1759
  this.initialized = this.initialize();
1712
1760
  }
1713
1761
  async interrupt() {
@@ -1724,28 +1772,20 @@ var CodingAgentManager = class {
1724
1772
  isProcessing() {
1725
1773
  return this.messageQueue.isProcessing();
1726
1774
  }
1727
- async enqueueMessage(message, model, customInstructions, images, permissionMode) {
1775
+ async enqueueMessage(request) {
1728
1776
  await this.initialized;
1729
- return this.messageQueue.enqueue(message, model, customInstructions, images, permissionMode);
1730
- }
1731
- getQueueStatus() {
1732
- return this.messageQueue.getStatus();
1733
- }
1734
- setBaseSystemPrompt(prompt) {
1735
- this.baseSystemPrompt = prompt;
1736
- }
1737
- getBaseSystemPrompt() {
1738
- return this.baseSystemPrompt;
1777
+ return this.messageQueue.enqueue(request);
1739
1778
  }
1740
1779
  buildCombinedInstructions(customInstructions) {
1741
1780
  const startHooksInstruction = this.getStartHooksInstruction();
1781
+ const repositorySystemPromptInstruction = this.getRepositorySystemPromptInstruction();
1742
1782
  const parts = [];
1743
- if (this.baseSystemPrompt) {
1744
- parts.push(this.baseSystemPrompt);
1745
- }
1746
1783
  if (startHooksInstruction) {
1747
1784
  parts.push(startHooksInstruction);
1748
1785
  }
1786
+ if (repositorySystemPromptInstruction) {
1787
+ parts.push(repositorySystemPromptInstruction);
1788
+ }
1749
1789
  if (customInstructions) {
1750
1790
  parts.push(customInstructions);
1751
1791
  }
@@ -1762,7 +1802,7 @@ var CodingAgentManager = class {
1762
1802
  };
1763
1803
  }
1764
1804
  emitInterruptedQueueEvent(queue) {
1765
- const linearSessionId = process.env.LINEAR_SESSION_ID;
1805
+ const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
1766
1806
  if (!linearSessionId || queue.length === 0) {
1767
1807
  return;
1768
1808
  }
@@ -1777,12 +1817,6 @@ var CodingAgentManager = class {
1777
1817
  monolithService.sendEvent({ type: "agent_update", payload: thoughtEvent }).catch(() => {
1778
1818
  });
1779
1819
  }
1780
- async emitTurnCompleteEvent() {
1781
- const linearSessionId = process.env.LINEAR_SESSION_ID;
1782
- const repoStatuses = await repoService.listRepoStatuses();
1783
- const payload = linearSessionId ? { linearSessionId, repoStatuses } : { repoStatuses };
1784
- await monolithService.sendEvent({ type: "agent_turn_complete", payload });
1785
- }
1786
1820
  getStartHooksInstruction() {
1787
1821
  if (!replicasConfigService.areHooksRunning()) {
1788
1822
  return void 0;
@@ -1793,6 +1827,13 @@ var CodingAgentManager = class {
1793
1827
  }
1794
1828
  return START_HOOKS_RUNNING_PROMPT;
1795
1829
  }
1830
+ getRepositorySystemPromptInstruction() {
1831
+ const repoSystemPrompts = replicasConfigService.getSystemPrompts();
1832
+ if (repoSystemPrompts.length === 0) {
1833
+ return void 0;
1834
+ }
1835
+ return repoSystemPrompts.join("\n");
1836
+ }
1796
1837
  };
1797
1838
 
1798
1839
  // src/managers/claude-manager.ts
@@ -1843,21 +1884,9 @@ var ClaudeManager = class extends CodingAgentManager {
1843
1884
  activeQuery = null;
1844
1885
  activePromptStream = null;
1845
1886
  pendingInterrupt = false;
1846
- loadSessionId;
1847
- saveSessionId;
1848
- onEvent;
1849
1887
  constructor(options) {
1850
- const normalized = typeof options === "string" ? { workingDirectory: options } : options ?? {};
1851
- super(normalized.workingDirectory);
1852
- this.historyFile = normalized.historyFilePath ?? join7(homedir7(), ".replicas", "claude", "history.jsonl");
1853
- this.loadSessionId = normalized.loadSessionId ?? (async () => {
1854
- const persistedState = await loadEngineState();
1855
- return persistedState.claudeSessionId;
1856
- });
1857
- this.saveSessionId = normalized.saveSessionId ?? (async (sessionId) => {
1858
- await saveEngineState({ claudeSessionId: sessionId });
1859
- });
1860
- this.onEvent = normalized.onEvent;
1888
+ super(options);
1889
+ this.historyFile = options.historyFilePath ?? join7(homedir6(), ".replicas", "claude", "history.jsonl");
1861
1890
  this.initializeManager(this.processMessageInternal.bind(this));
1862
1891
  }
1863
1892
  async interruptActiveTurn() {
@@ -1867,18 +1896,18 @@ var ClaudeManager = class extends CodingAgentManager {
1867
1896
  await this.activeQuery.interrupt();
1868
1897
  }
1869
1898
  }
1870
- /**
1871
- * Legacy sendMessage method - now uses the queue internally
1872
- * @deprecated Use enqueueMessage for better control over queue status
1873
- */
1874
- async sendMessage(message, model, customInstructions, images, permissionMode) {
1875
- await this.enqueueMessage(message, model, customInstructions, images, permissionMode);
1876
- }
1877
1899
  /**
1878
1900
  * Internal method that actually processes the message
1879
1901
  */
1880
- async processMessageInternal(message, model, customInstructions, images, permissionMode) {
1881
- const linearSessionId = process.env.LINEAR_SESSION_ID;
1902
+ async processMessageInternal(request) {
1903
+ const {
1904
+ message,
1905
+ model,
1906
+ customInstructions,
1907
+ images,
1908
+ permissionMode
1909
+ } = request;
1910
+ const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
1882
1911
  if (!message || !message.trim()) {
1883
1912
  throw new Error("Message cannot be empty");
1884
1913
  }
@@ -1946,6 +1975,14 @@ var ClaudeManager = class extends CodingAgentManager {
1946
1975
  for await (const msg of response) {
1947
1976
  await this.handleMessage(msg);
1948
1977
  if (linearSessionId) {
1978
+ const plan = extractPlanFromClaudeEvent(msg);
1979
+ if (plan) {
1980
+ monolithService.sendEvent({
1981
+ type: "agent_plan_update",
1982
+ payload: { linearSessionId, plan }
1983
+ }).catch(() => {
1984
+ });
1985
+ }
1949
1986
  const linearEvent = convertClaudeEvent(msg, linearSessionId);
1950
1987
  if (linearEvent) {
1951
1988
  if (latestThoughtEvent) {
@@ -1975,7 +2012,7 @@ var ClaudeManager = class extends CodingAgentManager {
1975
2012
  this.activePromptStream?.close();
1976
2013
  this.activePromptStream = null;
1977
2014
  this.pendingInterrupt = false;
1978
- await this.emitTurnCompleteEvent();
2015
+ await this.onTurnComplete();
1979
2016
  }
1980
2017
  }
1981
2018
  async getHistory() {
@@ -1986,61 +2023,18 @@ var ClaudeManager = class extends CodingAgentManager {
1986
2023
  events
1987
2024
  };
1988
2025
  }
1989
- /**
1990
- * Get paginated history from the end (bottom-up pagination).
1991
- * @param limit - Maximum number of events to return
1992
- * @param offset - Number of events to skip from the end
1993
- * @returns Paginated history result
1994
- */
1995
- async getHistoryPaginated(limit, offset = 0) {
1996
- await this.initialized;
1997
- const result = await readJSONLPaginated(this.historyFile, limit, offset);
1998
- return {
1999
- thread_id: this.sessionId,
2000
- events: result.events,
2001
- total: result.total,
2002
- hasMore: result.hasMore
2003
- };
2004
- }
2005
- async getStatus() {
2006
- await this.initialized;
2007
- const status = {
2008
- has_active_thread: this.messageQueue.isProcessing(),
2009
- thread_id: this.sessionId,
2010
- working_directory: this.workingDirectory
2011
- };
2012
- return status;
2013
- }
2014
- async getUpdates(since) {
2015
- await this.initialized;
2016
- const allEvents = await readJSONL(this.historyFile);
2017
- const events = allEvents.filter((event) => event.timestamp > since);
2018
- const isComplete = !this.messageQueue.isProcessing();
2019
- return { events, isComplete };
2020
- }
2021
- async reset() {
2022
- await this.initialized;
2023
- this.sessionId = null;
2024
- this.messageQueue.reset();
2025
- await this.saveSessionId(null);
2026
- try {
2027
- await rm(this.historyFile, { force: true });
2028
- } catch {
2029
- }
2030
- }
2031
2026
  async initialize() {
2032
- const historyDir = join7(homedir7(), ".replicas", "claude");
2027
+ const historyDir = join7(homedir6(), ".replicas", "claude");
2033
2028
  await mkdir5(historyDir, { recursive: true });
2034
- const persistedSessionId = await this.loadSessionId();
2035
- if (persistedSessionId) {
2036
- this.sessionId = persistedSessionId;
2029
+ if (this.initialSessionId) {
2030
+ this.sessionId = this.initialSessionId;
2037
2031
  console.log(`[ClaudeManager] Restored session ID from persisted state: ${this.sessionId}`);
2038
2032
  }
2039
2033
  }
2040
2034
  async handleMessage(message) {
2041
2035
  if ("session_id" in message && message.session_id && !this.sessionId) {
2042
2036
  this.sessionId = message.session_id;
2043
- await this.saveSessionId(this.sessionId);
2037
+ await this.onSaveSessionId(this.sessionId);
2044
2038
  console.log(`[ClaudeManager] Captured and persisted session ID: ${this.sessionId}`);
2045
2039
  }
2046
2040
  await this.recordEvent(message);
@@ -2053,68 +2047,28 @@ var ClaudeManager = class extends CodingAgentManager {
2053
2047
  payload: event
2054
2048
  };
2055
2049
  await appendFile4(this.historyFile, JSON.stringify(jsonEvent) + "\n", "utf-8");
2056
- if (this.onEvent) {
2057
- this.onEvent(jsonEvent);
2058
- }
2059
- }
2060
- };
2061
-
2062
- // src/providers/claude-adapter.ts
2063
- var ClaudeAdapter = class {
2064
- manager;
2065
- constructor(options) {
2066
- this.manager = new ClaudeManager({
2067
- workingDirectory: options.workingDirectory,
2068
- historyFilePath: options.historyFilePath,
2069
- loadSessionId: options.loadSessionId,
2070
- saveSessionId: options.saveSessionId,
2071
- onEvent: options.onEvent
2072
- });
2073
- }
2074
- async enqueueMessage(input) {
2075
- return this.manager.enqueueMessage(
2076
- input.message,
2077
- input.model,
2078
- input.customInstructions,
2079
- input.images,
2080
- input.permissionMode
2081
- );
2082
- }
2083
- async interrupt() {
2084
- return this.manager.interrupt();
2085
- }
2086
- async reset() {
2087
- await this.manager.reset();
2088
- }
2089
- async getHistory() {
2090
- return this.manager.getHistory();
2091
- }
2092
- isProcessing() {
2093
- return this.manager.isProcessing();
2050
+ this.onEvent(jsonEvent);
2094
2051
  }
2095
2052
  };
2096
2053
 
2097
2054
  // src/managers/codex-manager.ts
2098
2055
  import { Codex } from "@openai/codex-sdk";
2099
2056
  import { randomUUID as randomUUID2 } from "crypto";
2100
- import { readdir as readdir3, stat as stat3, writeFile as writeFile4, mkdir as mkdir6, readFile as readFile5 } from "fs/promises";
2057
+ import { readdir as readdir2, stat as stat2, writeFile as writeFile4, mkdir as mkdir6, readFile as readFile4 } from "fs/promises";
2101
2058
  import { existsSync as existsSync4 } from "fs";
2102
2059
  import { join as join8 } from "path";
2103
- import { homedir as homedir8 } from "os";
2060
+ import { homedir as homedir7 } from "os";
2104
2061
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2105
2062
  var DEFAULT_MODEL = "gpt-5.3-codex";
2106
- var CODEX_CONFIG_PATH = join8(homedir8(), ".codex", "config.toml");
2107
- function isRecord4(value) {
2108
- return typeof value === "object" && value !== null;
2109
- }
2063
+ var CODEX_CONFIG_PATH = join8(homedir7(), ".codex", "config.toml");
2110
2064
  function isLinearThoughtEvent2(event) {
2111
2065
  return event.content.type === "thought";
2112
2066
  }
2113
2067
  function isJsonlEvent2(value) {
2114
- if (!isRecord4(value)) {
2068
+ if (!isRecord(value)) {
2115
2069
  return false;
2116
2070
  }
2117
- return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord4(value.payload);
2071
+ return typeof value.timestamp === "string" && typeof value.type === "string" && isRecord(value.payload);
2118
2072
  }
2119
2073
  function sleep(ms) {
2120
2074
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -2125,30 +2079,15 @@ var CodexManager = class extends CodingAgentManager {
2125
2079
  currentThread = null;
2126
2080
  tempImageDir;
2127
2081
  activeAbortController = null;
2128
- loadThreadId;
2129
- saveThreadId;
2130
- onEvent;
2131
- onTurnComplete;
2132
2082
  constructor(options) {
2133
- const normalized = typeof options === "string" ? { workingDirectory: options } : options ?? {};
2134
- super(normalized.workingDirectory);
2083
+ super(options);
2135
2084
  this.codex = new Codex();
2136
- this.loadThreadId = normalized.loadThreadId ?? (async () => {
2137
- const persistedState = await loadEngineState();
2138
- return persistedState.codexThreadId;
2139
- });
2140
- this.saveThreadId = normalized.saveThreadId ?? (async (threadId) => {
2141
- await saveEngineState({ codexThreadId: threadId });
2142
- });
2143
- this.onEvent = normalized.onEvent;
2144
- this.onTurnComplete = normalized.onTurnComplete;
2145
- this.tempImageDir = join8(homedir8(), ".replicas", "codex", "temp-images");
2085
+ this.tempImageDir = join8(homedir7(), ".replicas", "codex", "temp-images");
2146
2086
  this.initializeManager(this.processMessageInternal.bind(this));
2147
2087
  }
2148
2088
  async initialize() {
2149
- const persistedThreadId = await this.loadThreadId();
2150
- if (persistedThreadId) {
2151
- this.currentThreadId = persistedThreadId;
2089
+ if (this.initialSessionId) {
2090
+ this.currentThreadId = this.initialSessionId;
2152
2091
  console.log(`[CodexManager] Restored thread ID from persisted state: ${this.currentThreadId}`);
2153
2092
  }
2154
2093
  }
@@ -2163,14 +2102,14 @@ var CodexManager = class extends CodingAgentManager {
2163
2102
  */
2164
2103
  async updateCodexConfig(developerInstructions) {
2165
2104
  try {
2166
- const codexDir = join8(homedir8(), ".codex");
2105
+ const codexDir = join8(homedir7(), ".codex");
2167
2106
  await mkdir6(codexDir, { recursive: true });
2168
2107
  let config = {};
2169
2108
  if (existsSync4(CODEX_CONFIG_PATH)) {
2170
2109
  try {
2171
- const existingContent = await readFile5(CODEX_CONFIG_PATH, "utf-8");
2110
+ const existingContent = await readFile4(CODEX_CONFIG_PATH, "utf-8");
2172
2111
  const parsed = parseToml(existingContent);
2173
- if (isRecord4(parsed)) {
2112
+ if (isRecord(parsed)) {
2174
2113
  config = parsed;
2175
2114
  }
2176
2115
  } catch (parseError) {
@@ -2209,8 +2148,15 @@ var CodexManager = class extends CodingAgentManager {
2209
2148
  /**
2210
2149
  * Internal method that actually processes the message
2211
2150
  */
2212
- async processMessageInternal(message, model, customInstructions, images, permissionMode) {
2213
- const linearSessionId = process.env.LINEAR_SESSION_ID;
2151
+ async processMessageInternal(request) {
2152
+ const {
2153
+ message,
2154
+ model,
2155
+ customInstructions,
2156
+ images,
2157
+ permissionMode
2158
+ } = request;
2159
+ const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
2214
2160
  let tempImagePaths = [];
2215
2161
  let stopTail = null;
2216
2162
  let abortController = null;
@@ -2240,22 +2186,22 @@ var CodexManager = class extends CodingAgentManager {
2240
2186
  for await (const event of events2) {
2241
2187
  if (event.type === "thread.started") {
2242
2188
  this.currentThreadId = event.thread_id;
2243
- await this.saveThreadId(this.currentThreadId);
2189
+ await this.onSaveSessionId(this.currentThreadId);
2244
2190
  console.log(`[CodexManager] Captured and persisted thread ID: ${this.currentThreadId}`);
2245
2191
  }
2246
2192
  }
2247
2193
  if (!this.currentThreadId && this.currentThread.id) {
2248
2194
  this.currentThreadId = this.currentThread.id;
2249
- await this.saveThreadId(this.currentThreadId);
2195
+ await this.onSaveSessionId(this.currentThreadId);
2250
2196
  console.log(`[CodexManager] Captured and persisted thread ID from thread.id: ${this.currentThreadId}`);
2251
2197
  }
2252
2198
  }
2253
- stopTail = this.onEvent && this.currentThreadId ? await this.startSessionTail(this.currentThreadId, this.onEvent) : null;
2199
+ stopTail = this.currentThreadId ? await this.startSessionTail(this.currentThreadId) : null;
2254
2200
  let input;
2255
2201
  if (tempImagePaths.length > 0) {
2256
2202
  const inputItems = [
2257
2203
  { type: "text", text: message },
2258
- ...tempImagePaths.map((path5) => ({ type: "local_image", path: path5 }))
2204
+ ...tempImagePaths.map((path4) => ({ type: "local_image", path: path4 }))
2259
2205
  ];
2260
2206
  input = inputItems;
2261
2207
  } else {
@@ -2265,6 +2211,14 @@ var CodexManager = class extends CodingAgentManager {
2265
2211
  let latestThoughtEvent = null;
2266
2212
  for await (const event of events) {
2267
2213
  if (linearSessionId) {
2214
+ const plan = extractPlanFromCodexEvent(event);
2215
+ if (plan) {
2216
+ monolithService.sendEvent({
2217
+ type: "agent_plan_update",
2218
+ payload: { linearSessionId, plan }
2219
+ }).catch(() => {
2220
+ });
2221
+ }
2268
2222
  const linearEvent = convertCodexEvent(event, linearSessionId);
2269
2223
  if (linearEvent) {
2270
2224
  if (latestThoughtEvent) {
@@ -2290,11 +2244,8 @@ var CodexManager = class extends CodingAgentManager {
2290
2244
  if (stopTail) {
2291
2245
  await stopTail();
2292
2246
  }
2293
- if (!abortController?.signal.aborted) {
2294
- this.onTurnComplete?.();
2295
- }
2247
+ await this.onTurnComplete();
2296
2248
  this.activeAbortController = null;
2297
- await this.emitTurnCompleteEvent();
2298
2249
  }
2299
2250
  }
2300
2251
  async getHistory() {
@@ -2317,84 +2268,9 @@ var CodexManager = class extends CodingAgentManager {
2317
2268
  events
2318
2269
  };
2319
2270
  }
2320
- /**
2321
- * Get paginated history from the end (bottom-up pagination).
2322
- * @param limit - Maximum number of events to return
2323
- * @param offset - Number of events to skip from the end
2324
- * @returns Paginated history result
2325
- */
2326
- async getHistoryPaginated(limit, offset = 0) {
2327
- if (!this.currentThreadId) {
2328
- return {
2329
- thread_id: null,
2330
- events: [],
2331
- total: 0,
2332
- hasMore: false
2333
- };
2334
- }
2335
- const sessionFile = await this.findSessionFile(this.currentThreadId);
2336
- if (!sessionFile) {
2337
- return {
2338
- thread_id: this.currentThreadId,
2339
- events: [],
2340
- total: 0,
2341
- hasMore: false
2342
- };
2343
- }
2344
- const result = await readJSONLPaginated(sessionFile, limit, offset);
2345
- return {
2346
- thread_id: this.currentThreadId,
2347
- events: result.events,
2348
- total: result.total,
2349
- hasMore: result.hasMore
2350
- };
2351
- }
2352
- async getStatus() {
2353
- let sessionFile = null;
2354
- if (this.currentThreadId) {
2355
- sessionFile = await this.findSessionFile(this.currentThreadId);
2356
- }
2357
- return {
2358
- has_active_thread: this.currentThreadId !== null,
2359
- thread_id: this.currentThreadId,
2360
- session_file: sessionFile,
2361
- working_directory: this.workingDirectory
2362
- };
2363
- }
2364
- async reset() {
2365
- this.currentThread = null;
2366
- this.currentThreadId = null;
2367
- this.messageQueue.reset();
2368
- await this.saveThreadId(null);
2369
- }
2370
- getThreadId() {
2371
- return this.currentThreadId;
2372
- }
2373
- async getUpdates(since) {
2374
- if (!this.currentThreadId) {
2375
- return {
2376
- events: [],
2377
- isComplete: true
2378
- };
2379
- }
2380
- const sessionFile = await this.findSessionFile(this.currentThreadId);
2381
- if (!sessionFile) {
2382
- return {
2383
- events: [],
2384
- isComplete: true
2385
- };
2386
- }
2387
- const allEvents = await readJSONL(sessionFile);
2388
- const events = allEvents.filter((event) => event.timestamp > since);
2389
- const isComplete = allEvents.some((event) => event.type === "turn.completed");
2390
- return {
2391
- events,
2392
- isComplete
2393
- };
2394
- }
2395
2271
  // Helper methods for finding session files
2396
2272
  async findSessionFile(threadId) {
2397
- const sessionsDir = join8(homedir8(), ".codex", "sessions");
2273
+ const sessionsDir = join8(homedir7(), ".codex", "sessions");
2398
2274
  try {
2399
2275
  const now = /* @__PURE__ */ new Date();
2400
2276
  const year = now.getFullYear();
@@ -2420,11 +2296,11 @@ var CodexManager = class extends CodingAgentManager {
2420
2296
  }
2421
2297
  async findFileInDirectory(directory, threadId) {
2422
2298
  try {
2423
- const files = await readdir3(directory);
2299
+ const files = await readdir2(directory);
2424
2300
  for (const file of files) {
2425
2301
  if (file.endsWith(".jsonl") && file.includes(threadId)) {
2426
2302
  const fullPath = join8(directory, file);
2427
- const stats = await stat3(fullPath);
2303
+ const stats = await stat2(fullPath);
2428
2304
  if (stats.isFile()) {
2429
2305
  return fullPath;
2430
2306
  }
@@ -2446,7 +2322,7 @@ var CodexManager = class extends CodingAgentManager {
2446
2322
  }
2447
2323
  return null;
2448
2324
  }
2449
- async startSessionTail(threadId, onEvent) {
2325
+ async startSessionTail(threadId) {
2450
2326
  const sessionFile = await this.waitForSessionFile(threadId);
2451
2327
  if (!sessionFile) {
2452
2328
  return async () => {
@@ -2456,7 +2332,7 @@ var CodexManager = class extends CodingAgentManager {
2456
2332
  const seenLines = /* @__PURE__ */ new Set();
2457
2333
  const seedSeenLines = async () => {
2458
2334
  try {
2459
- const content = await readFile5(sessionFile, "utf-8");
2335
+ const content = await readFile4(sessionFile, "utf-8");
2460
2336
  const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
2461
2337
  for (const line of lines) {
2462
2338
  seenLines.add(line);
@@ -2468,7 +2344,7 @@ var CodexManager = class extends CodingAgentManager {
2468
2344
  const pump = async () => {
2469
2345
  let emitted = 0;
2470
2346
  try {
2471
- const content = await readFile5(sessionFile, "utf-8");
2347
+ const content = await readFile4(sessionFile, "utf-8");
2472
2348
  const lines = content.split("\n");
2473
2349
  const completeLines = content.endsWith("\n") ? lines : lines.slice(0, -1);
2474
2350
  for (const line of completeLines) {
@@ -2480,7 +2356,7 @@ var CodexManager = class extends CodingAgentManager {
2480
2356
  try {
2481
2357
  const parsed = JSON.parse(trimmed);
2482
2358
  if (isJsonlEvent2(parsed)) {
2483
- onEvent(parsed);
2359
+ this.onEvent(parsed);
2484
2360
  emitted += 1;
2485
2361
  }
2486
2362
  } catch {
@@ -2512,45 +2388,7 @@ var CodexManager = class extends CodingAgentManager {
2512
2388
  }
2513
2389
  };
2514
2390
 
2515
- // src/providers/codex-adapter.ts
2516
- var CodexAdapter = class {
2517
- manager;
2518
- constructor(options) {
2519
- this.manager = new CodexManager({
2520
- workingDirectory: options.workingDirectory,
2521
- loadThreadId: options.loadThreadId,
2522
- saveThreadId: options.saveThreadId,
2523
- onEvent: options.onEvent,
2524
- onTurnComplete: options.onTurnComplete
2525
- });
2526
- }
2527
- async enqueueMessage(input) {
2528
- return this.manager.enqueueMessage(
2529
- input.message,
2530
- input.model,
2531
- input.customInstructions,
2532
- input.images,
2533
- input.permissionMode
2534
- );
2535
- }
2536
- async interrupt() {
2537
- return this.manager.interrupt();
2538
- }
2539
- async reset() {
2540
- await this.manager.reset();
2541
- }
2542
- async getHistory() {
2543
- return this.manager.getHistory();
2544
- }
2545
- isProcessing() {
2546
- return this.manager.isProcessing();
2547
- }
2548
- };
2549
-
2550
- // src/chat-service.ts
2551
- var ENGINE_DIR2 = join9(homedir9(), ".replicas", "engine");
2552
- var CHATS_FILE = join9(ENGINE_DIR2, "chats.json");
2553
- var CLAUDE_HISTORY_DIR = join9(ENGINE_DIR2, "claude-histories");
2391
+ // src/services/chat/errors.ts
2554
2392
  var ChatNotFoundError = class extends Error {
2555
2393
  constructor(chatId) {
2556
2394
  super(`Chat not found: ${chatId}`);
@@ -2575,17 +2413,17 @@ var DuplicateDefaultChatError = class extends Error {
2575
2413
  this.name = "DuplicateDefaultChatError";
2576
2414
  }
2577
2415
  };
2578
- function isRecord5(value) {
2579
- return typeof value === "object" && value !== null;
2580
- }
2416
+
2417
+ // src/services/chat/chat-service.ts
2418
+ var ENGINE_DIR2 = join9(homedir8(), ".replicas", "engine");
2419
+ var CHATS_FILE = join9(ENGINE_DIR2, "chats.json");
2420
+ var CLAUDE_HISTORY_DIR = join9(ENGINE_DIR2, "claude-histories");
2581
2421
  function isPersistedChat(value) {
2582
- if (!isRecord5(value)) {
2422
+ if (!isRecord(value)) {
2583
2423
  return false;
2584
2424
  }
2585
2425
  const candidate = value;
2586
- const repoScope = candidate.repoScope;
2587
- const hasValidRepoScope = repoScope === "all" || Array.isArray(repoScope) && repoScope.every((entry) => typeof entry === "string");
2588
- return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && hasValidRepoScope && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string");
2426
+ return typeof candidate.id === "string" && (candidate.provider === "claude" || candidate.provider === "codex") && typeof candidate.title === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" && (candidate.providerSessionId === null || typeof candidate.providerSessionId === "string");
2589
2427
  }
2590
2428
  var ChatService = class {
2591
2429
  constructor(workingDirectory) {
@@ -2621,7 +2459,6 @@ var ChatService = class {
2621
2459
  title,
2622
2460
  createdAt: now,
2623
2461
  updatedAt: now,
2624
- repoScope: request.repoScope ?? "all",
2625
2462
  providerSessionId: null
2626
2463
  };
2627
2464
  const runtime = this.createRuntimeChat(persisted);
@@ -2635,13 +2472,7 @@ var ChatService = class {
2635
2472
  }
2636
2473
  async sendMessage(chatId, request) {
2637
2474
  const chat = this.requireChat(chatId);
2638
- const result = await chat.provider.enqueueMessage({
2639
- message: request.message,
2640
- model: request.model,
2641
- customInstructions: request.customInstructions,
2642
- images: request.images,
2643
- permissionMode: request.permissionMode
2644
- });
2475
+ const result = await chat.provider.enqueueMessage(request);
2645
2476
  chat.pendingMessageIds.push(result.messageId);
2646
2477
  this.touch(chat);
2647
2478
  await this.publish({
@@ -2654,7 +2485,6 @@ var ChatService = class {
2654
2485
  }
2655
2486
  });
2656
2487
  return {
2657
- success: true,
2658
2488
  messageId: result.messageId,
2659
2489
  queued: result.queued,
2660
2490
  position: result.position
@@ -2676,12 +2506,6 @@ var ChatService = class {
2676
2506
  });
2677
2507
  return result;
2678
2508
  }
2679
- async reset(chatId) {
2680
- const chat = this.requireChat(chatId);
2681
- await chat.provider.reset();
2682
- chat.persisted.providerSessionId = null;
2683
- this.touch(chat);
2684
- }
2685
2509
  async deleteChat(chatId) {
2686
2510
  const chat = this.requireChat(chatId);
2687
2511
  if (chat.persisted.title === DEFAULT_CHAT_TITLES[chat.persisted.provider]) {
@@ -2694,7 +2518,7 @@ var ChatService = class {
2694
2518
  await this.persistAllChats();
2695
2519
  if (chat.persisted.provider === "claude") {
2696
2520
  const historyFilePath = join9(CLAUDE_HISTORY_DIR, `${chatId}.jsonl`);
2697
- await rm2(historyFilePath, { force: true });
2521
+ await rm(historyFilePath, { force: true });
2698
2522
  }
2699
2523
  await this.publish({
2700
2524
  type: "chat.deleted",
@@ -2728,50 +2552,25 @@ var ChatService = class {
2728
2552
  payload: { chat: this.toSummary(this.requireChat(persisted.id)) }
2729
2553
  });
2730
2554
  };
2731
- const provider = persisted.provider === "claude" ? new ClaudeAdapter({
2555
+ const onProviderTurnComplete = async () => {
2556
+ await this.handleTurnFinished(persisted.id);
2557
+ };
2558
+ const onProviderEvent = (event) => {
2559
+ this.handleTurnEvent(persisted.id, event);
2560
+ };
2561
+ const provider = persisted.provider === "claude" ? new ClaudeManager({
2732
2562
  workingDirectory: this.workingDirectory,
2733
2563
  historyFilePath: join9(CLAUDE_HISTORY_DIR, `${persisted.id}.jsonl`),
2734
- loadSessionId: async () => persisted.providerSessionId,
2735
- saveSessionId: saveSession,
2736
- onEvent: (event) => {
2737
- const runtimeChat = this.chats.get(persisted.id);
2738
- if (runtimeChat) {
2739
- this.handleProviderEvent(runtimeChat, event);
2740
- }
2741
- this.publish({
2742
- type: "chat.turn.delta",
2743
- payload: {
2744
- chatId: persisted.id,
2745
- event
2746
- }
2747
- }).catch(() => {
2748
- });
2749
- }
2750
- }) : new CodexAdapter({
2564
+ initialSessionId: persisted.providerSessionId,
2565
+ onSaveSessionId: saveSession,
2566
+ onTurnComplete: onProviderTurnComplete,
2567
+ onEvent: onProviderEvent
2568
+ }) : new CodexManager({
2751
2569
  workingDirectory: this.workingDirectory,
2752
- loadThreadId: async () => persisted.providerSessionId,
2753
- saveThreadId: saveSession,
2754
- onTurnComplete: () => {
2755
- const runtimeChat = this.chats.get(persisted.id);
2756
- if (!runtimeChat) {
2757
- return;
2758
- }
2759
- this.completeTurn(runtimeChat);
2760
- },
2761
- onEvent: (event) => {
2762
- const runtimeChat = this.chats.get(persisted.id);
2763
- if (runtimeChat) {
2764
- this.handleProviderEvent(runtimeChat, event);
2765
- }
2766
- this.publish({
2767
- type: "chat.turn.delta",
2768
- payload: {
2769
- chatId: persisted.id,
2770
- event
2771
- }
2772
- }).catch(() => {
2773
- });
2774
- }
2570
+ initialSessionId: persisted.providerSessionId,
2571
+ onSaveSessionId: saveSession,
2572
+ onTurnComplete: onProviderTurnComplete,
2573
+ onEvent: onProviderEvent
2775
2574
  });
2776
2575
  return {
2777
2576
  persisted,
@@ -2792,7 +2591,6 @@ var ChatService = class {
2792
2591
  title: chat.persisted.title,
2793
2592
  createdAt: chat.persisted.createdAt,
2794
2593
  updatedAt: chat.persisted.updatedAt,
2795
- repoScope: chat.persisted.repoScope,
2796
2594
  processing: chat.provider.isProcessing()
2797
2595
  };
2798
2596
  }
@@ -2803,7 +2601,11 @@ var ChatService = class {
2803
2601
  }
2804
2602
  return chat;
2805
2603
  }
2806
- handleProviderEvent(chat, event) {
2604
+ handleTurnEvent(chatId, event) {
2605
+ const chat = this.getRuntimeChat(chatId);
2606
+ if (!chat) {
2607
+ return;
2608
+ }
2807
2609
  if (!chat.hasActiveTurn && chat.pendingMessageIds.length > 0) {
2808
2610
  const messageId = chat.pendingMessageIds.shift();
2809
2611
  if (!messageId) {
@@ -2819,34 +2621,42 @@ var ChatService = class {
2819
2621
  }).catch(() => {
2820
2622
  });
2821
2623
  }
2822
- if (this.isTurnCompleteEvent(chat.persisted.provider, event.type)) {
2823
- this.completeTurn(chat);
2824
- }
2825
2624
  this.touch(chat);
2625
+ this.publish({
2626
+ type: "chat.turn.delta",
2627
+ payload: {
2628
+ chatId,
2629
+ event
2630
+ }
2631
+ }).catch(() => {
2632
+ });
2826
2633
  }
2827
- completeTurn(chat) {
2634
+ async handleTurnFinished(chatId) {
2635
+ const chat = this.getRuntimeChat(chatId);
2636
+ if (!chat) {
2637
+ return;
2638
+ }
2828
2639
  if (!chat.hasActiveTurn) {
2640
+ await this.publishAgentTurnCompleteWebhook();
2829
2641
  return;
2830
2642
  }
2831
2643
  chat.hasActiveTurn = false;
2832
2644
  this.publish({
2833
2645
  type: "chat.turn.completed",
2834
2646
  payload: {
2835
- chatId: chat.persisted.id,
2647
+ chatId,
2836
2648
  isComplete: true
2837
2649
  }
2838
2650
  }).catch(() => {
2839
2651
  });
2652
+ await this.publishAgentTurnCompleteWebhook();
2840
2653
  }
2841
- isTurnCompleteEvent(provider, eventType) {
2842
- if (provider === "claude") {
2843
- return eventType === "claude-result";
2844
- }
2845
- return eventType === "turn.completed";
2654
+ getRuntimeChat(chatId) {
2655
+ return this.chats.get(chatId) ?? null;
2846
2656
  }
2847
2657
  async loadChats() {
2848
2658
  try {
2849
- const content = await readFile6(CHATS_FILE, "utf-8");
2659
+ const content = await readFile5(CHATS_FILE, "utf-8");
2850
2660
  const parsed = JSON.parse(content);
2851
2661
  if (!Array.isArray(parsed)) {
2852
2662
  return [];
@@ -2875,33 +2685,38 @@ var ChatService = class {
2875
2685
  };
2876
2686
  await eventService.publish(event);
2877
2687
  }
2688
+ async publishAgentTurnCompleteWebhook() {
2689
+ const linearSessionId = ENGINE_ENV.LINEAR_SESSION_ID;
2690
+ const repoStatuses = await gitService.refreshRepos();
2691
+ const payload = linearSessionId ? { linearSessionId, repoStatuses } : { repoStatuses };
2692
+ await monolithService.sendEvent({ type: "agent_turn_complete", payload });
2693
+ }
2878
2694
  };
2879
2695
 
2880
2696
  // src/v1-routes.ts
2881
2697
  import { Hono } from "hono";
2882
2698
  import { z } from "zod";
2883
- import { randomUUID as randomUUID4 } from "crypto";
2884
2699
 
2885
- // src/plan-service.ts
2886
- import { readdir as readdir4, readFile as readFile7 } from "fs/promises";
2887
- import { homedir as homedir10 } from "os";
2888
- import { basename as basename2, join as join10 } from "path";
2700
+ // src/services/plan-service.ts
2701
+ import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
2702
+ import { homedir as homedir9 } from "os";
2703
+ import { basename, join as join10 } from "path";
2889
2704
  var PLAN_DIRECTORIES = [
2890
- join10(homedir10(), ".claude", "plans"),
2891
- join10(homedir10(), ".replicas", "plans")
2705
+ join10(homedir9(), ".claude", "plans"),
2706
+ join10(homedir9(), ".replicas", "plans")
2892
2707
  ];
2893
2708
  function isMarkdownFile(filename) {
2894
2709
  return filename.toLowerCase().endsWith(".md");
2895
2710
  }
2896
2711
  function sanitizePlanFilename(filename) {
2897
- return basename2(filename);
2712
+ return basename(filename);
2898
2713
  }
2899
2714
  var PlanService = class {
2900
2715
  async listPlans() {
2901
2716
  const planNames = /* @__PURE__ */ new Set();
2902
2717
  for (const directory of PLAN_DIRECTORIES) {
2903
2718
  try {
2904
- const entries = await readdir4(directory, { withFileTypes: true });
2719
+ const entries = await readdir3(directory, { withFileTypes: true });
2905
2720
  for (const entry of entries) {
2906
2721
  if (!entry.isFile()) {
2907
2722
  continue;
@@ -2924,7 +2739,7 @@ var PlanService = class {
2924
2739
  for (const directory of PLAN_DIRECTORIES) {
2925
2740
  const filePath = join10(directory, safeFilename);
2926
2741
  try {
2927
- const content = await readFile7(filePath, "utf-8");
2742
+ const content = await readFile6(filePath, "utf-8");
2928
2743
  return { filename: safeFilename, content };
2929
2744
  } catch {
2930
2745
  }
@@ -2937,8 +2752,7 @@ var planService = new PlanService();
2937
2752
  // src/v1-routes.ts
2938
2753
  var createChatSchema = z.object({
2939
2754
  provider: z.enum(["claude", "codex"]),
2940
- title: z.string().min(1).optional(),
2941
- repoScope: z.union([z.literal("all"), z.array(z.string().min(1))]).optional()
2755
+ title: z.string().min(1).optional()
2942
2756
  });
2943
2757
  var imageMediaTypeSchema = z.enum(IMAGE_MEDIA_TYPES);
2944
2758
  var sendMessageSchema = z.object({
@@ -3110,39 +2924,15 @@ function createV1Routes(deps) {
3110
2924
  return c.json(jsonError("Failed to interrupt chat", error instanceof Error ? error.message : "Unknown error"), 404);
3111
2925
  }
3112
2926
  });
3113
- app2.post("/chats/:chatId/reset", async (c) => {
3114
- try {
3115
- await deps.chatService.reset(c.req.param("chatId"));
3116
- return c.json({ success: true });
3117
- } catch (error) {
3118
- if (error instanceof ChatNotFoundError) {
3119
- return c.json(jsonError("Failed to reset chat", error.message), 404);
3120
- }
3121
- return c.json(jsonError("Failed to reset chat", error instanceof Error ? error.message : "Unknown error"), 404);
3122
- }
3123
- });
3124
2927
  app2.get("/repos", async (c) => {
3125
- const repos = await repoService.listRepos();
2928
+ const includeDiffs = c.req.query("includeDiffs") === "true";
2929
+ const repos = await gitService.listRepos({ includeDiffs });
3126
2930
  const response = {
3127
2931
  repos,
3128
- workspaceRoot: repoService.getWorkspaceRoot()
2932
+ workspaceRoot: gitService.getWorkspaceRoot()
3129
2933
  };
3130
2934
  return c.json(response);
3131
2935
  });
3132
- app2.post("/repos/refresh", async (c) => {
3133
- const repos = await repoService.listRepos();
3134
- await eventService.publish({
3135
- id: randomUUID4(),
3136
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3137
- type: "repo.discovered",
3138
- payload: { repos }
3139
- });
3140
- return c.json({ repos, workspaceRoot: repoService.getWorkspaceRoot() });
3141
- });
3142
- app2.get("/repos/status", async (c) => {
3143
- const repos = await repoService.listRepoStatuses();
3144
- return c.json({ repos, workspaceRoot: repoService.getWorkspaceRoot() });
3145
- });
3146
2936
  app2.get("/plans", async (c) => {
3147
2937
  try {
3148
2938
  const plans = await planService.listPlans();
@@ -3189,7 +2979,7 @@ var READY_MESSAGE = "========= REPLICAS WORKSPACE READY ==========";
3189
2979
  var COMPLETION_MESSAGE = "========= REPLICAS WORKSPACE INITIALIZATION COMPLETE ==========";
3190
2980
  function checkActiveSSHSessions() {
3191
2981
  try {
3192
- const output = execSync2('who | grep -v "^$" | wc -l', { encoding: "utf-8" });
2982
+ const output = execSync('who | grep -v "^$" | wc -l', { encoding: "utf-8" });
3193
2983
  const sessionCount = parseInt(output.trim(), 10);
3194
2984
  return sessionCount > 0;
3195
2985
  } catch {
@@ -3201,10 +2991,7 @@ var bootTimeMs = Date.now();
3201
2991
  var engineReady = false;
3202
2992
  var authMiddleware = async (c, next) => {
3203
2993
  const secret = c.req.header("X-Replicas-Engine-Secret");
3204
- const expectedSecret = process.env.REPLICAS_ENGINE_SECRET;
3205
- if (!expectedSecret) {
3206
- return c.json({ error: "Server configuration error: REPLICAS_ENGINE_SECRET not set" }, 500);
3207
- }
2994
+ const expectedSecret = ENGINE_ENV.REPLICAS_ENGINE_SECRET;
3208
2995
  if (!secret) {
3209
2996
  return c.json({ error: "Unauthorized: X-Replicas-Engine-Secret header required" }, 401);
3210
2997
  }
@@ -3216,13 +3003,13 @@ var authMiddleware = async (c, next) => {
3216
3003
  }
3217
3004
  await next();
3218
3005
  };
3219
- var chatService = new ChatService(repoService.getWorkspaceRoot());
3006
+ var chatService = new ChatService(gitService.getWorkspaceRoot());
3220
3007
  app.get("/health", async (c) => {
3221
3008
  if (!engineReady) {
3222
3009
  return c.json({ status: "initializing", timestamp: (/* @__PURE__ */ new Date()).toISOString() }, 503);
3223
3010
  }
3224
3011
  try {
3225
- const logContent = await readFile8("/var/log/cloud-init-output.log", "utf-8");
3012
+ const logContent = await readFile7("/var/log/cloud-init-output.log", "utf-8");
3226
3013
  let status;
3227
3014
  if (logContent.includes(COMPLETION_MESSAGE)) {
3228
3015
  status = "active";
@@ -3239,7 +3026,7 @@ app.get("/health", async (c) => {
3239
3026
  app.use("*", authMiddleware);
3240
3027
  app.get("/status", async (c) => {
3241
3028
  try {
3242
- const repos = await repoService.listRepos();
3029
+ const repos = await gitService.listRepos();
3243
3030
  const status = {
3244
3031
  uptimeSeconds: Math.floor((Date.now() - bootTimeMs) / 1e3),
3245
3032
  hasActiveSSHSessions: checkActiveSSHSessions(),
@@ -3265,22 +3052,43 @@ app.get("/token-refresh/health", async (c) => {
3265
3052
  });
3266
3053
  });
3267
3054
  app.route("/", createV1Routes({ chatService }));
3268
- var port = Number(process.env.PORT) || 3737;
3055
+ var port = ENGINE_ENV.REPLICAS_ENGINE_PORT;
3269
3056
  function startStatusBroadcaster() {
3270
3057
  let previousRepoStatus = "";
3271
3058
  let previousHookStatus = "";
3272
3059
  let previousEngineStatus = "";
3273
3060
  let lastHooksRunning = replicasConfigService.areHooksRunning();
3274
3061
  setInterval(() => {
3275
- repoService.listRepoStatuses().then(async (repos) => {
3062
+ gitService.listRepos().then((repos) => {
3276
3063
  const serialized = JSON.stringify(repos);
3277
3064
  if (serialized !== previousRepoStatus) {
3278
3065
  previousRepoStatus = serialized;
3279
- await eventService.publish({
3280
- id: randomUUID5(),
3066
+ eventService.publish({
3067
+ id: randomUUID4(),
3281
3068
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3282
3069
  type: "repo.status.changed",
3283
3070
  payload: { repos }
3071
+ }).catch(() => {
3072
+ });
3073
+ }
3074
+ const engineStatus = {
3075
+ uptimeSeconds: Math.floor((Date.now() - bootTimeMs) / 1e3),
3076
+ hasActiveSSHSessions: checkActiveSSHSessions(),
3077
+ activeSseClients: eventService.getSubscriberCount(),
3078
+ chatsTotal: chatService.listChats().length,
3079
+ chatsProcessing: chatService.getProcessingCount(),
3080
+ reposTotal: repos.length,
3081
+ hooksRunning: replicasConfigService.areHooksRunning()
3082
+ };
3083
+ const engineStatusJson = JSON.stringify(engineStatus);
3084
+ if (engineStatusJson !== previousEngineStatus) {
3085
+ previousEngineStatus = engineStatusJson;
3086
+ eventService.publish({
3087
+ id: randomUUID4(),
3088
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3089
+ type: "engine.status.changed",
3090
+ payload: { status: engineStatus }
3091
+ }).catch(() => {
3284
3092
  });
3285
3093
  }
3286
3094
  }).catch(() => {
@@ -3297,7 +3105,7 @@ function startStatusBroadcaster() {
3297
3105
  previousHookStatus = hookSnapshot;
3298
3106
  if (!lastHooksRunning && hooksRunning) {
3299
3107
  eventService.publish({
3300
- id: randomUUID5(),
3108
+ id: randomUUID4(),
3301
3109
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3302
3110
  type: "hooks.started",
3303
3111
  payload: { running: true, completed: false }
@@ -3306,7 +3114,7 @@ function startStatusBroadcaster() {
3306
3114
  }
3307
3115
  if (hooksRunning) {
3308
3116
  eventService.publish({
3309
- id: randomUUID5(),
3117
+ id: randomUUID4(),
3310
3118
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3311
3119
  type: "hooks.progress",
3312
3120
  payload: { running: true, completed: false }
@@ -3315,7 +3123,7 @@ function startStatusBroadcaster() {
3315
3123
  }
3316
3124
  if (lastHooksRunning && !hooksRunning && hooksCompleted && !hooksFailed) {
3317
3125
  eventService.publish({
3318
- id: randomUUID5(),
3126
+ id: randomUUID4(),
3319
3127
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3320
3128
  type: "hooks.completed",
3321
3129
  payload: { running: false, completed: true }
@@ -3324,7 +3132,7 @@ function startStatusBroadcaster() {
3324
3132
  }
3325
3133
  if (lastHooksRunning && !hooksRunning && hooksFailed) {
3326
3134
  eventService.publish({
3327
- id: randomUUID5(),
3135
+ id: randomUUID4(),
3328
3136
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3329
3137
  type: "hooks.failed",
3330
3138
  payload: { running: false, completed: hooksCompleted }
@@ -3332,7 +3140,7 @@ function startStatusBroadcaster() {
3332
3140
  });
3333
3141
  }
3334
3142
  eventService.publish({
3335
- id: randomUUID5(),
3143
+ id: randomUUID4(),
3336
3144
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3337
3145
  type: "hooks.status",
3338
3146
  payload: {
@@ -3343,29 +3151,6 @@ function startStatusBroadcaster() {
3343
3151
  });
3344
3152
  lastHooksRunning = hooksRunning;
3345
3153
  }
3346
- repoService.listRepos().then((repos) => {
3347
- const engineStatus = {
3348
- uptimeSeconds: Math.floor((Date.now() - bootTimeMs) / 1e3),
3349
- hasActiveSSHSessions: checkActiveSSHSessions(),
3350
- activeSseClients: eventService.getSubscriberCount(),
3351
- chatsTotal: chatService.listChats().length,
3352
- chatsProcessing: chatService.getProcessingCount(),
3353
- reposTotal: repos.length,
3354
- hooksRunning: replicasConfigService.areHooksRunning()
3355
- };
3356
- const engineStatusJson = JSON.stringify(engineStatus);
3357
- if (engineStatusJson !== previousEngineStatus) {
3358
- previousEngineStatus = engineStatusJson;
3359
- eventService.publish({
3360
- id: randomUUID5(),
3361
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3362
- type: "engine.status.changed",
3363
- payload: { status: engineStatus }
3364
- }).catch(() => {
3365
- });
3366
- }
3367
- }).catch(() => {
3368
- });
3369
3154
  }, 2e3);
3370
3155
  }
3371
3156
  serve(
@@ -3375,7 +3160,7 @@ serve(
3375
3160
  },
3376
3161
  async (info) => {
3377
3162
  console.log(`Replicas Engine running on port ${info.port}`);
3378
- const gitResult = await initializeGitRepository();
3163
+ const gitResult = await gitService.initializeGitRepository();
3379
3164
  if (!gitResult.success) {
3380
3165
  console.warn(`Git initialization warning: ${gitResult.error}`);
3381
3166
  }
@@ -3385,22 +3170,22 @@ serve(
3385
3170
  await githubTokenManager.start();
3386
3171
  await claudeTokenManager.start();
3387
3172
  await codexTokenManager.start();
3388
- const repos = await repoService.listRepos();
3173
+ const repos = await gitService.listRepos();
3389
3174
  await eventService.publish({
3390
- id: randomUUID5(),
3175
+ id: randomUUID4(),
3391
3176
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3392
3177
  type: "repo.discovered",
3393
3178
  payload: { repos }
3394
3179
  });
3395
- const repoStatuses = await repoService.listRepoStatuses();
3180
+ const repoStatuses = await gitService.listRepos();
3396
3181
  await eventService.publish({
3397
- id: randomUUID5(),
3182
+ id: randomUUID4(),
3398
3183
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3399
3184
  type: "repo.status.changed",
3400
3185
  payload: { repos: repoStatuses }
3401
3186
  });
3402
3187
  await eventService.publish({
3403
- id: randomUUID5(),
3188
+ id: randomUUID4(),
3404
3189
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3405
3190
  type: "engine.ready",
3406
3191
  payload: { version: "v1" }