Skip to main content

ADR-0001 · Persona Manager integration approach

Status: Accepted · Date: 2026-05-13 · Deciders: Builder agent (autonomous) · Supersedes: none

Context

A standalone Persona Manager POC at hwork-persona-manager/ is build-complete. It is its own FastAPI + Next.js stack with its own Postgres, its own Celery queue, its own audit log, and its own hwork_client outbound to the main Human.work application.

The integration task is to land its functionality inside the existing Human.work monorepo (api/ NestJS, agent/ FastAPI, frontend/ Next.js) without disrupting a production-grade codebase that is one operational task away from MVP launch (45/46 P0 items complete per MVP_LAUNCH_CHECKLIST.md).

The Builder operating prompt (prompts/persona-builder.md) sets a hard constraint: do not modify existing code unless a task explicitly tells you to.

Three high-level approaches were considered.

Options

A — "Lift and shift": import the POC repo as-is

Copy hwork-persona-manager/apps/api into the monorepo as a third backend app alongside api/ and agent/. Run it on its own port and DB connection. Bridge to hwork-app over HTTP using the POC's existing hwork_client.

  • Pros: preserves the POC verbatim. Zero re-implementation effort.
  • Cons: duplicates auth, multi-tenancy, audit, the Postgres connection, CI orchestration, deployment surface, secret management. Two systems drift over time. Cross-tenant isolation now spans two services and two DBs — testing it nightly becomes a meaningfully harder problem. Violates the spirit of "single source of truth for org data."

B — "Additive re-implementation": rebuild inside the NestJS app

Build new NestJS modules (api/src/environment-profiles/, api/src/meetings/, api/src/tavus/) and new Next.js routes that read from the existing entities, write to five new tables, and reuse all existing cross-cutting infrastructure (AuditService, OrgRlsInterceptor, NotificationsGateway, JwtAuthGuard, PlatformRolesGuard, OrgRolesGuard, ExpertQueueService).

  • Pros: single Postgres connection, single auth model, single audit trail, single multi-tenancy mechanism. The integration touch points are countable and reviewable. Tests run in the existing harness. Nightly cross-tenant isolation already covers the new tables when added to its list (PM-0005). No drift between two parallel systems.
  • Cons: re-implements ~70% of what the POC already wrote in Python + SQLAlchemy. The POC's Celery workflow becomes BullMQ. The POC's separate Postgres becomes a schema namespace inside the main one. Some POC abstractions don't translate (e.g. work_log_reference collapses to live service-layer reads).

C — "Hybrid extraction": vendor select POC pieces, ignore the rest

Pull only the Tavus client, prompt templates, and function-call definitions from the POC; ignore everything else and rebuild the rest. Use the POC's hwork_client reversed — call from POC outward into hwork-app — converted to a NestJS module.

  • Pros: salvages the genuinely reusable POC outputs (Tavus contract understanding, function-call schemas, prompt structure).
  • Cons: ambiguous boundary. Halfway between A and B without the benefits of either. Hard to draw the line during implementation.

Decision

Option B. Additive re-implementation inside the NestJS app.

The five new tables (environment_profiles, meetings, meeting_decisions, meeting_priorities, tavus_function_definitions) stand alone and reference existing entities by FK. New code lives in new directories. Cross-cutting infrastructure is reused, not duplicated. Agent service gets new prompt files but no code changes (the existing load_prompt(role) already routes on filename).

The POC remains in hwork-persona-manager/ as reference material for entity mapping, function-call schemas, and Tavus contract details, but it is not imported, deployed, or run.

Consequences

Positive

  • One auth model, one audit log, one DB connection, one multi-tenancy mechanism, one notification gateway.
  • The integration touch points are explicit and small: additive functions in frontend/src/lib/api.ts, single-line module imports in api/src/app.module.ts, additive entity entries in .github/workflows/nightly-isolation.yml. Anything else in an existing file is a violation requiring a requires_existing_code_change event.
  • Reporting decisions ride the existing Action entity as a designed extension point (actorType=reporting_meeting). The existing audit log viewer surfaces them for free.
  • Reporting escalations go through ExpertQueueService.create(...). The expert workspace queue surface is reused unchanged.
  • The brief assembler reads live from Conversation/Message/Action/ ExpertQueueItem/Correction via service-layer queries — no parallel work-log store, no sync job.
  • Single Postgres + single OrgRlsInterceptor covers tenancy. Each new table gets an RLS policy in its migration matching migrations 9, 12, 17, 20, 27, 33.

Negative / accepted

  • Re-implementation effort. The POC's Python services become TypeScript services. The mitigation is that the POC's domain model carries over cleanly to TypeORM entities — most of the translation is mechanical.
  • The POC's Celery + Redis async pattern is replaced by BullMQ (already in package.json and minimally used). Only the persona archival job (PM-0066) actually needs background scheduling; the rest of the workflow is request-response.
  • Versioning of role templates (POC's role_template_version entity) is deferred. See ADR-0003 if/when an actual need arises.

Neutral

  • The agent/prompts/ directory becomes a shared prompt registry read by both the agent service (load_prompt(role)) and the NestJS persona-generator.service.ts (file read + parameter substitution before passing to tavusClient.createPersona(prompt)). This is fine because the agent's loader keys on role and the NestJS reader simply renders the same template into a different consumer. No code change to the loader.

Permitted touches to existing files

These are sanctioned by the Builder prompt and listed exhaustively here so future cycles can reference them without re-deriving:

FileAllowed changeWhy
frontend/src/lib/api.tsAppend new exported functions only. No signature changes, no reordering, no deletion.Single API client convention in the frontend.
api/src/app.module.tsAdd imports: [..., NewModule] single-line entries.NestJS module registration.
.github/workflows/nightly-isolation.ymlAppend new entity names to the test list.Cross-tenant isolation is existential; new tables must be covered.

Every other existing file is read-only. Any task that appears to require otherwise must emit a requires_existing_code_change event.

Verification

  • PM-0001 confirmed all referenced entities and patterns exist where the Builder prompt indicated.
  • The HMAC mirror pattern (PM-0012) reproduces channels.controller.ts:47-62 without import.
  • The Action extension-point write (PM-0044) is exercised by the Phase 6 Playwright E2E test (PM-0061) which asserts the row appears in the existing audit log viewer.

References

  • prompts/persona-builder.md — Builder operating prompt (canonical)
  • docs/specs/persona-manager-integration.md — system-level integration spec
  • hwork-persona-manager/docs/architecture.md — POC reference
  • api/migrations/ 9, 12 (MemoryRLS), 13, 17, 27, 33 — RLS patterns (PM-0067 fix)
  • api/src/channels/channels.controller.ts:23-62 — HMAC pattern
  • api/src/common/entities.tsAction extension-point entity