Skip to main content

Per-Org Agent Runtime Routing Implementation Plan

SUPERSEDED β€” 2026-08-05, ADR-046. The agent_runtimes per-org endpoint-routing model described here predates the managed Hermes architecture: one resident Bun machine per supervisor, one shared organization AgentFS overlay, one separate organization SessionDB, and one native Hermes child per conversation holder. For how a turn runs today see docs/architecture/agent-runtime-end-shape.md. Kept as historical record; do not implement the plan below.

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Persist one agent runtime row per org, auto-provision it on org creation, and route conversation + Expert side-chat agent calls through the org-specific endpoint.

Architecture: Add agent_runtimes as the control-plane dispatch table in Postgres/TypeORM. AgentLifecycleService becomes the only writer for runtime registration/configuration, and AgentClient.forOrg(orgId) becomes the only read path used by downstream dispatch. Phase A keeps behavior unchanged by storing the existing shared Railway agent endpoint in every org row.

Tech Stack: NestJS 11, TypeORM, PostgreSQL production migrations, better-sqlite3/in-memory tests, Jest, existing FastAPI agent /v1/* contract.


File Structure​

  • Create api/migrations/1748200000001-CreateAgentRuntimes.ts
    • Creates agent_runtimes with unique org_id, endpoint/status/config/token fields, timestamps, and indexes.
  • Modify api/src/common/entities.ts
    • Add AgentRuntime entity and AgentRuntimeStatus type.
  • Modify api/src/agent-api/agent-lifecycle.service.ts
    • Inject Repository<AgentRuntime>.
    • Replace in-memory Map with DB upsert/read/update/delete.
    • Resolve transitional endpoint from AGENT_SERVICE_URL, fallback AGENT_URL, fallback https://agent-dev-5b3e.up.railway.app in non-prod/dev only.
    • Log a warning when AGENT_URL fallback is used.
  • Modify api/src/agent-api/agent-api.module.ts
    • Register AgentRuntime in TypeOrmModule.forFeature.
  • Modify api/src/organizations/organizations.service.ts
    • Inject optional AgentLifecycleService.
    • Call provisionAgent(saved.id) after successful org creation in both createOrganization and createAmOrg.
    • Failure policy: log and continue in Phase A if provisioning fails? No β€” direct call should fail loudly for new org creation unless explicitly decided otherwise. For first implementation, throw after org save is ugly because org already exists; better: log error and mark runtime missing? I recommend provisioning failure throws before returning but records enough log. If the user wants org create to always succeed, switch to event/reconciler. For Phase A dogfood, direct call + throw is acceptable.
  • Modify api/src/organizations/organizations.module.ts
    • Import AgentApiModule, or preferably a small runtime module if created. For this plan, import AgentApiModule to minimize scope.
  • Modify api/src/common/agent.client.ts
    • Inject optional Repository<AgentRuntime>.
    • Add forOrg(orgId): Promise<AgentClient> or scoped helper.
    • Do not mutate singleton baseUrl.
    • Internal constructor/private factory should accept an override base URL.
    • Every HTTP method must use the scoped baseUrl.
  • Modify api/src/conversations/conversations.module.ts
    • Register AgentRuntime in TypeOrmModule.forFeature so AgentClient can inject its repo.
  • Modify api/src/conversations/conversations.service.ts
    • Replace this.agentClient.chat(...) and chatStream(...) with await this.agentClient.forOrg(orgId).
  • Modify api/src/conversations/expert-agent-thread.service.ts
    • Replace this.agentClient.chat(...) and chatStream(...) with await this.agentClient.forOrg(conversation.orgId).
  • Create api/scripts/backfill-agent-runtimes.ts or add a service method plus one-off script.
    • For every organization without a runtime row, call provisionAgent(org.id).
    • Phase A endpoint points to shared Railway.
  • Modify tests:
    • api/test/agent-lifecycle.service.spec.ts or new api/test/agent-lifecycle.spec.ts
    • api/test/agent-client.spec.ts
    • api/test/conversations.spec.ts
    • api/test/expert-agent-thread.service.spec.ts
    • api/test/expert-agent-thread.stream.spec.ts
    • Add org-create provisioning test, likely new api/test/org-agent-provisioning.spec.ts to avoid bloating unrelated tests.

Chunk 1: Persistence model and lifecycle service​

Task 1: Add the AgentRuntime entity and migration​

Files:

  • Modify: api/src/common/entities.ts

  • Create: api/migrations/1748200000001-CreateAgentRuntimes.ts

  • Test: api/test/agent-lifecycle.spec.ts

  • Step 1: Write failing lifecycle persistence test

Create api/test/agent-lifecycle.spec.ts with a Nest testing module registering AgentRuntime and AgentLifecycleService. Test that provisionAgent(orgId) creates one row and returns the same row on a second call.

Core expectation:

const first = await service.provisionAgent(orgId);
const second = await service.provisionAgent(orgId);
const rows = await runtimeRepo.find({ where: { orgId } });

expect(rows).toHaveLength(1);
expect(second.agentId).toBe(first.agentId);
expect(rows[0].endpointUrl).toBe('https://agent-dev-5b3e.up.railway.app');
  • Step 2: Run test to verify it fails

Run:

cd api
npm test -- agent-lifecycle.spec.ts --runInBand

Expected: FAIL because AgentRuntime entity/table and DB-backed lifecycle do not exist.

  • Step 3: Add entity

Add to api/src/common/entities.ts near other agent/runtime entities:

export type AgentRuntimeStatus = "provisioning" | "ready" | "failed" | "disabled";

@Entity("agent_runtimes")
@Unique(["orgId"])
@Index(["orgId"])
@Index(["status"])
export class AgentRuntime {
@PrimaryGeneratedColumn("uuid")
id: string;

@Column({ name: "org_id", type: "uuid" })
orgId: string;

@Column({ name: "agent_id" })
agentId: string;

@Column({ name: "endpoint_url" })
endpointUrl: string;

@Column({ name: "token", type: "text" })
token: string;

@Column({ name: "token_expires_at" })
tokenExpiresAt: Date;

@Column({ default: "ready" })
status: AgentRuntimeStatus;

@Column({ type: "jsonb", nullable: false, default: {} })
config: Record<string, unknown>;

@Column({ name: "provisioned_at" })
provisionedAt: Date;

@CreateDateColumn({ name: "created_at" })
createdAt: Date;

@UpdateDateColumn({ name: "updated_at" })
updatedAt: Date;
}

Note: if SQLite tests choke on jsonb, follow existing test patterns. This repo already uses jsonb in entities with better-sqlite compatibility in many specs, but if this specific test module fails, change test DB setup to match existing helpers instead of weakening prod schema.

  • Step 4: Add migration

Create api/migrations/1748200000001-CreateAgentRuntimes.ts:

import { MigrationInterface, QueryRunner } from "typeorm";

export class CreateAgentRuntimes1748200000001 implements MigrationInterface {
name = "CreateAgentRuntimes1748200000001";

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "agent_runtimes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"org_id" uuid NOT NULL,
"agent_id" varchar NOT NULL,
"endpoint_url" varchar NOT NULL,
"token" text NOT NULL,
"token_expires_at" timestamptz NOT NULL,
"status" varchar NOT NULL DEFAULT 'ready',
"config" jsonb NOT NULL DEFAULT '{}'::jsonb,
"provisioned_at" timestamptz NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "idx_agent_runtimes_org_id_unique"
ON "agent_runtimes" ("org_id")
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "idx_agent_runtimes_status"
ON "agent_runtimes" ("status")
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS "idx_agent_runtimes_status"`);
await queryRunner.query(`DROP INDEX IF EXISTS "idx_agent_runtimes_org_id_unique"`);
await queryRunner.query(`DROP TABLE IF EXISTS "agent_runtimes"`);
}
}
  • Step 5: Register entity in AgentApiModule

Modify api/src/agent-api/agent-api.module.ts to include AgentRuntime in the import from ../common/entities and in TypeOrmModule.forFeature([...]).

  • Step 6: Run test to verify entity registration progresses

Run:

cd api
npm test -- agent-lifecycle.spec.ts --runInBand

Expected: still FAIL because AgentLifecycleService still uses the Map.

Task 2: Rewrite AgentLifecycleService to persist registrations​

Files:

  • Modify: api/src/agent-api/agent-lifecycle.service.ts

  • Test: api/test/agent-lifecycle.spec.ts

  • Step 1: Add tests for endpoint resolution and token refresh

Add cases:

it("uses AGENT_SERVICE_URL before AGENT_URL", async () => { ... });
it("uses AGENT_URL as transitional fallback", async () => { ... });
it("refreshes expired registration token without creating duplicate rows", async () => { ... });
  • Step 2: Run tests to verify failures

Run:

cd api
npm test -- agent-lifecycle.spec.ts --runInBand

Expected: FAIL.

  • Step 3: Inject repo and remove Map

In AgentLifecycleService constructor:

constructor(
private readonly jwtService: JwtService,
private readonly config: ConfigService,
@InjectRepository(AgentRuntime)
private readonly runtimeRepo: Repository<AgentRuntime>,
) {}

Remove private readonly registry = new Map....

  • Step 4: Implement endpoint resolver

Add private method:

private resolveDefaultEndpointUrl(): string {
const serviceUrl = this.config.get<string>('AGENT_SERVICE_URL');
if (serviceUrl) return serviceUrl;

const legacyUrl = this.config.get<string>('AGENT_URL');
if (legacyUrl) {
this.logger.warn('AGENT_URL is deprecated; write agent_runtimes.endpoint_url / use AGENT_SERVICE_URL instead');
return legacyUrl;
}

const appEnv = this.config.get<string>('APP_ENV') ?? 'development';
if (appEnv === 'production') {
throw new Error('No default agent endpoint configured for production');
}
return 'https://agent-dev-5b3e.up.railway.app';
}
  • Step 5: Implement DB-backed provisionAgent

Pseudo-code:

async provisionAgent(orgId: string) {
const existing = await this.runtimeRepo.findOne({ where: { orgId } });
if (existing && existing.tokenExpiresAt > new Date()) {
return { agentId: existing.agentId, token: existing.token, expiresAt: existing.tokenExpiresAt };
}

const agentId = existing?.agentId ?? `agent-${orgId.slice(0, 8)}-${Date.now()}`;
const token = this.jwtService.sign(claims, { expiresIn: '24h' });
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
const endpointUrl = existing?.endpointUrl ?? this.resolveDefaultEndpointUrl();

await this.runtimeRepo.save(this.runtimeRepo.create({
...(existing ?? {}),
orgId,
agentId,
endpointUrl,
token,
tokenExpiresAt: expiresAt,
status: 'ready',
config: existing?.config ?? {},
provisionedAt: existing?.provisionedAt ?? new Date(),
}));

return { agentId, token, expiresAt };
}
  • Step 6: Rewrite configureAgent, teardownAgent, getRegistration

configureAgent reads runtime row, merges config, saves, and POSTs to ${runtime.endpointUrl}/v1/configure.

teardownAgent deletes runtime row or sets status disabled; for Phase A, delete is fine unless product wants historical rows.

getRegistration should become async:

async getRegistration(orgId: string): Promise<AgentRuntime | null>

If callers require sync currently, update them. rg getRegistration first.

  • Step 7: Run lifecycle tests

Run:

cd api
npm test -- agent-lifecycle.spec.ts --runInBand

Expected: PASS.

  • Step 8: Commit chunk
git add api/src/common/entities.ts api/migrations/1748200000001-CreateAgentRuntimes.ts api/src/agent-api/agent-api.module.ts api/src/agent-api/agent-lifecycle.service.ts api/test/agent-lifecycle.spec.ts
git commit -m "feat(agent): persist agent runtime registrations"

Chunk 2: Auto-provision on organization creation​

Task 3: Wire org creation to runtime provisioning​

Files:

  • Modify: api/src/organizations/organizations.service.ts

  • Modify: api/src/organizations/organizations.module.ts

  • Test: api/test/org-agent-provisioning.spec.ts

  • Step 1: Write failing service test

Create api/test/org-agent-provisioning.spec.ts that constructs OrganizationsService with a mock AgentLifecycleService and asserts both create paths provision:

expect(agentLifecycle.provisionAgent).toHaveBeenCalledWith(saved.id);

Cover:

  • createAmOrg(...)

  • createOrganization(...)

  • Step 2: Run test to verify failure

Run:

cd api
npm test -- org-agent-provisioning.spec.ts --runInBand

Expected: FAIL because constructor has no lifecycle dependency/calls.

  • Step 3: Import AgentApiModule in OrganizationsModule

Modify api/src/organizations/organizations.module.ts:

import { AgentApiModule } from "../agent-api/agent-api.module";
...
imports: [..., AgentApiModule]

Watch for circular dependency. If Nest reports a cycle, create AgentRuntimeModule containing only AgentLifecycleService, AgentRuntime, JwtModule, and exports it; then import that in both AgentApiModule and OrganizationsModule.

  • Step 4: Inject lifecycle service optionally

In OrganizationsService constructor add at the end:

@Optional() private readonly agentLifecycleService?: AgentLifecycleService,

Import Optional and AgentLifecycleService.

Optional keeps direct-instantiated unit tests from exploding; update direct constructor tests only if TypeScript compile forces an extra arg.

  • Step 5: Add helper and call it after saves

Add private method:

private async provisionRuntimeForOrg(orgId: string): Promise<void> {
if (!this.agentLifecycleService) {
this.logger.warn(`AgentLifecycleService unavailable; skipped runtime provisioning for org=${orgId}`);
return;
}
await this.agentLifecycleService.provisionAgent(orgId);
}

Call after orgRepo.save succeeds in:

  • createOrganization(...)
  • createAmOrg(...)

Important: call after related membership save in createOrganization or immediately after org save? Use after org save; if membership later fails the org already exists anyway. If this becomes transactional later, include runtime provisioning in transaction or use outbox/reconciler.

  • Step 6: Run org provisioning tests

Run:

cd api
npm test -- org-agent-provisioning.spec.ts --runInBand

Expected: PASS.

  • Step 7: Run affected existing org tests

Run:

cd api
npm test -- slug-lifecycle.spec.ts onboarding-ac.spec.ts seed-default-specialists.spec.ts specialists-export.spec.ts specialists-upsert.spec.ts --runInBand

Expected: PASS. If direct constructor tests fail, pass undefined as the final constructor arg or provide a mock lifecycle service.

  • Step 8: Commit chunk
git add api/src/organizations/organizations.service.ts api/src/organizations/organizations.module.ts api/test/org-agent-provisioning.spec.ts api/test/*.spec.ts
git commit -m "feat(orgs): auto-provision agent runtime on org create"

Chunk 3: Scoped AgentClient.forOrg(orgId)​

Task 4: Add scoped client support​

Files:

  • Modify: api/src/common/agent.client.ts

  • Modify: api/src/conversations/conversations.module.ts

  • Test: api/test/agent-client.spec.ts

  • Step 1: Write failing forOrg tests

Add to api/test/agent-client.spec.ts:

it("forOrg uses agent_runtimes.endpoint_url", async () => {
await runtimeRepo.save(runtimeRepo.create({ orgId, endpointUrl: 'https://org-agent.test', ... }));
const scoped = await client.forOrg(orgId);
await scoped.chat(baseReq);
expect(fetch).toHaveBeenCalledWith('https://org-agent.test/v1/chat', expect.anything());
});

it("forOrg falls back to global endpoint when no runtime row exists in Phase A", async () => { ... });
  • Step 2: Run test to verify failure

Run:

cd api
npm test -- agent-client.spec.ts --runInBand

Expected: FAIL because forOrg does not exist.

  • Step 3: Register AgentRuntime in ConversationsModule

Modify api/src/conversations/conversations.module.ts:

import { AgentRuntime, ... } from '../common/entities';
TypeOrmModule.forFeature([..., AgentRuntime])
  • Step 4: Refactor AgentClient constructor for scoped base URL

Make baseUrl not readonly from config only. One clean version:

constructor(
private readonly config: ConfigService,
@Optional() @InjectRepository(AgentRuntime)
private readonly runtimeRepo?: Repository<AgentRuntime>,
baseUrlOverride?: string,
) {
this.baseUrl = baseUrlOverride ?? this.resolveGlobalBaseUrl();
...
}

But Nest cannot inject baseUrlOverride; it will only be used manually inside forOrg:

private createScoped(baseUrl: string): AgentClient {
return new AgentClient(this.config, this.runtimeRepo, baseUrl);
}

If decorators complicate manual construction, create an internal lightweight class or clone method. Do not mutate singleton state.

  • Step 5: Implement forOrg
async forOrg(orgId: string): Promise<AgentClient> {
if (!orgId) return this;
if (!this.runtimeRepo) {
this.logger.warn(`AgentRuntime repository unavailable; using global agent endpoint for org ${orgId}`);
return this;
}
const runtime = await this.runtimeRepo.findOne({ where: { orgId, status: 'ready' } });
if (!runtime?.endpointUrl) {
this.logger.warn(`No ready agent runtime for org ${orgId}; using global agent endpoint`);
return this;
}
return this.createScoped(runtime.endpointUrl);
}

Phase A fallback is allowed. Add a TODO/comment saying production cutover should fail closed after all org rows exist.

  • Step 6: Run agent client tests

Run:

cd api
npm test -- agent-client.spec.ts --runInBand

Expected: PASS.

  • Step 7: Commit chunk
git add api/src/common/agent.client.ts api/src/conversations/conversations.module.ts api/test/agent-client.spec.ts
git commit -m "feat(agent): resolve agent client by org runtime"

Chunk 4: Wire dispatch callsites​

Task 5: Route ConversationsService through forOrg​

Files:

  • Modify: api/src/conversations/conversations.service.ts

  • Test: api/test/conversations.spec.ts

  • Possibly update many tests with mocked forOrg.

  • Step 1: Update mock shape in tests first

Most tests mock AgentClient as { chat: jest.fn(), chatStream: jest.fn() }. Add:

forOrg: jest.fn().mockResolvedValue(mockAgentClient)

Do this in affected tests where ConversationsService calls agent client.

  • Step 2: Write/adjust expectation

In api/test/conversations.spec.ts, assert:

expect(mockAgentClient.forOrg).toHaveBeenCalledWith(orgId);
expect(mockAgentClient.chat).toHaveBeenCalledTimes(1);
  • Step 3: Run test to verify failure before service change

Run:

cd api
npm test -- conversations.spec.ts --runInBand

Expected: FAIL because forOrg is not called.

  • Step 4: Update ConversationsService non-streaming call

At the this.agentClient.chat({ ... }) call around processMessage, change to:

const orgAgentClient = conv.orgId
? await this.agentClient.forOrg(conv.orgId)
: this.agentClient;
const agentResp = await orgAgentClient.chat({ ... });

Use the actual local variable for org/conversation at that callsite.

  • Step 5: Update streaming call

Around this.agentClient.chatStream({ ... }), resolve scoped client first:

const orgAgentClient = conversation.orgId
? await this.agentClient.forOrg(conversation.orgId)
: this.agentClient;
agentStream = await orgAgentClient.chatStream({ ... });
  • Step 6: Run conversation tests

Run:

cd api
npm test -- conversations.spec.ts conversations-extended.spec.ts auto-reply-mode.spec.ts expert-queue-respond.spec.ts --runInBand

Expected: PASS after mock updates.

Task 6: Route ExpertAgentThreadService through forOrg​

Files:

  • Modify: api/src/conversations/expert-agent-thread.service.ts

  • Test: api/test/expert-agent-thread.service.spec.ts

  • Test: api/test/expert-agent-thread.stream.spec.ts

  • Step 1: Update expert-thread mocks

Mock client should be self-returning:

const agentClient = {
forOrg: jest.fn().mockResolvedValue(undefined as any),
chat: jest.fn(),
chatStream: jest.fn(),
};
agentClient.forOrg.mockResolvedValue(agentClient);
  • Step 2: Add expectations

Non-stream:

expect(agentClient.forOrg).toHaveBeenCalledWith(conversation.orgId);
expect(agentClient.chat).toHaveBeenCalledWith(expect.objectContaining({ orgId: conversation.orgId }));

Stream:

expect(agentClient.forOrg).toHaveBeenCalledWith(conversation.orgId);
expect(agentClient.chatStream).toHaveBeenCalledWith(expect.objectContaining({ orgId: conversation.orgId }));
  • Step 3: Run tests to verify failure

Run:

cd api
npm test -- expert-agent-thread.service.spec.ts expert-agent-thread.stream.spec.ts --runInBand

Expected: FAIL before code change.

  • Step 4: Update non-streaming expert post

In post(...) before chat:

const orgAgentClient = await this.agentClient.forOrg(conversation.orgId);
agentResponse = await orgAgentClient.chat(...);
  • Step 5: Update streaming expert post

In postStream(...) before chatStream:

const orgAgentClient = await this.agentClient.forOrg(conversation.orgId);
agentStream = await orgAgentClient.chatStream(request);
  • Step 6: Run expert-thread tests

Run:

cd api
npm test -- expert-agent-thread.service.spec.ts expert-agent-thread.stream.spec.ts --runInBand

Expected: PASS.

  • Step 7: Commit chunk
git add api/src/conversations/conversations.service.ts api/src/conversations/expert-agent-thread.service.ts api/test/*.spec.ts
git commit -m "feat(conversations): dispatch agent calls through org runtime"

Chunk 5: Backfill and verification​

Task 7: Add backfill script for existing organizations​

Files:

  • Create: api/scripts/backfill-agent-runtimes.ts

  • Modify: api/scripts/README.md

  • Maybe modify: api/package.json scripts if the repo uses script aliases.

  • Step 1: Implement script

Follow existing api/scripts/*.ts patterns. Script should:

  1. Bootstrap TypeORM data source the same way existing scripts do.
  2. Query all Organization rows.
  3. Query AgentRuntime rows.
  4. For missing org IDs, create rows with shared endpoint and minted token. Either instantiate AgentLifecycleService through Nest app context or duplicate minimal provisioning logic carefully. Prefer Nest app context if not too heavy.
  5. Print summary:
agent_runtimes backfill: scanned=12 created=12 skipped=0
  • Step 2: Document command

In api/scripts/README.md add:

npx ts-node scripts/backfill-agent-runtimes.ts

or whatever existing script runner uses.

  • Step 3: Run lint/typecheck for script

Run:

cd api
npm run build

Expected: PASS.

  • Step 4: Commit chunk
git add api/scripts/backfill-agent-runtimes.ts api/scripts/README.md api/package.json
git commit -m "chore(agent): add agent runtime backfill script"

Task 8: Full verification​

Files:

  • No code changes unless failures surface.

  • Step 1: Run targeted API tests

Run:

cd api
npm test -- agent-lifecycle.spec.ts agent-client.spec.ts org-agent-provisioning.spec.ts conversations.spec.ts conversations-extended.spec.ts expert-agent-thread.service.spec.ts expert-agent-thread.stream.spec.ts --runInBand

Expected: PASS.

  • Step 2: Run broader API tests likely affected by AgentClient mocks

Run:

cd api
npm test -- auto-reply-mode.spec.ts expert-queue-respond.spec.ts queue-sort-by-activity.spec.ts queue-status-filter.spec.ts queue-customer-label.spec.ts assignable-experts.spec.ts --runInBand

Expected: PASS.

  • Step 3: Build API

Run:

cd api
npm run build

Expected: PASS.

  • Step 4: Record no-bs evidence if gates are active

Run from repo root:

python3 ~/.codex/no-bs/bin/no_bs.py record-verification 'api targeted tests + npm run build passed for per-org agent runtime routing'
python3 ~/.codex/no-bs/bin/no_bs.py check

Expected: either pass, or if this Pi session lacks CODEX_THREAD_ID, note the tool error in final report without claiming no-bs completion.

  • Step 5: Final commit if verification fixes were needed
git add <changed-files>
git commit -m "test(agent): verify per-org runtime routing"

Implementation Notes / Decisions​

  • Phase A fallback is intentional: existing orgs can still use the global/shared agent while backfill runs. Emit warnings on fallback so we can detect missed rows.
  • Do not introduce Drizzle/Prisma/Kysely for this one table. This codebase is TypeORM-shaped; adding a second DB stack here is gratuitous complexity.
  • Do not make AgentClient.forOrg mutate singleton state. That is a race across concurrent requests.
  • Do not wire Splinter worktree/runtime code into this path. Splinter informs the design, but Humanwork org runtimes are service endpoints, not local coding-agent worktrees.
  • Use correct product terminology: Specialist, Expert, org/client. Do not call Specialists β€œagents” in UI/client-facing strings.