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.
Stop minting a new subaccount per seed. Point the org at one shared Twilio account you've set up once.
Pull that account's IncomingPhoneNumbers into Loquent so the org owns real, callable numbers.
Clone the 4 base AI agents into the new user/org — the piece the seed silently skips today.
Drop the org on the Enterprise tier (DB-driven) so every paid feature is unlocked, on the Stripe sandbox.
A non-owner teammate + presence row, so transfers, assignment, and availability are testable.
10 contacts; one of them rich with an SMS thread, tasks, notes, and a call.
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:
| Step | Seed today | Signup (real) | This design |
|---|---|---|---|
| User · account · org · owner member | ✓ | ✓ | ✓ (kept) |
| Default tags · notification prefs | ✓ | ✓ | ✓ (kept) |
| Twilio | new subaccount each run | subaccount | shared account, reused |
| Phone numbers | ✗ none | ✗ none | imported from Twilio |
| AI agent clones | ✗ MISSING | ✓ | ✓ added |
| Subscription / plan | ✗ none | Free | Enterprise (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, orON CONFLICT DO NOTHING. Re-running the seed is safe. - Config-driven, zero shell changes.
worktree.shalready copiesseed.envinto each new worktree andworktree-db.shalready runsjust seed. Putting the shared Twilio creds + flags in the rootseed.envpropagates them to every worktree automatically. - Refactor for legibility.
seed.rsroughly doubles in size, so it gets split into namedasync fnsections (one per block below) rather than one longmain.
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.envvars: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_settingsrow 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.jsonwith the shared creds (reqwest already a dependency).- Insert one
phone_numberrow per number —number,twilio_sid,friendly_name, capability flags (ai_agent_sms_enabled/ai_agent_call_enabled),routing_mode,voice_webhook_active— guarded byON 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.
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 agent | Role | Cloned per user? |
|---|---|---|
| Master Orchestrator | Entry-point; delegates to other agents | ✓ |
| Loquent Assistant | User-facing chat assistant (domain tools attach at turn time) | ✓ |
| Follow-up Drafter | Behind-the-scenes bulk-draft worker | ✓ |
| Text Reply | Inbound-SMS agent (bound to a number in step 2) | ✓ |
| Agent Creator | Platform singleton | ✗ never cloned |
For each source, the clone copies, with NOT EXISTS guards:
- the
ai_agentrow — overridingorganization_id,user_id,system_source_id, and settingis_unedited_clone=true; - an empty
ai_agent_memoryrow; - the
ai_agent_skilllinks (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-driven — check_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', highestsort_order=30, all feature flags on),status='active', periodnow()..+1yr,stripe_subscription_id=NULL.org_budgetviaINSERT…SELECTfrom the Enterprise tier, mirroringreset_cycle_budget:included_credits = credits_per_seat × seats, every*_includedNULL→0,initial_included_credits= gross allowance.
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.
Create a real Stripe sandbox subscription object too?
- DB-only max plan + sandbox keys configured. Everything unlocks; billing page renders. No sandbox clutter.
- 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.
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-credentialsalongside the owner's. Idempotent by email. is_owner=falsewith 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.
- 9 bare contacts — names from the existing
seed_previewpools; a few get default tags applied so list/filter UIs show real state. - 1 rich contact gets all four: an SMS thread (
messagerows tied to an importedphone_number), twotaskrows (one open + one completed, with priority/due), twocontact_noterows (manual + AI-summaryarea), and onecall(status='completed',transcription+recording_urlstub,duration_secs,contact_idlinked).
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.
Subscription + budget
Enterprise organization_subscription + org_budget — unlocks features before anything reads tier flags.
Twilio settings
Insert organization_twilio_settings referencing the shared account.
Import numbers
Fetch IncomingPhoneNumbers → phone_number rows.
Clone AI agents
The 4 default sources → ai_agent (+memory, skills, edges).
Bind text agent → number
Set phone_number.ai_agent_id on the first number.
Second member + presence
user+account+member+staff_presence.
10 contacts (+ tags)
9 bare + 1 rich, tags applied to a few.
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
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
Bind the Text Reply agent to the first imported number?
- Yes — inbound SMS routes to the agent out of the box.
- No — leave numbers unbound; configure per test.
DB-only MAX plan (skip the live Stripe sandbox object)?
- Yes — DB rows unlock everything; sandbox keys wired for the billing UI.
- No — also create a real sandbox Customer + Subscription.
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.