Loquent · Worktree seed Design · Mode B · 2026-06-13
Dev tooling · Seed

A worktree that's ready to use the moment it boots.

Today just seed spins up a bare org, mints a throwaway Twilio subaccount, and stops. This design makes the seed produce a complete, log-in-ready account: a shared Twilio account with its real numbers, cloned AI agents, the MAX plan unlocked, a second teammate, and a small but realistic data set — all idempotent, all driven by seed.env.

Where: migration/src/bin/seed.rs Run by: worktree-db.sh → just seed Approach: raw SQL, idempotent Shell changes: none

00Overview & goal

One sentence: extend the existing dev seed so that every fresh worktree DB lands as a fully usable account — not an empty shell you have to hand-configure before you can test anything.

SHARED TWILIO

Stop minting a new subaccount per seed. Point the org at one shared Twilio account you've set up once.

REAL NUMBERS

Pull that account's IncomingPhoneNumbers into Loquent so the org owns real, callable numbers.

AGENT CLONES

Clone the 4 base AI agents into the new user/org — the piece the seed silently skips today.

MAX PLAN

Drop the org on the Enterprise tier (DB-driven) so every paid feature is unlocked, on the Stripe sandbox.

SECOND STAFF

A non-owner teammate + presence row, so transfers, assignment, and availability are testable.

SAMPLE DATA

10 contacts; one of them rich with an SMS thread, tasks, notes, and a call.

i

Why this matters

Every worktree currently costs minutes of manual setup (connect Twilio, find numbers, realize the assistant is broken, create test data) before real work starts. This collapses that to zero — the seed leaves a worktree indistinguishable from a real, mid-use account.

01What a seed gives you today

The current seed binary creates the bones and nothing else. Critically, two things are quietly missing or wrong:

StepSeed todaySignup (real)This design
User · account · org · owner member✓ (kept)
Default tags · notification prefs✓ (kept)
Twilionew subaccount each runsubaccountshared account, reused
Phone numbers✗ none✗ noneimported from Twilio
AI agent clones✗ MISSING✓ added
Subscription / plan✗ noneFreeEnterprise (MAX)
Second staff member
Sample contacts / messages / tasks / calls✓ 10 + 1 rich
!

The silent breakage

System agents exist after just migrate, but their per-user clones are only created during signup via provision_default_agents_for_user_best_effort(). The seed never calls it — so a seeded account opens the assistant and agents views to a broken/empty experience. Fixing this is the highest-value part of the change.

02Architecture & constraints

The migration crate is standalone — it does not depend on the main loquent crate, so the seed binaries can only use raw SQL + reqwest. They cannot call services like clone_system_agent_for_user or create_message.

Chosen: raw SQL in migration crate

Fast compile, matches the existing seed_preview pattern, no cfg-gating. Cost: agent-clone SQL must mirror the real clone logic, so it carries a documented drift risk.

Not chosen: bin in main crate

Would reuse real services (zero drift) but compiles the whole server and needs cfg-gating. Heavier than this dev tool warrants.

Two properties hold across every block:

  • Idempotent. Each block guards with email-exists checks, NOT EXISTS, or ON CONFLICT DO NOTHING. Re-running the seed is safe.
  • Config-driven, zero shell changes. worktree.sh already copies seed.env into each new worktree and worktree-db.sh already runs just seed. Putting the shared Twilio creds + flags in the root seed.env propagates them to every worktree automatically.
  • Refactor for legibility. seed.rs roughly doubles in size, so it gets split into named async fn sections (one per block below) rather than one long main.

031 · Shared Twilio account replace

Today the seed POSTs a brand-new Twilio subaccount on every run — dozens of throwaway subaccounts over a worktree's life. Instead, reference one shared account you configured once (with its numbers and an event sink already pointed at your constant ngrok).

Behavior

  • New seed.env vars: TWILIO_WORKTREE_SUBACCOUNT_SID / _TOKEN (plus optional _EVENT_SINK_SID, _SMS_SUBSCRIPTION_SID, _CALL_SUBSCRIPTION_SID, _TWIML_APP_SID, _API_KEY_SID, _API_KEY_SECRET).
  • When set → insert one organization_twilio_settings row pointing at the shared account: is_byo=false, the event-sink SIDs, sms_events_active=true, call_events_active=true, connected_at=now(). No create-subaccount API call.
  • When absent → fall back to the existing behavior (so nothing breaks for non-worktree use).

Why no webhook reconfiguration is needed

Your ngrok is constant and the shared account's event sink + per-number webhooks already point at it. The seed only writes DB rows — live inbound "just works" against whichever worktree the ngrok currently forwards to. No per-seed Twilio mutation.

042 · Import phone numbers new

With the shared account connected, pull its real numbers into Loquent so the org owns callable numbers from the first boot.

  • GET /2010-04-01/Accounts/{SID}/IncomingPhoneNumbers.json with the shared creds (reqwest already a dependency).
  • Insert one phone_number row per number — number, twilio_sid, friendly_name, capability flags (ai_agent_sms_enabled / ai_agent_call_enabled), routing_mode, voice_webhook_active — guarded by ON CONFLICT (number) DO NOTHING.
  • Bind the cloned Text Reply agent to the first imported number (phone_number.ai_agent_id) so inbound SMS routes to the agent out of the box.
i

No dedicated "import numbers from Twilio" service exists in the app yet — the seed does this directly. If a real import flow ships later, the seed's inline version can defer to it.

053 · Clone the base AI agents new · high value

The fix for the silent breakage. Mirror clone_system_agent_for_user in idempotent SQL for the four DEFAULT_USER_AGENT_SOURCES.

System agentRoleCloned per user?
Master OrchestratorEntry-point; delegates to other agents
Loquent AssistantUser-facing chat assistant (domain tools attach at turn time)
Follow-up DrafterBehind-the-scenes bulk-draft worker
Text ReplyInbound-SMS agent (bound to a number in step 2)
Agent CreatorPlatform singleton✗ never cloned

For each source, the clone copies, with NOT EXISTS guards:

  • the ai_agent row — overriding organization_id, user_id, system_source_id, and setting is_unedited_clone=true;
  • an empty ai_agent_memory row;
  • the ai_agent_skill links (preserving enabled state);
  • the can-invoke edges (the permission allowlist).
!

Documented drift risk

Because this is raw SQL, it must track changes to clone_system_agent_for_user. A header comment in seed.rs and a /note will flag the coupling so a future change to the real clone logic is mirrored here.

064 · MAX plan + Stripe sandbox new

Plan gating is 100% DB-drivencheck_tier_feature_service reads the organization_subscription → subscription_tier join, never Stripe. So the MAX plan is just two rows.

  • organization_subscription → the Enterprise tier (slug='enterprise', highest sort_order=30, all feature flags on), status='active', period now()..+1yr, stripe_subscription_id=NULL.
  • org_budget via INSERT…SELECT from the Enterprise tier, mirroring reset_cycle_budget: included_credits = credits_per_seat × seats, every *_included NULL→0, initial_included_credits = gross allowance.
subscription_tier (enterprise) organization_subscription credits_per_seat ───────┐ ├─ subscription_tier_id ── enterprise *_included ──────────┼──copy────► ├─ status = 'active' feature flags ──────────┘ └─ period now()..+1yr │ └── INSERT…SELECT ──► org_budget (included_credits, *_included, initial_*) │ check_tier_feature_service ◄────┘ ⇒ every paid feature unlocked

Stripe sandbox

Ensure core_conf.stripe_secret_key / stripe_webhook_secret hold the sandbox keys (already read from STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET at migrate-time — added to seed.env.example), and set tiers' stripe_product_id if provided. The billing UI then renders and talks to the sandbox.

DECISION · STRIPE OBJECT

Create a real Stripe sandbox subscription object too?

  • Also create a live sandbox Customer + Subscription (best-effort, gated on sandbox key + an Enterprise price existing). More realistic billing page; more API calls + clutter per seed.
Resolved · DB-only. Sandbox keys wired so the billing UI renders against Stripe sandbox; no live Customer/Subscription object created per seed.

075 · Second staff member new

One non-owner teammate makes presence, transfers, and assignment testable.

  • user + email_password_account (staff@… / password) + member(is_owner=false) + staff_presence(status='available').
  • Credentials appended to .local-credentials alongside the owner's. Idempotent by email.
  • is_owner=false with no role row = base staff permissions, which is the realistic default for a teammate.

086 · Sample data new

Small and realistic — the opposite of seed_preview's 5,000×500 stress set. Ten contacts; one of them fully populated.

10
contacts (9 bare + 1 rich)
1
SMS thread (in + out)
2
tasks (open + done)
2
notes (manual + AI)
1
call (with transcript stub)
  • 9 bare contacts — names from the existing seed_preview pools; a few get default tags applied so list/filter UIs show real state.
  • 1 rich contact gets all four: an SMS thread (message rows tied to an imported phone_number), two task rows (one open + one completed, with priority/due), two contact_note rows (manual + AI-summary area), and one call (status='completed', transcription + recording_url stub, duration_secs, contact_id linked).

tables touched · contact · contact_phone · contact_tag(link) · message · task · contact_note · call

09Build order & data flow

Ordered so every FK target exists before it's referenced. Existing bootstrap (user · account · org · owner · tags · prefs) runs first, unchanged.

1

Subscription + budget

Enterprise organization_subscription + org_budget — unlocks features before anything reads tier flags.

2

Twilio settings

Insert organization_twilio_settings referencing the shared account.

3

Import numbers

Fetch IncomingPhoneNumbersphone_number rows.

4

Clone AI agents

The 4 default sources → ai_agent (+memory, skills, edges).

5

Bind text agent → number

Set phone_number.ai_agent_id on the first number.

6

Second member + presence

user+account+member+staff_presence.

7

10 contacts (+ tags)

9 bare + 1 rich, tags applied to a few.

8

Rich-contact children

messages · tasks · notes · call — depend on the contact + an imported number.

10seed.env additions

New keys added to seed.env.example. Filling them in the root seed.env propagates to every future worktree.

# Shared Twilio account reused across all worktrees (replaces per-seed subaccount)
TWILIO_WORKTREE_SUBACCOUNT_SID=
TWILIO_WORKTREE_SUBACCOUNT_TOKEN=
# Optional — its already-wired event sink (live inbound via your constant ngrok)
TWILIO_WORKTREE_EVENT_SINK_SID=
TWILIO_WORKTREE_SMS_SUBSCRIPTION_SID=
TWILIO_WORKTREE_CALL_SUBSCRIPTION_SID=
TWILIO_WORKTREE_TWIML_APP_SID=
TWILIO_WORKTREE_API_KEY_SID=
TWILIO_WORKTREE_API_KEY_SECRET=

# Stripe sandbox (read at migrate-time into core_conf)
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

# Sample-data toggles (sensible defaults; rarely changed)
SEED_SAMPLE_DATA=1          # 1 = create the 10 contacts + rich contact
SEED_SECOND_STAFF_EMAIL=staff@example.com
i

All new behavior is opt-in by presence of config with safe defaults — a seed.env without the Twilio worktree vars behaves like the old seed (minus the always-on agent clones, max plan, and sample data, which need no external creds).

11Open decisions & scope

CONFIRM A resolved

Bind the Text Reply agent to the first imported number?

  • No — leave numbers unbound; configure per test.
Resolved · Yes. The Text Reply clone is bound to the first imported number.
CONFIRM B resolved

DB-only MAX plan (skip the live Stripe sandbox object)?

  • No — also create a real sandbox Customer + Subscription.
Resolved · DB-only. Enterprise subscription + budget rows; sandbox keys configured; no live Stripe object per seed.

Out of scope YAGNI

  • A real Stripe subscription object per seed (unless Confirm B flips).
  • Per-worktree Twilio webhook reconfiguration — unneeded with a constant ngrok.
  • High-volume data — that remains seed_preview's job.
  • Moving the seed into the main crate — explicitly not chosen.