Per-Org Agent Runtime Routing Implementation Plan
SUPERSEDED β 2026-08-05, ADR-046. The
agent_runtimesper-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 seedocs/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_runtimeswith uniqueorg_id, endpoint/status/config/token fields, timestamps, and indexes.
- Creates
- Modify
api/src/common/entities.ts- Add
AgentRuntimeentity andAgentRuntimeStatustype.
- Add
- Modify
api/src/agent-api/agent-lifecycle.service.ts- Inject
Repository<AgentRuntime>. - Replace in-memory
Mapwith DB upsert/read/update/delete. - Resolve transitional endpoint from
AGENT_SERVICE_URL, fallbackAGENT_URL, fallbackhttps://agent-dev-5b3e.up.railway.appin non-prod/dev only. - Log a warning when
AGENT_URLfallback is used.
- Inject
- Modify
api/src/agent-api/agent-api.module.ts- Register
AgentRuntimeinTypeOrmModule.forFeature.
- Register
- Modify
api/src/organizations/organizations.service.ts- Inject optional
AgentLifecycleService. - Call
provisionAgent(saved.id)after successful org creation in bothcreateOrganizationandcreateAmOrg. - 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.
- Inject optional
- Modify
api/src/organizations/organizations.module.ts- Import
AgentApiModule, or preferably a small runtime module if created. For this plan, importAgentApiModuleto minimize scope.
- Import
- 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.
- Inject optional
- Modify
api/src/conversations/conversations.module.ts- Register
AgentRuntimeinTypeOrmModule.forFeaturesoAgentClientcan inject its repo.
- Register
- Modify
api/src/conversations/conversations.service.ts- Replace
this.agentClient.chat(...)andchatStream(...)withawait this.agentClient.forOrg(orgId).
- Replace
- Modify
api/src/conversations/expert-agent-thread.service.ts- Replace
this.agentClient.chat(...)andchatStream(...)withawait this.agentClient.forOrg(conversation.orgId).
- Replace
- Create
api/scripts/backfill-agent-runtimes.tsor 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.
- For every organization without a runtime row, call
- Modify tests:
api/test/agent-lifecycle.service.spec.tsor newapi/test/agent-lifecycle.spec.tsapi/test/agent-client.spec.tsapi/test/conversations.spec.tsapi/test/expert-agent-thread.service.spec.tsapi/test/expert-agent-thread.stream.spec.ts- Add org-create provisioning test, likely new
api/test/org-agent-provisioning.spec.tsto 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
AgentApiModuleinOrganizationsModule
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
forOrgtests
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
AgentRuntimeinConversationsModule
Modify api/src/conversations/conversations.module.ts:
import { AgentRuntime, ... } from '../common/entities';
TypeOrmModule.forFeature([..., AgentRuntime])
- Step 4: Refactor
AgentClientconstructor 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
ConversationsServicenon-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.jsonscripts if the repo uses script aliases. -
Step 1: Implement script
Follow existing api/scripts/*.ts patterns. Script should:
- Bootstrap TypeORM data source the same way existing scripts do.
- Query all
Organizationrows. - Query
AgentRuntimerows. - For missing org IDs, create rows with shared endpoint and minted token. Either instantiate
AgentLifecycleServicethrough Nest app context or duplicate minimal provisioning logic carefully. Prefer Nest app context if not too heavy. - 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.forOrgmutate 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.