@agentmeshhq/agent 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +6 -0
  2. package/dist/__tests__/context-template.test.d.ts +4 -0
  3. package/dist/__tests__/context-template.test.js +235 -0
  4. package/dist/__tests__/context-template.test.js.map +1 -0
  5. package/dist/__tests__/default-branch-fallback.test.d.ts +10 -0
  6. package/dist/__tests__/default-branch-fallback.test.js +123 -0
  7. package/dist/__tests__/default-branch-fallback.test.js.map +1 -0
  8. package/dist/__tests__/default-repo-fallback.test.d.ts +12 -0
  9. package/dist/__tests__/default-repo-fallback.test.js +156 -0
  10. package/dist/__tests__/default-repo-fallback.test.js.map +1 -0
  11. package/dist/__tests__/loader.test.js +140 -28
  12. package/dist/__tests__/loader.test.js.map +1 -1
  13. package/dist/__tests__/no-respawn.test.d.ts +1 -0
  14. package/dist/__tests__/no-respawn.test.js +254 -0
  15. package/dist/__tests__/no-respawn.test.js.map +1 -0
  16. package/dist/__tests__/onboard.test.d.ts +5 -0
  17. package/dist/__tests__/onboard.test.js +343 -0
  18. package/dist/__tests__/onboard.test.js.map +1 -0
  19. package/dist/__tests__/project-flag.test.d.ts +9 -0
  20. package/dist/__tests__/project-flag.test.js +140 -0
  21. package/dist/__tests__/project-flag.test.js.map +1 -0
  22. package/dist/__tests__/shared-resource-guards.test.d.ts +7 -0
  23. package/dist/__tests__/shared-resource-guards.test.js +260 -0
  24. package/dist/__tests__/shared-resource-guards.test.js.map +1 -0
  25. package/dist/cli/index.js +6 -0
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/start.d.ts +4 -0
  28. package/dist/cli/start.js +6 -0
  29. package/dist/cli/start.js.map +1 -1
  30. package/dist/config/loader.d.ts +0 -4
  31. package/dist/config/loader.js +102 -42
  32. package/dist/config/loader.js.map +1 -1
  33. package/dist/config/schema.d.ts +2 -2
  34. package/dist/core/daemon/bootstrap.d.ts +4 -0
  35. package/dist/core/daemon/bootstrap.js +2 -0
  36. package/dist/core/daemon/bootstrap.js.map +1 -1
  37. package/dist/core/daemon/context-template.d.ts +11 -0
  38. package/dist/core/daemon/context-template.js +144 -0
  39. package/dist/core/daemon/context-template.js.map +1 -0
  40. package/dist/core/daemon/crash-log.d.ts +0 -2
  41. package/dist/core/daemon/crash-log.js +0 -1
  42. package/dist/core/daemon/crash-log.js.map +1 -1
  43. package/dist/core/daemon/git-auth.d.ts +18 -0
  44. package/dist/core/daemon/git-auth.js +94 -0
  45. package/dist/core/daemon/git-auth.js.map +1 -0
  46. package/dist/core/daemon/health-policy.d.ts +0 -4
  47. package/dist/core/daemon/health-policy.js +0 -8
  48. package/dist/core/daemon/health-policy.js.map +1 -1
  49. package/dist/core/daemon/state.js +1 -0
  50. package/dist/core/daemon/state.js.map +1 -1
  51. package/dist/core/daemon/workspace.d.ts +13 -0
  52. package/dist/core/daemon/workspace.js +39 -0
  53. package/dist/core/daemon/workspace.js.map +1 -1
  54. package/dist/core/daemon.d.ts +19 -6
  55. package/dist/core/daemon.js +337 -199
  56. package/dist/core/daemon.js.map +1 -1
  57. package/dist/core/injector.d.ts +5 -1
  58. package/dist/core/injector.js +77 -0
  59. package/dist/core/injector.js.map +1 -1
  60. package/dist/core/registry.d.ts +97 -0
  61. package/dist/core/registry.js +59 -0
  62. package/dist/core/registry.js.map +1 -1
  63. package/dist/core/tmux-runtime.js +4 -0
  64. package/dist/core/tmux-runtime.js.map +1 -1
  65. package/package.json +1 -1
@@ -1,32 +1,30 @@
1
- import { spawn } from "node:child_process";
1
+ import { execSync, spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { getAgentState, resetAgentRestartCount, updateAgentInState } from "../config/loader.js";
5
+ import { getAgentState, loadState, updateAgentInState } from "../config/loader.js";
6
6
  import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
7
7
  import { renderMissingWorkdirMessage } from "./daemon/assignment-message.js";
8
8
  import { bootstrapDaemon } from "./daemon/bootstrap.js";
9
+ import { removeClaudeMd, writeClaudeMd } from "./daemon/context-template.js";
9
10
  import { formatCrashLog } from "./daemon/crash-log.js";
10
- import { getNudgeMessage, getStuckDetail, isWithinNudgeWaitWindow, shouldResetRestartCount, } from "./daemon/health-policy.js";
11
+ import { cleanupGitAuth, setupGitAuth } from "./daemon/git-auth.js";
12
+ import { getNudgeMessage, getStuckDetail, isWithinNudgeWaitWindow, } from "./daemon/health-policy.js";
11
13
  import { writeSandboxOpencodeConfig } from "./daemon/sandbox-config.js";
12
14
  import { captureAgentChildPids, persistRunningState } from "./daemon/state.js";
13
15
  import { startTmuxRuntimeSession } from "./daemon/tmux-session.js";
14
- import { setupWorkspace } from "./daemon/workspace.js";
16
+ import { configureGitIdentity, setupWorkspace, validatePushAccess } from "./daemon/workspace.js";
15
17
  import { Heartbeat } from "./heartbeat.js";
16
- import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
17
- import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
18
+ import { handleWebSocketEvent, injectOnboardMessage, injectRestoredContext, injectStartupMessage, } from "./injector.js";
19
+ import { checkInbox, createSelfAssignment, fetchAssignments, fetchOnboard, fetchProjectByCode, registerAgent, } from "./registry.js";
18
20
  import { getRunnerDisplayName } from "./runner.js";
19
21
  import { DockerSandbox } from "./sandbox.js";
20
- import { getLatestSessionId, snapshotSessionId, waitForNewSessionId } from "./session-id.js";
21
- import { captureSessionContext, captureSessionOutput, createSession, destroySession, isSessionHealthy, killProcessTree, updateSessionEnvironment, } from "./tmux.js";
22
+ import { getLatestSessionId, waitForNewSessionId } from "./session-id.js";
23
+ import { captureSessionContext, captureSessionOutput, destroySession, isSessionHealthy, killProcessTree, updateSessionEnvironment, } from "./tmux.js";
22
24
  import { prepareOpenCodeRuntime } from "./tmux-runtime.js";
23
25
  import { checkAgentProgress, cleanupOrphanContainers, isProcessRunning, sendNudge, } from "./watchdog.js";
24
26
  import { AgentWebSocket } from "./websocket.js";
25
- // Maximum number of auto-restart attempts
26
- const MAX_RESTART_ATTEMPTS = 3;
27
- // Time after which restart count resets (30 minutes of stable operation)
28
- const RESTART_COUNT_RESET_MS = 30 * 60 * 1000;
29
- // Time to wait after nudging before restarting (2 minutes)
27
+ // Time to wait after nudging before marking as stuck (2 minutes)
30
28
  const NUDGE_WAIT_MS = 2 * 60 * 1000;
31
29
  // Path to the sandbox OpenCode config (permissive permissions)
32
30
  const SANDBOX_OPENCODE_CONFIG_PATH = path.join(os.homedir(), ".agentmesh", "opencode-sandbox.json");
@@ -56,14 +54,15 @@ export class AgentDaemon {
56
54
  sandboxImage;
57
55
  sandboxCpu;
58
56
  sandboxMemory;
57
+ onboardData = null;
59
58
  sandbox = null;
59
+ projectCode;
60
+ projectRole;
60
61
  healthCheckInterval = null;
61
62
  // Session resume tracking
62
63
  _preStartSessionId;
63
64
  _attemptedResumeSessionId;
64
- // Auto-restart tracking
65
- restartCount = 0;
66
- lastStableTime = null;
65
+ // Stuck detection tracking
67
66
  stuckSince = null;
68
67
  nudgeSentAt = null;
69
68
  constructor(options) {
@@ -80,6 +79,8 @@ export class AgentDaemon {
80
79
  this.sandboxImage = boot.sandboxImage;
81
80
  this.sandboxCpu = boot.sandboxCpu;
82
81
  this.sandboxMemory = boot.sandboxMemory;
82
+ this.projectCode = boot.projectCode;
83
+ this.projectRole = boot.projectRole;
83
84
  this.runnerConfig = boot.runnerConfig;
84
85
  const runnerName = getRunnerDisplayName(this.runnerConfig.type);
85
86
  console.log(`Runner: ${runnerName}`);
@@ -113,9 +114,6 @@ export class AgentDaemon {
113
114
  console.log(`Cleaned up ${cleaned} orphan container(s)`);
114
115
  }
115
116
  }
116
- // Reset restart count on manual start
117
- this.restartCount = 0;
118
- this.lastStableTime = new Date();
119
117
  // Register with hub first (needed for assignment check)
120
118
  console.log("Registering with AgentMesh hub...");
121
119
  console.log(`Existing state: ${existingState ? `agentId=${existingState.agentId}` : "none"}`);
@@ -139,8 +137,70 @@ export class AgentDaemon {
139
137
  else {
140
138
  console.log(`Registered as: ${this.agentId}`);
141
139
  }
142
- // Check assignments and auto-setup workdir if needed (before creating tmux session)
143
- await this.checkAssignments();
140
+ // Fetch onboard payload (project context, credentials, team)
141
+ try {
142
+ this.onboardData = await fetchOnboard(this.config.hubUrl, this.token);
143
+ if (this.onboardData) {
144
+ console.log(`Onboard data received${this.onboardData.project ? `: project=${this.onboardData.project.name}` : ""}`);
145
+ // Set up git credentials before workspace setup
146
+ for (const repo of this.onboardData.repos) {
147
+ if (repo.credential) {
148
+ const extraEnv = setupGitAuth({
149
+ type: repo.credential.type,
150
+ value: repo.credential.value,
151
+ repoUrl: repo.url,
152
+ agentName: this.agentName,
153
+ });
154
+ Object.assign(this.runnerConfig.env, extraEnv);
155
+ }
156
+ }
157
+ // Cache onboard data locally for offline fallback
158
+ this.cacheOnboardData();
159
+ }
160
+ }
161
+ catch (error) {
162
+ console.log("Onboard fetch failed (non-fatal):", error.message);
163
+ // Try loading cached onboard data
164
+ this.onboardData = this.loadCachedOnboardData();
165
+ if (this.onboardData) {
166
+ console.log("Loaded cached onboard data");
167
+ }
168
+ }
169
+ // Resolve workdir: --project flag takes precedence, otherwise fall back to assignments
170
+ if (this.projectCode) {
171
+ await this.resolveProjectWorkdir();
172
+ }
173
+ else {
174
+ await this.checkAssignments();
175
+ }
176
+ // Enforce unique working directories — no two running agents may share the same workdir
177
+ if (this.agentConfig.workdir) {
178
+ const resolvedWorkdir = path.resolve(this.agentConfig.workdir);
179
+ const state = loadState();
180
+ const conflict = state.agents.find((a) => a.name !== this.agentName &&
181
+ a.workdir &&
182
+ path.resolve(a.workdir) === resolvedWorkdir &&
183
+ a.pid > 0 &&
184
+ isProcessRunning(a.pid));
185
+ if (conflict) {
186
+ throw new Error(`Workdir conflict: "${resolvedWorkdir}" is already in use by agent "${conflict.name}" (PID: ${conflict.pid}).\n` +
187
+ `Each agent must have its own working directory to avoid git conflicts.\n` +
188
+ `Use --workdir to specify a unique path, e.g.:\n` +
189
+ ` agentmesh start --name ${this.agentName} --workdir ~/Dev/${this.assignedProject || "project"}-${this.agentName}`);
190
+ }
191
+ }
192
+ // Check for serve port collision
193
+ if (this.serveMode) {
194
+ const state = loadState();
195
+ const portConflict = state.agents.find((a) => a.name !== this.agentName &&
196
+ a.servePort === this.servePort &&
197
+ a.pid > 0 &&
198
+ isProcessRunning(a.pid));
199
+ if (portConflict) {
200
+ throw new Error(`Port ${this.servePort} already in use by agent "${portConflict.name}" (PID: ${portConflict.pid}).\n` +
201
+ `Use --serve-port to specify a different port.`);
202
+ }
203
+ }
144
204
  // Choose runtime mode: sandbox > serve > tmux
145
205
  if (this.sandboxMode) {
146
206
  await this.startSandboxMode();
@@ -277,6 +337,20 @@ export class AgentDaemon {
277
337
  console.error("Failed to check inbox:", error);
278
338
  injectStartupMessage(this.agentName, 0);
279
339
  }
340
+ // Inject onboard project context
341
+ if (this.onboardData?.project) {
342
+ injectOnboardMessage(this.agentName, this.onboardData);
343
+ }
344
+ // Write persistent CLAUDE.md context file
345
+ if (this.onboardData && this.agentConfig.workdir) {
346
+ const mdPath = writeClaudeMd({
347
+ workdir: this.agentConfig.workdir,
348
+ onboard: this.onboardData,
349
+ });
350
+ if (mdPath) {
351
+ console.log(`✓ CLAUDE.md written: ${mdPath}`);
352
+ }
353
+ }
280
354
  // Restore context from previous session
281
355
  if (this.shouldRestoreContext && this.agentId) {
282
356
  console.log("Checking for previous context...");
@@ -368,17 +442,11 @@ Nudge agent:
368
442
  this.healthCheckInterval = setInterval(async () => {
369
443
  if (!this.isRunning)
370
444
  return;
371
- // Reset restart count after stable operation
372
- if (shouldResetRestartCount(this.restartCount, this.lastStableTime, RESTART_COUNT_RESET_MS)) {
373
- console.log(`[HEALTH] Agent stable for 30+ minutes, resetting restart count`);
374
- this.restartCount = 0;
375
- resetAgentRestartCount(this.agentName);
376
- }
377
445
  // For sandbox mode, pass container name so health check looks inside container
378
446
  const containerName = this.sandboxMode ? this.sandbox?.getContainerName() : undefined;
379
447
  const health = isSessionHealthy(this.agentName, containerName);
380
448
  if (!health.healthy) {
381
- // Session died - attempt restart
449
+ // Session died - log and mark as failed (no auto-restart)
382
450
  await this.handleSessionDeath(health.reason || "unknown", logDir);
383
451
  return;
384
452
  }
@@ -395,7 +463,7 @@ Nudge agent:
395
463
  console.log(`[HEALTH] Agent is waiting for human input: ${progress.details}`);
396
464
  }
397
465
  else if (progress.status === "permission_blocked" || progress.status === "stuck") {
398
- await this.handleStuckAgent(progress);
466
+ this.handleStuckAgent(progress);
399
467
  }
400
468
  else if (progress.status === "active") {
401
469
  // Agent is working - reset stuck tracking
@@ -405,7 +473,6 @@ Nudge agent:
405
473
  this.nudgeSentAt = null;
406
474
  updateAgentInState(this.agentName, { stuckSince: undefined, status: "running" });
407
475
  }
408
- this.lastStableTime = new Date();
409
476
  }
410
477
  }, 60000); // Check every 60 seconds
411
478
  }
@@ -428,57 +495,34 @@ Nudge agent:
428
495
  agentName: this.agentName,
429
496
  agentId: this.agentId,
430
497
  reason,
431
- restartCount: this.restartCount,
432
- maxRestartAttempts: MAX_RESTART_ATTEMPTS,
433
498
  sandboxLabel: this.sandboxMode ? this.sandbox?.getContainerName() || "sandbox" : "none",
434
499
  workdir: this.agentConfig.workdir,
435
500
  model: this.runnerConfig.model,
436
501
  lastOutput,
437
502
  });
438
503
  fs.appendFileSync(logFile, crashLog);
439
- // Save context (including session ID) before restart attempt
504
+ // Save context before marking as failed
440
505
  if (this.agentId) {
441
506
  this.saveAgentContext();
442
507
  }
443
- // Check if we can restart
444
- if (this.restartCount < MAX_RESTART_ATTEMPTS) {
445
- this.restartCount++;
446
- console.error(`[CRASH] Session died: ${reason}. Attempting restart (${this.restartCount}/${MAX_RESTART_ATTEMPTS})...`);
447
- updateAgentInState(this.agentName, {
448
- restartCount: this.restartCount,
449
- lastRestartAt: timestamp,
450
- status: "running",
451
- });
452
- try {
453
- await this.restartSession();
454
- console.log(`[RESTART] Agent restarted successfully`);
455
- this.lastStableTime = new Date();
456
- }
457
- catch (error) {
458
- console.error(`[RESTART] Failed to restart: ${error.message}`);
459
- }
460
- }
461
- else {
462
- // Exceeded restart limit - mark as failed
463
- console.error(`[FAILED] Agent exceeded restart limit (${MAX_RESTART_ATTEMPTS}). Manual intervention required.`);
464
- // Terminal bell to alert user
465
- process.stdout.write("\x07");
466
- updateAgentInState(this.agentName, {
467
- status: "failed",
468
- restartCount: this.restartCount,
469
- });
470
- // Stop monitoring
471
- this.isRunning = false;
472
- if (this.healthCheckInterval) {
473
- clearInterval(this.healthCheckInterval);
474
- this.healthCheckInterval = null;
475
- }
508
+ // Mark as failed no auto-restart, user must intervene
509
+ console.error(`[FAILED] Session died: ${reason}. Manual intervention required (use 'agentmesh restart ${this.agentName}').`);
510
+ // Terminal bell to alert user
511
+ process.stdout.write("\x07");
512
+ updateAgentInState(this.agentName, {
513
+ status: "failed",
514
+ });
515
+ // Stop monitoring
516
+ this.isRunning = false;
517
+ if (this.healthCheckInterval) {
518
+ clearInterval(this.healthCheckInterval);
519
+ this.healthCheckInterval = null;
476
520
  }
477
521
  }
478
522
  /**
479
523
  * Handles stuck agent - sends nudge first, then restarts if still stuck
480
524
  */
481
- async handleStuckAgent(progress) {
525
+ handleStuckAgent(progress) {
482
526
  const now = new Date();
483
527
  if (!this.stuckSince) {
484
528
  // First detection of stuck state
@@ -489,7 +533,7 @@ Nudge agent:
489
533
  status: "stuck",
490
534
  });
491
535
  }
492
- // Only nudge worker agents - others restart immediately
536
+ // Nudge worker agents don't escalate to restart
493
537
  if (this.isWorkerAgent) {
494
538
  // If we haven't sent a nudge yet, send one
495
539
  if (!this.nudgeSentAt) {
@@ -510,124 +554,9 @@ Nudge agent:
510
554
  // Still waiting for agent to respond to nudge
511
555
  return;
512
556
  }
557
+ // Nudge grace period expired — log warning but do NOT restart
558
+ console.log(`[HEALTH] Agent still stuck after nudge. Manual intervention required.`);
513
559
  }
514
- // Agent still stuck - trigger restart (or restart immediately if not a worker)
515
- console.log(`[HEALTH] Agent still stuck${this.isWorkerAgent ? " after nudge" : ""}, triggering restart...`);
516
- this.stuckSince = null;
517
- this.nudgeSentAt = null;
518
- await this.handleSessionDeath("stuck_after_nudge", path.join(os.homedir(), ".agentmesh", "logs"));
519
- }
520
- /**
521
- * Restarts the agent session (sandbox or non-sandbox)
522
- */
523
- async restartSession() {
524
- // Retrieve tracked child PIDs before destroying the session
525
- const currentState = getAgentState(this.agentName);
526
- const childPids = currentState?.childPids ?? [];
527
- // Destroy existing session AND kill all tracked child processes
528
- destroySession(this.agentName, childPids);
529
- // Allow cleanup to settle before spawning a new session
530
- await new Promise((resolve) => setTimeout(resolve, 1000));
531
- if (this.sandboxMode && this.sandbox) {
532
- // Restart sandbox container
533
- const newContainerId = await this.sandbox.restart();
534
- console.log(`[RESTART] New container: ${newContainerId.substring(0, 12)}`);
535
- // Recreate tmux session for sandbox
536
- const containerName = this.sandbox.getContainerName();
537
- // Build environment args for docker exec
538
- const envArgs = [];
539
- const allEnv = {
540
- ...this.runnerConfig.env,
541
- AGENT_TOKEN: this.token,
542
- AGENTMESH_AGENT_ID: this.agentId,
543
- };
544
- for (const [key, value] of Object.entries(allEnv)) {
545
- if (value !== undefined && value !== "") {
546
- envArgs.push(`-e "${key}=${value}"`);
547
- }
548
- }
549
- const envString = envArgs.join(" ");
550
- const modelArg = this.runnerConfig.env?.OPENCODE_MODEL
551
- ? ` --model ${this.runnerConfig.env.OPENCODE_MODEL}`
552
- : "";
553
- const dockerExecCommand = `docker exec -it ${envString} ${containerName} opencode${modelArg}`;
554
- const created = createSession(this.agentName, dockerExecCommand, undefined, undefined);
555
- if (!created) {
556
- throw new Error("Failed to create tmux session for restarted sandbox");
557
- }
558
- // Track new child PIDs and update state
559
- const newChildPids = captureAgentChildPids(this.agentName);
560
- updateAgentInState(this.agentName, {
561
- sandboxContainer: containerName,
562
- childPids: newChildPids,
563
- });
564
- if (newChildPids.length > 0) {
565
- console.log(`[RESTART] Tracking ${newChildPids.length} child PIDs: ${newChildPids.join(", ")}`);
566
- }
567
- }
568
- else {
569
- // Non-sandbox restart — load saved session ID for native resume
570
- let savedSessionId;
571
- let savedContext = null;
572
- if (this.agentId) {
573
- savedContext = loadContext(this.agentId);
574
- savedSessionId = savedContext?.custom?.opencodeSessionId;
575
- if (savedSessionId) {
576
- console.log(`[RESTART] Attempting to resume OpenCode session: ${savedSessionId}`);
577
- }
578
- }
579
- const preRestartSessionId = snapshotSessionId(this.agentName);
580
- const created = createSession(this.agentName, this.agentConfig.command, this.agentConfig.workdir, this.runnerConfig.env, savedSessionId);
581
- if (!created) {
582
- throw new Error("Failed to create tmux session");
583
- }
584
- // Re-inject environment
585
- updateSessionEnvironment(this.agentName, {
586
- AGENT_TOKEN: this.token,
587
- AGENTMESH_AGENT_ID: this.agentId,
588
- ...this.runnerConfig.env,
589
- });
590
- // Track new child PIDs
591
- const newChildPids = captureAgentChildPids(this.agentName);
592
- updateAgentInState(this.agentName, { childPids: newChildPids });
593
- if (newChildPids.length > 0) {
594
- console.log(`[RESTART] Tracking ${newChildPids.length} child PIDs: ${newChildPids.join(", ")}`);
595
- }
596
- // Verify native resume and fallback if needed
597
- if (savedSessionId && savedContext) {
598
- const newSessionId = await waitForNewSessionId(this.agentName, preRestartSessionId, 15000);
599
- if (!newSessionId) {
600
- const health = isSessionHealthy(this.agentName);
601
- const currentSessionId = getLatestSessionId(this.agentName);
602
- if (!health.healthy) {
603
- console.log(`[RESTART] Fallback: OpenCode not healthy, injecting text summary.`);
604
- savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
605
- saveContext(savedContext);
606
- injectRestoredContext(this.agentName, savedContext);
607
- }
608
- else if (currentSessionId === savedSessionId) {
609
- console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
610
- }
611
- else {
612
- console.log(`[RESTART] Fallback: session not found. Injecting text summary.`);
613
- savedContext.custom = { ...savedContext.custom, opencodeSessionId: undefined };
614
- saveContext(savedContext);
615
- injectRestoredContext(this.agentName, savedContext);
616
- }
617
- }
618
- else if (newSessionId === savedSessionId) {
619
- console.log(`[RESTART] Resumed OpenCode session ${savedSessionId}`);
620
- }
621
- else {
622
- console.log(`[RESTART] Fallback: resume failed (got ${newSessionId}). Injecting text summary.`);
623
- savedContext.custom = { ...savedContext.custom, opencodeSessionId: newSessionId };
624
- saveContext(savedContext);
625
- injectRestoredContext(this.agentName, savedContext);
626
- }
627
- }
628
- }
629
- // Wait for session to be ready
630
- await new Promise((resolve) => setTimeout(resolve, 2000));
631
560
  }
632
561
  async stop() {
633
562
  console.log(`\nStopping agent: ${this.agentName}`);
@@ -642,6 +571,12 @@ Nudge agent:
642
571
  console.log("Saving agent context...");
643
572
  this.saveAgentContext();
644
573
  }
574
+ // Clean up git credential files
575
+ cleanupGitAuth(this.agentName);
576
+ // Remove CLAUDE.md context file
577
+ if (this.agentConfig.workdir) {
578
+ removeClaudeMd(this.agentConfig.workdir);
579
+ }
645
580
  // Stop heartbeat
646
581
  if (this.heartbeat) {
647
582
  this.heartbeat.stop();
@@ -887,6 +822,84 @@ Logs: docker logs ${containerName}
887
822
  console.error("Failed to save agent context:", error);
888
823
  }
889
824
  }
825
+ /**
826
+ * Resolves workdir from --project flag: looks up project by code, clones repo, self-assigns.
827
+ */
828
+ async resolveProjectWorkdir() {
829
+ if (!this.token || !this.agentId || !this.projectCode)
830
+ return;
831
+ console.log(`Resolving project "${this.projectCode}" from HQ...`);
832
+ const result = await fetchProjectByCode(this.config.hubUrl, this.token, this.projectCode);
833
+ if (!result) {
834
+ throw new Error(`Project "${this.projectCode}" not found. Check the project code in the admin UI.`);
835
+ }
836
+ const { project, repo } = result;
837
+ this.assignedProject = project.name;
838
+ console.log(`Project found: ${project.name} (${project.code})`);
839
+ if (!repo) {
840
+ throw new Error(`Project "${project.name}" has no repositories configured. Add a repo in the admin UI first.`);
841
+ }
842
+ // Compute per-agent workdir
843
+ let uniqueWorkdir;
844
+ if (project.workdir) {
845
+ // Sibling directory: /Users/raju/Dev/agentmesh → /Users/raju/Dev/agentmesh-agent-1
846
+ uniqueWorkdir = path.join(path.dirname(project.workdir), `${path.basename(project.workdir)}-${this.agentName}`);
847
+ }
848
+ else {
849
+ // Fallback to managed workspace path
850
+ uniqueWorkdir = path.join(os.homedir(), ".agentmesh", "workspaces", this.config.workspace, project.code.toLowerCase(), this.agentName);
851
+ }
852
+ console.log(`Per-agent workdir: ${uniqueWorkdir}`);
853
+ // Clone or pull
854
+ const gitDir = path.join(uniqueWorkdir, ".git");
855
+ if (!fs.existsSync(gitDir)) {
856
+ this.agentConfig.workdir = setupWorkspace({
857
+ workspacePath: uniqueWorkdir,
858
+ repoUrl: repo.url,
859
+ defaultBranch: repo.default_branch ?? project.default_branch ?? "main",
860
+ projectName: project.name,
861
+ });
862
+ }
863
+ else {
864
+ console.log(`Workspace exists, pulling latest...`);
865
+ try {
866
+ execSync("git fetch origin && git pull", {
867
+ cwd: uniqueWorkdir,
868
+ stdio: "pipe",
869
+ timeout: 30_000,
870
+ });
871
+ }
872
+ catch (error) {
873
+ console.warn(`Could not pull latest: ${error.message}`);
874
+ }
875
+ this.agentConfig.workdir = uniqueWorkdir;
876
+ }
877
+ // Configure git identity + validate push access
878
+ configureGitIdentity({
879
+ workspacePath: uniqueWorkdir,
880
+ agentName: this.agentName,
881
+ agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
882
+ });
883
+ validatePushAccess(uniqueWorkdir);
884
+ // Auto-assign the agent to the project
885
+ try {
886
+ const assignmentResult = await createSelfAssignment(this.config.hubUrl, this.token, {
887
+ workspace_id: this.config.workspace,
888
+ project_id: project.project_id,
889
+ agent_id: this.agentId,
890
+ role: this.projectRole,
891
+ });
892
+ if (assignmentResult === "existing") {
893
+ console.log(`Already assigned to project ${project.name}`);
894
+ }
895
+ else {
896
+ console.log(`Auto-assigned to project ${project.name} as ${this.projectRole}`);
897
+ }
898
+ }
899
+ catch (error) {
900
+ console.warn(`Could not auto-assign to project: ${error.message}`);
901
+ }
902
+ }
890
903
  /**
891
904
  * Fetches assignments from HQ and validates workdir setup
892
905
  * Uses project.workdir from HQ as source of truth, falls back to helpful instructions
@@ -912,31 +925,111 @@ Logs: docker logs ${containerName}
912
925
  if (!this.agentConfig.workdir) {
913
926
  const assignmentWithWorkdir = assignments.find((a) => a.project.workdir);
914
927
  if (assignmentWithWorkdir?.project.workdir) {
915
- console.log(`Using workdir from project settings: ${assignmentWithWorkdir.project.workdir}`);
916
- this.agentConfig.workdir = assignmentWithWorkdir.project.workdir;
928
+ // Append agent name as sibling directory to avoid shared workdir conflicts
929
+ // e.g. /Users/raju/Dev/agentmesh -> /Users/raju/Dev/agentmesh-agent-1
930
+ const baseWorkdir = assignmentWithWorkdir.project.workdir;
931
+ const uniqueWorkdir = path.join(path.dirname(baseWorkdir), `${path.basename(baseWorkdir)}-${this.agentName}`);
932
+ console.log(`Using workdir from project settings (per-agent): ${uniqueWorkdir}`);
933
+ // If the unique dir doesn't exist, set up workspace (clone or pull)
934
+ const gitDir = path.join(uniqueWorkdir, ".git");
935
+ if (!fs.existsSync(gitDir)) {
936
+ // Find repo info from assignments or onboard data for cloning
937
+ const repoAssignment = assignments.find((a) => a.repo !== null);
938
+ const projectDefaultBranch = assignments[0]?.project.default_branch ?? null;
939
+ const projectDefaultRepoId = assignments[0]?.project.default_repo_id ?? null;
940
+ const defaultRepo = projectDefaultRepoId
941
+ ? this.onboardData?.repos.find((r) => r.repo_id === projectDefaultRepoId)
942
+ : null;
943
+ const fallbackRepo = defaultRepo ?? this.onboardData?.repos[0];
944
+ const repoInfo = repoAssignment?.repo ??
945
+ (fallbackRepo
946
+ ? {
947
+ url: fallbackRepo.url,
948
+ default_branch: projectDefaultBranch ?? fallbackRepo.default_branch ?? "main",
949
+ full_name: fallbackRepo.full_name,
950
+ }
951
+ : null);
952
+ const projectName = repoAssignment?.project.name ?? assignments[0]?.project.name ?? "unknown";
953
+ if (repoInfo) {
954
+ this.agentConfig.workdir = setupWorkspace({
955
+ workspacePath: uniqueWorkdir,
956
+ repoUrl: repoInfo.url,
957
+ defaultBranch: repoInfo.default_branch,
958
+ projectName,
959
+ });
960
+ configureGitIdentity({
961
+ workspacePath: uniqueWorkdir,
962
+ agentName: this.agentName,
963
+ agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
964
+ });
965
+ validatePushAccess(uniqueWorkdir);
966
+ }
967
+ else {
968
+ // Genuinely no repo info — just create the directory
969
+ fs.mkdirSync(uniqueWorkdir, { recursive: true });
970
+ this.agentConfig.workdir = uniqueWorkdir;
971
+ }
972
+ }
973
+ else {
974
+ // Directory exists with .git — pull latest
975
+ console.log(`Workspace exists, pulling latest...`);
976
+ try {
977
+ execSync("git fetch origin && git pull", {
978
+ cwd: uniqueWorkdir,
979
+ stdio: "pipe",
980
+ timeout: 30_000,
981
+ });
982
+ }
983
+ catch (error) {
984
+ console.warn(`Could not pull latest: ${error.message}`);
985
+ }
986
+ this.agentConfig.workdir = uniqueWorkdir;
987
+ }
917
988
  return;
918
989
  }
919
- // No project.workdir set, check if we have a repo assignment
990
+ // No project.workdir set, check if we have a repo assignment or onboard repo
920
991
  const repoAssignment = assignments.find((a) => a.repo !== null);
921
- if (repoAssignment) {
922
- const repo = repoAssignment.repo;
923
- const expandedPath = path.join(os.homedir(), ".agentmesh", "workspaces", this.config.workspace, repoAssignment.project.code.toLowerCase(), this.agentName);
924
- const suggestedPath = `~/.agentmesh/workspaces/${this.config.workspace}/${repoAssignment.project.code.toLowerCase()}/${this.agentName}`;
992
+ const projectDefaultBranch = assignments[0]?.project.default_branch ?? null;
993
+ const projectDefaultRepoId = assignments[0]?.project.default_repo_id ?? null;
994
+ const defaultRepo = projectDefaultRepoId
995
+ ? this.onboardData?.repos.find((r) => r.repo_id === projectDefaultRepoId)
996
+ : null;
997
+ const fallbackRepo = defaultRepo ?? this.onboardData?.repos[0];
998
+ const repoInfo = repoAssignment?.repo ??
999
+ (fallbackRepo
1000
+ ? {
1001
+ url: fallbackRepo.url,
1002
+ default_branch: projectDefaultBranch ?? fallbackRepo.default_branch ?? "main",
1003
+ full_name: fallbackRepo.full_name,
1004
+ }
1005
+ : null);
1006
+ const projectCode = repoAssignment?.project.code ?? assignments[0]?.project.code ?? "unknown";
1007
+ const projectName = repoAssignment?.project.name ?? assignments[0]?.project.name ?? "unknown";
1008
+ if (repoInfo) {
1009
+ const expandedPath = path.join(os.homedir(), ".agentmesh", "workspaces", this.config.workspace, projectCode.toLowerCase(), this.agentName);
1010
+ const suggestedPath = `~/.agentmesh/workspaces/${this.config.workspace}/${projectCode.toLowerCase()}/${this.agentName}`;
925
1011
  // If --auto-setup is enabled, automatically clone the repo
926
1012
  if (this.autoSetup) {
927
1013
  this.agentConfig.workdir = setupWorkspace({
928
1014
  workspacePath: expandedPath,
929
- repoUrl: repo.url,
930
- defaultBranch: repo.default_branch,
931
- projectName: repoAssignment.project.name,
1015
+ repoUrl: repoInfo.url,
1016
+ defaultBranch: repoInfo.default_branch,
1017
+ projectName,
1018
+ });
1019
+ // Configure git identity and validate push access
1020
+ configureGitIdentity({
1021
+ workspacePath: expandedPath,
1022
+ agentName: this.agentName,
1023
+ agentDisplayName: this.onboardData?.agent.display_name ?? this.agentName,
932
1024
  });
1025
+ validatePushAccess(expandedPath);
933
1026
  return;
934
1027
  }
935
1028
  console.error(renderMissingWorkdirMessage({
936
- projectName: repoAssignment.project.name,
937
- repoFullName: repo.full_name,
938
- repoUrl: repo.url,
939
- defaultBranch: repo.default_branch,
1029
+ projectName,
1030
+ repoFullName: repoInfo.full_name,
1031
+ repoUrl: repoInfo.url,
1032
+ defaultBranch: repoInfo.default_branch,
940
1033
  suggestedPath,
941
1034
  agentName: this.agentName,
942
1035
  }));
@@ -962,5 +1055,50 @@ Logs: docker logs ${containerName}
962
1055
  });
963
1056
  console.log(`Updated sandbox OpenCode config: ${SANDBOX_OPENCODE_CONFIG_PATH}`);
964
1057
  }
1058
+ /**
1059
+ * Caches onboard data to disk for offline fallback
1060
+ */
1061
+ cacheOnboardData() {
1062
+ if (!this.onboardData || !this.agentId)
1063
+ return;
1064
+ try {
1065
+ const cacheDir = path.join(os.homedir(), ".agentmesh", "context");
1066
+ if (!fs.existsSync(cacheDir)) {
1067
+ fs.mkdirSync(cacheDir, { recursive: true });
1068
+ }
1069
+ // Strip credential values before caching
1070
+ const cacheData = {
1071
+ ...this.onboardData,
1072
+ repos: this.onboardData.repos.map((r) => ({
1073
+ ...r,
1074
+ credential: r.credential ? { type: r.credential.type, value: "***" } : null,
1075
+ })),
1076
+ cached_at: new Date().toISOString(),
1077
+ };
1078
+ const cachePath = path.join(cacheDir, `${this.agentId}-onboard.json`);
1079
+ fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
1080
+ }
1081
+ catch (error) {
1082
+ console.log("Failed to cache onboard data:", error.message);
1083
+ }
1084
+ }
1085
+ /**
1086
+ * Loads cached onboard data from disk (without credentials)
1087
+ */
1088
+ loadCachedOnboardData() {
1089
+ if (!this.agentId)
1090
+ return null;
1091
+ try {
1092
+ const cachePath = path.join(os.homedir(), ".agentmesh", "context", `${this.agentId}-onboard.json`);
1093
+ if (!fs.existsSync(cachePath))
1094
+ return null;
1095
+ const raw = fs.readFileSync(cachePath, "utf-8");
1096
+ const data = JSON.parse(raw);
1097
+ return data;
1098
+ }
1099
+ catch {
1100
+ return null;
1101
+ }
1102
+ }
965
1103
  }
966
1104
  //# sourceMappingURL=daemon.js.map