# Cross-Repo DRY Migration Roadmap

> **Status:** ✅ **COMPLETE** — Phases 0–4 implemented and pushed
> **Created:** 2026-03-20
> **Completed:** 2026-03-20
> **Audit:** See [`../CROSS_REPO_DRY_AUDIT.md`](../CROSS_REPO_DRY_AUDIT.md)
> **Total effort:** ~11 days across 5 phases
> **Total LOC eliminated:** ~2,700 duplicated lines across 9 product repos + 5 web dashboards

---

## Phase Overview

| Phase | Name                                                | New Packages | Repos Touched | Est. Effort | Status |
| ----- | --------------------------------------------------- | ------------ | ------------- | ----------- | ------ |
| **0** | Quick Wins (no new packages)                        | 0            | 9             | 1 day       | ✅     |
| **1** | `@bytelyst/fastify-auth`                            | 1            | 10            | 3 days      | ✅     |
| **2** | `@bytelyst/backend-config` + DB_PROVIDER cleanup    | 1            | 12            | 3 days      | ✅     |
| **3** | Backend utilities (flags, telemetry, domain events) | 2            | 3             | 2 days      | ✅     |
| **4** | Web client DRY (telemetry, diagnostics, config)     | 0            | 7             | 2 days      | ✅     |

**Repos involved (9 product backends):**

| Short Name  | Repo                                | Backend Port | Backend Tests |
| ----------- | ----------------------------------- | ------------ | ------------- |
| LysnrAI     | `learning_voice_ai_agent`           | 4015         | 67            |
| MindLyst    | `learning_multimodal_memory_agents` | 4014         | 63            |
| ChronoMind  | `learning_ai_clock`                 | 4011         | 176           |
| JarvisJr    | `learning_ai_jarvis_jr`             | 4012         | 203           |
| NomGap      | `learning_ai_fastgap`               | 4013         | 203           |
| PeakPulse   | `learning_ai_peakpulse`             | 4010         | 59            |
| FlowMonk    | `learning_ai_flowmonk`              | 4017         | 181           |
| NoteLett    | `learning_ai_notes`                 | 4016         | 80            |
| ActionTrail | `learning_ai_trails`                | 4018         | 185           |

---

## Phase 0 — Quick Wins (No New Packages)

> **Goal:** Delete dead re-export files, standardize product identity loading.
> **Effort:** ~1 day
> **Risk:** Low — mechanical changes only.

### 0.1 Delete `errors.ts` re-export files

These files exist in 8 repos and contain only re-exports from `@bytelyst/errors`. Every module that imports from `./errors.js` should import from `@bytelyst/errors` directly (FlowMonk already does this).

**Files to delete:**

| Repo        | File                        |
| ----------- | --------------------------- |
| LysnrAI     | `backend/src/lib/errors.ts` |
| MindLyst    | `backend/src/lib/errors.ts` |
| ChronoMind  | `backend/src/lib/errors.ts` |
| JarvisJr    | `backend/src/lib/errors.ts` |
| NomGap      | `backend/src/lib/errors.ts` |
| PeakPulse   | `backend/src/lib/errors.ts` |
| NoteLett    | `backend/src/lib/errors.ts` |
| ActionTrail | `backend/src/lib/errors.ts` |

**Steps per repo:**

1. `grep -rn "from './errors.js'" backend/src/` → list all importing files
2. Replace `from './errors.js'` with `from '@bytelyst/errors'` in each file
3. Also replace `from '../lib/errors.js'` and `from '../../lib/errors.js'` patterns
4. Delete `backend/src/lib/errors.ts`
5. Run: `cd backend && npm run typecheck && npm test`

**Commit:** `refactor(backend): remove errors.ts re-export, import @bytelyst/errors directly`

**Completed commits:**

- PeakPulse `6381597`, MindLyst `2f18801`, LysnrAI `dfa289b`, ChronoMind `b4a6c62`
- JarvisJr `6432a21`, NomGap `5f842c6`, NoteLett `2a32a54`, ActionTrail `232990e`

### 0.2 Add `product-config.ts` to repos that hardcode product IDs

Six repos hardcode `const PRODUCT_ID = '<name>'` in `request-context.ts` instead of reading from `shared/product.json`.

**Repos needing `product-config.ts`:**

| Repo       | Hardcoded PRODUCT_ID                   | Port |
| ---------- | -------------------------------------- | ---- |
| LysnrAI    | `'lysnrai'` in `request-context.ts`    | 4015 |
| MindLyst   | `'mindlyst'` in `request-context.ts`   | 4014 |
| ChronoMind | `'chronomind'` in `request-context.ts` | 4011 |
| JarvisJr   | `'jarvisjr'` in `request-context.ts`   | 4012 |
| NomGap     | `'nomgap'` in `request-context.ts`     | 4013 |
| PeakPulse  | `'peakpulse'` in `request-context.ts`  | 4010 |

**Steps per repo:**

1. Verify `shared/product.json` exists with correct `productId`
2. Create `backend/src/lib/product-config.ts`:

   ```ts
   import { readFileSync } from 'node:fs';
   import { resolve, dirname } from 'node:path';
   import { fileURLToPath } from 'node:url';

   const __dirname = dirname(fileURLToPath(import.meta.url));
   // Path works from both src/ (dev with tsx) and dist/ (compiled):
   // src/lib/ -> ../../shared/  OR  dist/lib/ -> ../../shared/
   // Both resolve to <repo>/shared/product.json since backend/ is a subdirectory.
   const raw = JSON.parse(readFileSync(resolve(__dirname, '../../../shared/product.json'), 'utf8'));

   export const PRODUCT_ID: string = raw.productId;
   export const productConfig = raw as {
     productId: string;
     displayName: string;
     domain: string;
     backendPort: number;
     [key: string]: unknown;
   };
   ```

   > **IMPORTANT:** The relative path `../../../shared/product.json` resolves from
   > `backend/src/lib/` (3 levels up = repo root) and also from `backend/dist/lib/`
   > (3 levels up = repo root). Verify with: `ls $(dirname $0)/../../../shared/product.json`.
   > For repos where `backend/` is at repo root, the path may need to be `../../../../shared/product.json`
   > — check the existing `product-config.ts` in FlowMonk/ActionTrail/NoteLett for the correct depth.

3. Update `request-context.ts`: replace `const PRODUCT_ID = '<hardcoded>'` with `import { PRODUCT_ID } from './product-config.js'`
4. Update `config.ts` if it hardcodes the port: use `productConfig.backendPort`
5. Run: `cd backend && npm run typecheck && npm test`

**Commit:** `refactor(backend): load product identity from shared/product.json`

**Completed commits:**

- PeakPulse `f4c91f8`, MindLyst `211107f`, LysnrAI `541d389`
- ChronoMind `3087246`, JarvisJr `7e9f9c9`, NomGap `d8da403`

### 0.3 Verification checkpoint

```bash
# Run ALL backend tests across all 9 repos
for repo in learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock \
  learning_ai_jarvis_jr learning_ai_fastgap learning_ai_peakpulse \
  learning_ai_flowmonk learning_ai_notes learning_ai_trails; do
  echo "=== $repo ===" && cd /Users/sd9235/code/mygh/$repo/backend && npm test 2>&1 | tail -3 && cd -
done
```

**Expected:** All 1,217 backend tests pass (unchanged count).

---

## Phase 1 — `@bytelyst/fastify-auth` Package

> **Goal:** Extract the duplicated JWT auth + request context into a single shared package. Delete `auth.ts` and `request-context.ts` from all 9 product repos.
> **Effort:** ~3 days
> **Risk:** Medium — auth is critical path. Must verify every repo's tests pass.

### 1.1 Create `packages/fastify-auth/`

**Location:** `learning_ai_common_plat/packages/fastify-auth/`

**Package API:**

```ts
// packages/fastify-auth/src/index.ts

export interface FastifyAuthConfig {
  /** HS256 JWT secret (required) */
  jwtSecret: string;
  /** RS256 JWKS URL from platform-service (optional, tried first) */
  jwksUrl?: string;
}

export interface RequestContextConfig {
  /** Product ID to enforce (e.g., 'nomgap') */
  productId: string;
  /** If true, returns 'demo-user' in non-production when no JWT present */
  allowDemoUser?: boolean;
}

// Re-export AuthPayload type from @bytelyst/auth
export type { AuthPayload } from '@bytelyst/auth';

/**
 * Extract and verify JWT from Authorization: Bearer header.
 * Tries RS256 via JWKS first, falls back to HS256.
 */
export function createExtractAuth(
  config: FastifyAuthConfig
): (req: { headers: { authorization?: string } }) => Promise<AuthPayload>;

/**
 * Require specific roles. Calls extractAuth first, then checks role.
 */
export function createRequireRole(
  config: FastifyAuthConfig
): (req: { headers: { authorization?: string } }, ...roles: string[]) => Promise<AuthPayload>;

/**
 * Create product-scoped request context helpers.
 */
export function createRequestContext(config: RequestContextConfig): {
  getUserId(req: FastifyRequest): string;
  getRequestProductId(req: FastifyRequest): string;
};
```

**Files to create:**

| File                                                          | Purpose                                             |
| ------------------------------------------------------------- | --------------------------------------------------- |
| `packages/fastify-auth/package.json`                          | Package manifest (`@bytelyst/fastify-auth`)         |
| `packages/fastify-auth/tsconfig.json`                         | Extends `../../tsconfig.base.json`                  |
| `packages/fastify-auth/src/index.ts`                          | Main exports                                        |
| `packages/fastify-auth/src/extract-auth.ts`                   | JWT verification (RS256 + HS256 fallback)           |
| `packages/fastify-auth/src/request-context.ts`                | Product-scoped getUserId + getRequestProductId      |
| `packages/fastify-auth/src/types.ts`                          | Fastify module augmentation (JwtPayload on request) |
| `packages/fastify-auth/src/__tests__/extract-auth.test.ts`    | Unit tests                                          |
| `packages/fastify-auth/src/__tests__/request-context.test.ts` | Unit tests                                          |

**Dependencies:**

```json
{
  "dependencies": {
    "@bytelyst/errors": "workspace:*"
  },
  "peerDependencies": {
    "jose": ">=5.0.0",
    "fastify": ">=5.0.0",
    "@bytelyst/auth": "workspace:*"
  }
}
```

**Steps:**

1. Create package files (scaffold from existing `@bytelyst/auth` package structure)
2. Extract the canonical `extractAuth()` from any repo (they're identical)
3. Parameterize: accept `{ jwtSecret, jwksUrl }` instead of importing `config`
4. Extract `createRequestContext()` — parameterize with `{ productId, allowDemoUser }`
5. Add Fastify module augmentation for `req.jwtPayload` and `req.apiKeyUserId`
6. Write tests (port existing `auth.test.ts` from any repo)
7. Build: `pnpm --filter @bytelyst/fastify-auth build`
8. Run tests: `pnpm --filter @bytelyst/fastify-auth test`

**Commit:** `feat(fastify-auth): new package — extractAuth, requireRole, createRequestContext`

**Completed commits:**

- common-plat: `f61a1f0` (create), `ea2cb4c` (getter fn support)

### 1.2 Migrate all 9 product backends

**For each repo, the migration is identical:**

**Files to modify:**

| Action      | File                                 | Change                                                                                       |
| ----------- | ------------------------------------ | -------------------------------------------------------------------------------------------- |
| **Add dep** | `backend/package.json`               | Add `"@bytelyst/fastify-auth": "file:../../learning_ai_common_plat/packages/fastify-auth"`   |
| **Rewrite** | `backend/src/lib/auth.ts`            | Replace 60-90 lines with ~10-line wrapper that calls `createExtractAuth()` with local config |
| **Rewrite** | `backend/src/lib/request-context.ts` | Replace 30-50 lines with ~5-line wrapper that calls `createRequestContext()`                 |
| **Verify**  | —                                    | `cd backend && npm install && npm run typecheck && npm test`                                 |

**New `auth.ts` (per repo, ~10 lines):**

```ts
import { createExtractAuth, createRequireRole } from '@bytelyst/fastify-auth';
import { config } from './config.js';
export type { AuthPayload } from '@bytelyst/fastify-auth';

export const extractAuth = createExtractAuth({
  jwtSecret: config.JWT_SECRET,
  jwksUrl: config.PLATFORM_JWKS_URL,
});

export const requireRole = createRequireRole({
  jwtSecret: config.JWT_SECRET,
  jwksUrl: config.PLATFORM_JWKS_URL,
});
```

**New `request-context.ts` (per repo, ~10 lines):**

```ts
import { createRequestContext } from '@bytelyst/fastify-auth';
import { PRODUCT_ID } from './product-config.js';
export type { JwtPayload } from '@bytelyst/fastify-auth';

const ctx = createRequestContext({ productId: PRODUCT_ID, allowDemoUser: true });
export const getUserId = ctx.getUserId;
export const getRequestProductId = ctx.getRequestProductId;
```

**Migration order (by test count — smallest first for safe iteration):**

| Order | Repo        | Tests | Notes                                                        |
| ----- | ----------- | ----- | ------------------------------------------------------------ |
| 1     | PeakPulse   | 59    | Simplest, good canary                                        |
| 2     | MindLyst    | 63    |                                                              |
| 3     | LysnrAI     | 67    | Has `audit.ts` extra import                                  |
| 4     | NoteLett    | 80    |                                                              |
| 5     | ChronoMind  | 176   |                                                              |
| 6     | FlowMonk    | 181   | Already imports `@bytelyst/errors` directly                  |
| 7     | ActionTrail | 185   | Has `apiKeyUserId` on request — include in type augmentation |
| 8     | JarvisJr    | 203   |                                                              |
| 9     | NomGap      | 203   |                                                              |

**Per-repo commit:** `refactor(backend): migrate auth to @bytelyst/fastify-auth`

**Completed commits:**

- PeakPulse `7d2aa7b`, LysnrAI `1399de8`, MindLyst `770a8f2`, ChronoMind `a7b0ae9`
- JarvisJr `2abf0b5`, NomGap `25970f4`, FlowMonk `8a324bf`, NoteLett `942d00c`, ActionTrail `e21fb48`

### 1.3 Delete `auth.test.ts` from repos that test shared logic

After migration, the `auth.test.ts` files in each repo test the same JWT logic that's now in the shared package. Keep only product-specific auth behavior tests; delete the ones testing `extractAuth()` and `requireRole()` directly (those now live in `packages/fastify-auth`).

**Files to evaluate (keep if product-specific, delete if duplicate):**

| Repo  | File                           | Expected Action                                             |
| ----- | ------------------------------ | ----------------------------------------------------------- |
| All 9 | `backend/src/lib/auth.test.ts` | Review — likely delete (logic now tested in shared package) |

### 1.4 Verification checkpoint

```bash
# Build shared package
cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/fastify-auth build && pnpm --filter @bytelyst/fastify-auth test

# Run ALL backend tests
for repo in learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock \
  learning_ai_jarvis_jr learning_ai_fastgap learning_ai_peakpulse \
  learning_ai_flowmonk learning_ai_notes learning_ai_trails; do
  echo "=== $repo ===" && cd /Users/sd9235/code/mygh/$repo/backend && npm install && npm run typecheck && npm test 2>&1 | tail -3 && cd -
done
```

**Expected:** All 1,217+ backend tests pass. Shared package has its own test suite (~15-20 tests).

---

## Phase 2 — `@bytelyst/backend-config` + Datastore Migration

> **Goal:** Extract the duplicated Zod env config schema into a composable factory. Migrate 3 older repos to `@bytelyst/datastore`.
> **Effort:** ~3 days
> **Risk:** Medium — config parsing is startup-critical.

### 2.1 Create `packages/backend-config/`

**Location:** `learning_ai_common_plat/packages/backend-config/`

**Package API:**

```ts
// packages/backend-config/src/index.ts
import { z } from 'zod';

/**
 * Base schema shared across ALL product backends.
 * Includes PORT, HOST, NODE_ENV, CORS_ORIGIN, SERVICE_NAME,
 * DB_PROVIDER, COSMOS_*, JWT_SECRET, PLATFORM_JWKS_URL.
 */
export const baseBackendSchema = z.object({
  PORT: z.coerce.number(),
  HOST: z.string().default('0.0.0.0'),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  CORS_ORIGIN: z.string().optional(),
  SERVICE_NAME: z.string(),
  DB_PROVIDER: z.enum(['cosmos', 'memory']).default('cosmos'),
  COSMOS_ENDPOINT: z.string().default(''),
  COSMOS_KEY: z.string().default(''),
  COSMOS_DATABASE: z.string().default('lysnrai'),
  JWT_SECRET: z.string().default('dev-secret-do-not-use-in-prod'),
  PLATFORM_JWKS_URL: z.string().url().optional(),
  PLATFORM_SERVICE_URL: z.string().default('http://localhost:4003'),
});

export type BaseBackendConfig = z.infer<typeof baseBackendSchema>;

/**
 * Create a product-specific config by extending the base schema.
 */
export function createBackendConfig<T extends z.ZodRawShape>(opts: {
  serviceName: string;
  defaultPort: number;
  defaultDatabase?: string;
  extraSchema?: z.ZodObject<T>;
}): BaseBackendConfig & z.infer<z.ZodObject<T>>;
```

**Files to create:**

| File                                                   | Purpose                                       |
| ------------------------------------------------------ | --------------------------------------------- |
| `packages/backend-config/package.json`                 | Package manifest                              |
| `packages/backend-config/tsconfig.json`                | TS config                                     |
| `packages/backend-config/src/index.ts`                 | Base schema + `createBackendConfig()` factory |
| `packages/backend-config/src/__tests__/config.test.ts` | Tests                                         |

**Steps:**

1. Create package scaffold
2. Extract common schema fields from the 9 existing `config.ts` files
3. Build the `createBackendConfig()` factory that merges base + extra schemas
4. Handle the Cosmos credential validation (required when `DB_PROVIDER=cosmos`)
5. Write tests
6. Build + test

**Commit:** `feat(backend-config): new package — composable Zod env schema factory`

**Completed commits:**

- common-plat: `08661bb` (create package, 8 tests)

### 2.2 Migrate all 9 product backends to `@bytelyst/backend-config`

**New `config.ts` per repo (example: NomGap):**

```ts
import { z } from 'zod';
import { createBackendConfig } from '@bytelyst/backend-config';

export const config = createBackendConfig({
  serviceName: 'nomgap-backend',
  defaultPort: 4013,
  extraSchema: z.object({
    CORS_ORIGIN: z.string().default('http://localhost:3040'),
  }),
});
```

**Migration order:** Same as Phase 1 (smallest test count first).

**Per-repo steps:**

1. Add `"@bytelyst/backend-config": "file:../../learning_ai_common_plat/packages/backend-config"` to `package.json`
2. Rewrite `config.ts` to use `createBackendConfig()`
3. Ensure all modules still find the config fields they need
4. `cd backend && npm install && npm run typecheck && npm test`

**Per-repo commit:** `refactor(backend): migrate config to @bytelyst/backend-config`

**Completed commits (Phase 2.2 + 2.3 combined — DB_PROVIDER included in base schema):**

- PeakPulse `71dc558`, LysnrAI `75153d7`, MindLyst `45b4c0d`, ChronoMind `0bda7dd`
- JarvisJr `df5c652`, NomGap `6611643`, FlowMonk `6ce2f17`, NoteLett `b04189d`, ActionTrail `1ee7a4e`

### 2.3 Add `DB_PROVIDER` to config schema in 3 older repos

All 9 repos already use `@bytelyst/datastore` in their `datastore.ts`. However, 3 older repos (LysnrAI, MindLyst, ChronoMind) read `DB_PROVIDER` directly from `process.env` inside `datastore.ts` instead of declaring it in their Zod config schema. This means `DB_PROVIDER` isn't validated at startup.

**Repos to fix:**

| Repo       | Current Pattern                             | Target                                             |
| ---------- | ------------------------------------------- | -------------------------------------------------- |
| LysnrAI    | `process.env.DB_PROVIDER` in `datastore.ts` | `config.DB_PROVIDER` via Zod schema in `config.ts` |
| MindLyst   | `process.env.DB_PROVIDER` in `datastore.ts` | `config.DB_PROVIDER` via Zod schema in `config.ts` |
| ChronoMind | `process.env.DB_PROVIDER` in `datastore.ts` | `config.DB_PROVIDER` via Zod schema in `config.ts` |

**Steps per repo:**

1. Add `DB_PROVIDER: z.enum(['cosmos', 'memory']).default('cosmos')` to `config.ts` Zod schema
2. Update `datastore.ts` to read `config.DB_PROVIDER` instead of `process.env.DB_PROVIDER`
3. `cd backend && npm run typecheck && npm test`

**Note:** Do NOT delete `cosmos-init.ts` files — these register product-specific Cosmos containers and are still needed alongside `@bytelyst/datastore`.

**Per-repo commit:** `refactor(backend): add DB_PROVIDER to config schema`

### 2.4 Verification checkpoint

```bash
# Build shared packages
cd /Users/sd9235/code/mygh/learning_ai_common_plat
pnpm --filter @bytelyst/backend-config build && pnpm --filter @bytelyst/backend-config test

# Run ALL backend tests
for repo in learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock \
  learning_ai_jarvis_jr learning_ai_fastgap learning_ai_peakpulse \
  learning_ai_flowmonk learning_ai_notes learning_ai_trails; do
  echo "=== $repo ===" && cd /Users/sd9235/code/mygh/$repo/backend && npm install && npm run typecheck && npm test 2>&1 | tail -3 && cd -
done
```

**Expected:** All 1,217+ backend tests pass. All 9 repos now have `DB_PROVIDER` in their validated config schema.

---

## Phase 3 — Backend Utilities (Flags, Telemetry, Domain Events)

> **Goal:** Extract the 3 remaining duplicated backend patterns from FlowMonk + ActionTrail.
> **Effort:** ~2 days
> **Risk:** Low — only 2 repos affected, patterns are small.

### 3.1 Create `packages/backend-flags/`

**Source:** Identical code in FlowMonk + ActionTrail `backend/src/lib/feature-flags.ts`

**Package API:**

```ts
export interface FlagRegistry {
  isFeatureEnabled(flag: string, userId?: string): boolean;
  getAllFlags(): Record<string, boolean>;
  setFlag(flag: string, value: boolean): void;
}

export function createFlagRegistry(defaults: Record<string, boolean>): FlagRegistry;
```

**Files to create:** `package.json`, `tsconfig.json`, `src/index.ts`, `src/__tests__/flags.test.ts`

**Migration:**

- FlowMonk: rewrite `feature-flags.ts` to `import { createFlagRegistry } from '@bytelyst/backend-flags'`
- ActionTrail: same

**Commit:** `feat(backend-flags): new package — in-memory feature flag registry`

**Completed commits:**

- common-plat: `0d8c0a5` (create backend-flags + backend-telemetry packages)
- FlowMonk `afb3795`, ActionTrail `dc83702`

### 3.2 Create `packages/backend-telemetry/`

**Source:** Byte-for-byte identical code in FlowMonk + ActionTrail `backend/src/lib/telemetry.ts`

**Package API:**

```ts
export interface TelemetryBuffer {
  trackEvent(event: string, userId?: string, properties?: Record<string, unknown>): void;
  getBufferedEvents(): TelemetryEvent[];
  flushEvents(): TelemetryEvent[];
}

export function createTelemetryBuffer(opts: { enabled: boolean }): TelemetryBuffer;
```

**Files to create:** `package.json`, `tsconfig.json`, `src/index.ts`, `src/__tests__/telemetry.test.ts`

**Migration:**

- FlowMonk: rewrite `telemetry.ts` to import from `@bytelyst/backend-telemetry`
- ActionTrail: same

**Commit:** `feat(backend-telemetry): new package — server-side telemetry event buffer`

**(Completed in same commits as 3.1 above)**

### 3.3 Create `packages/domain-events/`

**Source:** Near-identical event bus infrastructure in FlowMonk + ActionTrail (~150 lines each)

**Package API:**

```ts
export interface DomainEventBusConfig {
  productId: string;
  webhookSecret?: string;
}

export interface DomainEventBus<TEvent extends { type: string }> {
  emit(event: TEvent): void;
  on(type: TEvent['type'], handler: (event: TEvent) => void): void;
  getSSEHub(): SSEHub;
  getEventStore(): EventStore;
  registerWebhookTargets(targets: WebhookTarget[]): void;
}

export function createDomainEventBus<TEvent extends { type: string }>(
  config: DomainEventBusConfig
): DomainEventBus<TEvent>;
```

**Product repos would only define their event type interfaces and call `createDomainEventBus()` — eliminating ~120 lines of wiring boilerplate per repo.**

**Files to create:** `package.json`, `tsconfig.json`, `src/index.ts`, `src/bus.ts`, `src/__tests__/bus.test.ts`

**Dependencies:**

```json
{
  "dependencies": {
    "@bytelyst/event-store": "workspace:*",
    "@bytelyst/fastify-sse": "workspace:*",
    "@bytelyst/webhook-dispatch": "workspace:*"
  }
}
```

**Migration:**

- FlowMonk: replace 150-line `events.ts` with ~30-line type definitions + `createDomainEventBus()`
- ActionTrail: same

**Commit:** `feat(domain-events): new package — typed event bus + SSE + webhook wiring`

**STATUS: DEFERRED** — FlowMonk and ActionTrail events.ts have diverged significantly (different event types, different wiring patterns). The ROI of a shared package is low for 2 repos with divergent implementations. Revisit if a 3rd product adopts the same event bus pattern.

### 3.4 Verification checkpoint

```bash
cd /Users/sd9235/code/mygh/learning_ai_common_plat
pnpm --filter @bytelyst/backend-flags build && pnpm --filter @bytelyst/backend-flags test
pnpm --filter @bytelyst/backend-telemetry build && pnpm --filter @bytelyst/backend-telemetry test
pnpm --filter @bytelyst/domain-events build && pnpm --filter @bytelyst/domain-events test

# Test affected repos
for repo in learning_ai_flowmonk learning_ai_trails; do
  echo "=== $repo ===" && cd /Users/sd9235/code/mygh/$repo/backend && npm install && npm run typecheck && npm test 2>&1 | tail -3 && cd -
done
```

**Expected:** FlowMonk (181 tests) + ActionTrail (185 tests) still pass. 3 new packages with their own test suites.

---

## Phase 4 — Web Client DRY

> **Goal:** Reduce duplication in web app `lib/` directories. Add convenience functions to existing packages rather than creating new ones.
> **Effort:** ~2 days
> **Risk:** Low — web wrappers are thin, changes are cosmetic.

**Completed commits:**

- common-plat: `b9e37bb` (createWebTelemetry + createWebDiagnostics)
- NomGap `d9ef758`, NoteLett `4a5a782`, ChronoMind `f2450b6`
- LysnrAI `605f1b5`, JarvisJr `a7cfa46`, LocalMemGPT `4435347`

**Results:**

| Repo        | telemetry.ts | diagnostics.ts | Web Tests |
| ----------- | ------------ | -------------- | --------- |
| NomGap      | 35 → 13      | 38 → 16        | 130/130   |
| NoteLett    | 35 → 13      | 43 → 16        | 14/14     |
| ChronoMind  | 54 → 42      | 53 → 22        | 394/394   |
| LysnrAI     | 61 → 53      | 60 → 31        | —         |
| JarvisJr    | —            | 55 → 22        | 32/32     |
| LocalMemGPT | 57 → 19      | 36 → 14        | 23/23     |

**4.3 product-config.ts standardization:** Deferred — each repo has different consumer shapes (object literal vs constants vs functions), and migration risk exceeds ROI.

### 4.1 Add `createWebTelemetry()` to `@bytelyst/telemetry-client`

**Current state:** NomGap, NoteLett, ChronoMind, JarvisJr each have ~35-line `telemetry.ts` that wraps `createTelemetryClient()` with identical boilerplate.

**Add to `packages/telemetry-client/src/index.ts`:**

```ts
export function createWebTelemetry(opts: {
  productId: string;
  channel: string;
  baseUrl?: string;
  transport?: 'fetch' | 'beacon';
}) {
  let initialized = false;
  const client = createTelemetryClient({
    productId: opts.productId,
    baseUrl: opts.baseUrl ?? 'http://localhost:4003/api',
    endpoint: '/telemetry/events',
    platform: 'web',
    channel: opts.channel,
    transport: opts.transport ?? 'fetch',
    appVersion: '0.1.0',
    buildNumber: '1',
    releaseChannel: 'dev',
    osFamily: 'other',
  });

  return {
    client,
    init() {
      if (initialized) return client;
      client.init();
      client.trackEvent('info', 'app_shell', 'web_app_initialized');
      initialized = true;
      return client;
    },
    trackPageView(page: string) {
      client.trackEvent('info', 'navigation', 'page_view', { feature: page });
    },
  };
}
```

**Migration per web app:**

- Replace ~35-line `telemetry.ts` with ~5-line wrapper:
  ```ts
  'use client';
  import { createWebTelemetry } from '@bytelyst/telemetry-client';
  const { client, init, trackPageView } = createWebTelemetry({
    productId: 'nomgap',
    channel: 'nomgap_web',
  });
  export { client as telemetryClient, init as initTelemetry, trackPageView };
  ```

**Repos:** NomGap, NoteLett, ChronoMind, LysnrAI (user-dashboard-web)

**Note:** JarvisJr does NOT have a `telemetry.ts` wrapper file.

**Commit:** `feat(telemetry-client): add createWebTelemetry() convenience`

### 4.2 Add `createWebDiagnostics()` to `@bytelyst/diagnostics-client`

**Current state:** NomGap, NoteLett, ChronoMind, JarvisJr each have ~40-line `diagnostics.ts` with identical DiagnosticsClient init + install ID generation.

**Add to `packages/diagnostics-client/`:**

```ts
export function createWebDiagnostics(opts: {
  productId: string;
  channel: string;
  serverUrl: string;
  getAuthToken: () => string;
}): { init(): void; stop(): void };
```

**Handles:** install ID generation, localStorage, default config, error swallowing.

**Migration per web app:** Replace ~40-line file with ~8-line wrapper.

**Repos:** NomGap, NoteLett, ChronoMind, JarvisJr, LysnrAI (user-dashboard-web)

**Commit:** `feat(diagnostics-client): add createWebDiagnostics() convenience`

### 4.3 Standardize web `product-config.ts` pattern

**Current state:** 5 different patterns across repos.

**Target pattern (already used by NomGap + NoteLett):**

```ts
import productIdentity from '../../../shared/product.json';

export const PRODUCT_NAME = process.env.NEXT_PUBLIC_PRODUCT_NAME ?? productIdentity.displayName;
export const PRODUCT_ID = process.env.NEXT_PUBLIC_PRODUCT_ID ?? productIdentity.productId;
export const PLATFORM_SERVICE_URL =
  process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'http://localhost:4003/api';
export const PLATFORM_SERVICE_ORIGIN = PLATFORM_SERVICE_URL.replace(/\/api\/?$/, '');
export const BACKEND_API_URL =
  process.env.NEXT_PUBLIC_BACKEND_API_URL ?? `http://localhost:${productIdentity.backendPort}/api`;
export const DIAGNOSTICS_URL = process.env.NEXT_PUBLIC_DIAGNOSTICS_URL ?? PLATFORM_SERVICE_ORIGIN;
export const TELEMETRY_TRANSPORT =
  process.env.NEXT_PUBLIC_TELEMETRY_TRANSPORT === 'beacon' ? 'beacon' : 'fetch';
```

**Repos to standardize:**

- FlowMonk web: uses inline object literal → adopt `shared/product.json` import
- ActionTrail web: same
- JarvisJr web: uses custom `getPlatformBaseURL()` → standardize
- ChronoMind web: uses `auth-api.ts` for product config → separate into `product-config.ts`
- LysnrAI user-dashboard-web: uses `@bytelyst/config` `loadProductIdentity()` → align with `shared/product.json` pattern

**Per-repo commit:** `refactor(web): standardize product-config.ts to shared/product.json pattern`

### 4.4 Verification checkpoint

```bash
# Build updated packages
cd /Users/sd9235/code/mygh/learning_ai_common_plat
pnpm --filter @bytelyst/telemetry-client build
pnpm --filter @bytelyst/diagnostics-client build

# Typecheck all web apps
for repo in learning_ai_fastgap/web learning_ai_notes/web learning_ai_clock/web \
  learning_ai_jarvis_jr/web learning_ai_flowmonk/web learning_ai_trails/web \
  learning_voice_ai_agent/user-dashboard-web; do
  echo "=== $repo ===" && cd /Users/sd9235/code/mygh/$repo && npx tsc --noEmit 2>&1 | tail -3 && cd -
done
```

---

## Commit & Push Strategy

Each phase should be committed and pushed independently:

| Phase | Commits                                                            | Push Strategy                              |
| ----- | ------------------------------------------------------------------ | ------------------------------------------ |
| **0** | 2 commits per repo (errors cleanup + product-config)               | Push all repos after phase complete        |
| **1** | 1 commit in common-plat (new package) + 1 commit per product repo  | Push common-plat first, then product repos |
| **2** | 1 commit in common-plat + 1-2 commits per product repo             | Push common-plat first, then product repos |
| **3** | 3 commits in common-plat + 1 commit each in FlowMonk + ActionTrail | Push common-plat first                     |
| **4** | 2 commits in common-plat + 1 commit per web app                    | Push common-plat first                     |

**Total commits: ~35-40 across all repos**

---

## Rollback Plan

Every migration follows the same rollback strategy:

1. **Product repos reference shared packages via `file:` refs** — no npm publish needed
2. **Each phase is independently revertable** — revert the product repo commit and the old `lib/` files return
3. **Shared packages are additive** — creating `@bytelyst/fastify-auth` doesn't break anything that doesn't use it
4. **Test gate:** Every phase ends with a verification checkpoint. If any repo's tests fail, stop and fix before continuing.

---

## Final State (After All Phases)

### Backend `lib/` directory (target state per product repo)

| File                 | Source                                                  | Lines |
| -------------------- | ------------------------------------------------------- | ----- |
| `config.ts`          | `createBackendConfig()` from `@bytelyst/backend-config` | ~8-15 |
| `auth.ts`            | `createExtractAuth()` from `@bytelyst/fastify-auth`     | ~10   |
| `request-context.ts` | `createRequestContext()` from `@bytelyst/fastify-auth`  | ~6    |
| `product-config.ts`  | Reads `shared/product.json`                             | ~12   |
| `datastore.ts`       | `@bytelyst/datastore` with `DB_PROVIDER`                | ~15   |
| ~~`errors.ts`~~      | **DELETED** — import `@bytelyst/errors` directly        | 0     |

**Average backend `lib/` shrinks from ~200 lines to ~60 lines (70% reduction).**

### New packages in common-plat

| Package                       | Phase | Tests | Purpose                                         |
| ----------------------------- | ----- | ----- | ----------------------------------------------- |
| `@bytelyst/fastify-auth`      | 1     | ~20   | JWT auth + request context for Fastify backends |
| `@bytelyst/backend-config`    | 2     | ~10   | Composable Zod env config factory               |
| `@bytelyst/backend-flags`     | 3     | ~8    | In-memory feature flag registry                 |
| `@bytelyst/backend-telemetry` | 3     | ~8    | Server-side telemetry buffer                    |
| `@bytelyst/domain-events`     | 3     | ~12   | Event bus + SSE + webhook factory               |

**Total new shared tests: ~58**

### Updated packages in common-plat

| Package                        | Phase | Change                               |
| ------------------------------ | ----- | ------------------------------------ |
| `@bytelyst/telemetry-client`   | 4     | `createWebTelemetry()` convenience   |
| `@bytelyst/diagnostics-client` | 4     | `createWebDiagnostics()` convenience |

---

## Success Metrics

| Metric                                      | Before | After                               |
| ------------------------------------------- | ------ | ----------------------------------- |
| Duplicated backend `lib/` LOC               | ~1,700 | ~540 (thin wrappers)                |
| Duplicated web `lib/` LOC                   | ~1,000 | ~400 (thin wrappers)                |
| Total duplicated LOC                        | ~2,700 | ~940                                |
| **LOC eliminated**                          | —      | **~1,760**                          |
| Shared packages                             | 50     | 55                                  |
| Repos with `DB_PROVIDER` in config schema   | 6      | 9                                   |
| Repos with standardized `product-config.ts` | 3      | 9                                   |
| Backend test count                          | 1,217  | 1,217+ (unchanged + ~58 new shared) |

> **Note on package count:** The 50-package baseline counts directories in
> `learning_ai_common_plat/packages/` that contain a `package.json`. The 5 new
> packages proposed in this roadmap bring the total to 55.
