# Investigation: Server startup DX — .env resolution & credential error guidance

**Issues**: #393 (https://github.com/dynamous-community/remote-coding-agent/issues/393), #394 (https://github.com/dynamous-community/remote-coding-agent/issues/394)
**Type**: BUG
**Investigated**: 2026-02-17

### Assessment

| Metric     | Value    | Reasoning                                                                                      |
| ---------- | -------- | ---------------------------------------------------------------------------------------------- |
| Severity   | HIGH     | Server cannot start in any worktree without manual symlink — blocks all worktree-based dev/agent workflows |
| Complexity | LOW      | 1 file to change (index.ts), isolated to startup path, no architectural changes needed         |
| Confidence | HIGH     | Root cause is clear and reproducible, CLI already has the correct pattern to follow             |

---

## Problem Statement

The server fails to start in git worktrees (and fresh clones) because `import 'dotenv/config'` resolves `.env` relative to `process.cwd()` which is `packages/server/` when run via `bun --filter`. A manually-created symlink `packages/server/.env -> ../../.env` exists in the main repo but is gitignored and not recreated in worktrees. When this causes env vars to be missing, the fatal error `no_ai_credentials` provides zero guidance about what's wrong or how to fix it.

---

## Analysis

### Root Cause

**Issue #393 — .env not found in worktrees:**

WHY: Server exits with `no_ai_credentials` in worktrees
↓ BECAUSE: Environment variables aren't loaded from `.env`
Evidence: `packages/server/src/index.ts:8` — `import 'dotenv/config';`

↓ BECAUSE: `dotenv/config` resolves `.env` from `process.cwd()` which is `packages/server/` when run via workspace filter
Evidence: dotenv source — `path.resolve(process.cwd(), '.env')`

↓ BECAUSE: `packages/server/.env` is a symlink that only exists in the main repo (gitignored at `.gitignore:80`)
Evidence: `.gitignore:80` — `packages/server/.env`

↓ ROOT CAUSE: The server relies on a manual symlink that isn't tracked in git and isn't auto-created
Evidence: `packages/server/src/index.ts:7` — `// Note: packages/server/.env is a symlink to the root .env file`

**Issue #394 — Unhelpful error message:**

WHY: Developer has no idea what went wrong
↓ BECAUSE: The fatal log message is just a string label with no context
Evidence: `packages/server/src/index.ts:78` — `getLog().fatal('no_ai_credentials');`

↓ ROOT CAUSE: The Pino migration (commit c7cd67e, #388) replaced a multi-line `console.error` with a terse structured log message, losing all human-readable guidance

### Affected Files

| File                              | Lines | Action | Description                                          |
| --------------------------------- | ----- | ------ | ---------------------------------------------------- |
| `packages/server/src/index.ts`    | 6-8   | UPDATE | Replace `import 'dotenv/config'` with explicit path resolution |
| `packages/server/src/index.ts`    | 77-87 | UPDATE | Add actionable guidance to credential error messages |
| `packages/server/package.json`    | 18    | UPDATE | Remove `dotenv` dependency (use Bun native loading)  |

### Integration Points

- `packages/server/package.json:7` — `dev` script runs from `packages/server/` directory
- `package.json:12` — `dev:server` invokes via `bun --filter @archon/server dev`
- `packages/cli/src/cli.ts:11-31` — CLI already solves this correctly with explicit dotenv path
- `packages/core/src/clients/claude.ts:46-82` — downstream consumer of the env vars loaded here

### Git History

- **dotenv import introduced**: 718e01b (2026-01-20) — "Phase 1 - Monorepo structure" — the symlink comment was added during monorepo migration
- **Credential check original**: ca74774c (2025-11-11) — Cole Medin's original implementation
- **Pino migration**: c7cd67e (2026-02-16) — "Add Pino structured logging" — replaced console.error with terse log.fatal
- **Implication**: Pre-existing bug since monorepo migration, made worse by logging migration

---

## Implementation Plan

### Step 1: Replace `import 'dotenv/config'` with explicit .env path resolution

**File**: `packages/server/src/index.ts`
**Lines**: 6-8
**Action**: UPDATE

**Current code:**

```typescript
// Load environment variables FIRST
// Note: packages/server/.env is a symlink to the root .env file
import 'dotenv/config';
```

**Required change:**

```typescript
// Load environment variables FIRST — resolve to monorepo root .env
// Uses dotenv with explicit path so it works from any CWD (worktrees, packages/server/, etc.)
import { config } from 'dotenv';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
// Resolve from this file's location: packages/server/src/ → ../../.. → repo root
const envPath = resolve(__dirname, '..', '..', '..', '.env');
const dotenvResult = config({ path: envPath });

if (dotenvResult.error) {
  // Use console.error since logger depends on env vars (LOG_LEVEL)
  console.error(`Failed to load .env from ${envPath}: ${dotenvResult.error.message}`);
  console.error('Hint: Copy .env.example to .env and configure your credentials.');
}
```

**Why**: This resolves `.env` relative to the source file location (`packages/server/src/index.ts` → `../../..` → repo root), which works regardless of `process.cwd()`. The CLI already uses this pattern successfully (`packages/cli/src/cli.ts:22-30`). Provides a helpful hint if .env is missing rather than silently failing.

---

### Step 2: Add actionable guidance to credential validation errors

**File**: `packages/server/src/index.ts`
**Lines**: 77-87
**Action**: UPDATE

**Current code:**

```typescript
if (!hasClaudeCredentials && !hasCodexCredentials) {
  getLog().fatal('no_ai_credentials');
  process.exit(1);
}

if (!hasClaudeCredentials) {
  getLog().warn('claude_credentials_missing');
}
if (!hasCodexCredentials) {
  getLog().warn('codex_credentials_missing');
}
```

**Required change:**

```typescript
if (!hasClaudeCredentials && !hasCodexCredentials) {
  getLog().fatal(
    {
      checked: {
        claude: ['CLAUDE_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_USE_GLOBAL_AUTH'],
        codex: ['CODEX_ID_TOKEN', 'CODEX_ACCESS_TOKEN'],
      },
      hints: [
        'Set CLAUDE_USE_GLOBAL_AUTH=true in .env (requires `claude /login` first)',
        'Or set CLAUDE_API_KEY in .env',
        'Or set CODEX_ID_TOKEN + CODEX_ACCESS_TOKEN in .env',
        'See .env.example for all options',
      ],
      envFile: envPath,
    },
    'no_ai_credentials'
  );
  process.exit(1);
}

if (!hasClaudeCredentials) {
  getLog().warn(
    { checked: ['CLAUDE_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_USE_GLOBAL_AUTH'] },
    'claude_credentials_missing'
  );
}
if (!hasCodexCredentials) {
  getLog().warn(
    { checked: ['CODEX_ID_TOKEN', 'CODEX_ACCESS_TOKEN'] },
    'codex_credentials_missing'
  );
}
```

**Why**: Pino's structured logging supports a data object as first argument. This preserves machine-readable structured output while providing actionable human-readable guidance when pretty-printed in a TTY. References the resolved `envPath` so users know exactly which .env file was (or wasn't) loaded.

---

### Step 3: Remove dotenv dependency from server package

**File**: `packages/server/package.json`
**Lines**: 18
**Action**: UPDATE

**Current code:**

```json
"dotenv": "^17.2.3",
```

**Required change:**

Keep dotenv — we're still using it, just with explicit `config({ path })` instead of `import 'dotenv/config'`. The import changes from the auto-loading side-effect import to the explicit function import.

**Actually**: No change needed here. We still use `dotenv` but via `import { config } from 'dotenv'` with explicit path. This is the same pattern as the CLI package.

---

### Step 4: Update tests if any exist for startup

**File**: `packages/server/src/index.test.ts` (or similar)
**Action**: CHECK — verify no tests break

Run `bun test packages/server/` to check. The startup validation is not unit-tested (it's the main entry point), but adjacent tests should still pass.

---

## Patterns to Follow

**From CLI — explicit dotenv loading with error handling:**

```typescript
// SOURCE: packages/cli/src/cli.ts:20-31
const globalEnvPath = resolve(process.env.HOME ?? '~', '.archon', '.env');
if (existsSync(globalEnvPath)) {
  const result = config({ path: globalEnvPath, override: true });
  if (result.error) {
    console.error(`Error loading .env from ${globalEnvPath}: ${result.error.message}`);
    console.error('Hint: Check for syntax errors in your .env file.');
    process.exit(1);
  }
}
```

**From Pino convention — data object first, message string second:**

```typescript
// SOURCE: packages/server/src/index.ts:51
getLog().error({ err: error, platform, conversationId }, 'message_processing_failed');
```

---

## Edge Cases & Risks

| Risk/Edge Case                             | Mitigation                                                           |
| ------------------------------------------ | -------------------------------------------------------------------- |
| `import.meta.url` not available            | Bun supports ESM `import.meta.url` natively — not a risk            |
| .env missing entirely (fresh clone)        | `config()` returns error, we log helpful hint but don't exit — credential check catches it |
| Docker deployment                          | Docker uses its own .env mechanism; dotenv error is non-fatal        |
| Bun auto-loads CWD `.env` before dotenv    | We use `config()` which merges, not overwrites — Bun's auto-load from CWD won't have the root .env in packages/server/ anyway |
| `__dirname` not in scope after migration   | We compute it from `import.meta.url` using standard ESM pattern      |

---

## Validation

### Automated Checks

```bash
bun run type-check
bun test packages/server/
bun run lint
```

### Manual Verification

1. **Worktree test**: `cd` to this worktree (which has no `packages/server/.env` symlink) and run `bun run dev:server` — should start successfully using root `.env`
2. **Missing .env test**: Temporarily rename `.env` and run `bun run dev:server` — should show helpful error with credential options and .env.example reference
3. **Main repo test**: Run from main repo to confirm no regression

---

## Scope Boundaries

**IN SCOPE:**
- Fix .env path resolution in `packages/server/src/index.ts`
- Add actionable guidance to credential validation errors
- Both #393 and #394 in a single change

**OUT OF SCOPE (do not touch):**
- CLI .env loading (already works correctly)
- Removing dotenv in favor of Bun native loading (future optimization, separate issue)
- Worktree file-copy config to auto-copy .env (separate concern)
- Adding .env loading to `@archon/core` (not needed, core expects env vars pre-loaded)

---

## Metadata

- **Investigated by**: Claude
- **Timestamp**: 2026-02-17
- **Artifact**: `.claude/PRPs/issues/issue-393-394.md`
