tenwhy · the build plan

Two implementations, one architecture

We build tenwhy twice, in parallel. Implementation 1 wires every vendor by hand. Implementation 2 provisions the same vendors through the Stripe Projects ecosystem. The application code is identical between them — only the provisioning, credentials, and billing plane differs. Running both at once is the truest test that the architecture is portable and not accidentally welded to one set of vendors.

1 · The principle — the .env is the seam

Maestro, the specialists, the job contract, the brain client — none of them know which implementation they're running in. They read DATABASE_URL, OPENROUTER_API_KEY, SANDBOX_API_KEY, etc. from the environment. The only difference between Impl 1 and Impl 2 is how those values land in .env.

StepImpl 1 · hand-wiredImpl 2 · Stripe Projects
get a Postgres create it in the Neon/Render dashboard, copy DATABASE_URL into .env stripe projects add neon/postgresstripe projects env --pull
get an LLM key paste OpenRouter key into .env stripe projects add openrouterenv --pull
get a sandbox Sapiom/Blaxel API key into .env stripe projects add daytonaenv --pull

The app reads the same variable names either way. That makes the two implementations a controlled A/B: same code, two provenances.

2 · The proving loop (MVP)

The smallest thing that exercises the whole architecture: one task, one orchestrator, one specialist, one structured round trip, one durable audit. If this loop is green on both implementations, scaling to the full workforce is mechanical.

  1. A task arrives Customer types a request into a form (or curl). For the MVP there's one hard-coded test customer. web form / CLI → Maestro
  2. Maestro reasons Maestro (LLM) reads the relevant brain docs, composes a brief, and decides to fire Jon (catalog). Maestro · OpenRouter · reads brain via GitHub API
  3. Jon runs Jon receives the job-contract request, reasons over the brief + brain snapshot (LLM), and emits a structured proposal — no external API in the MVP, just catalog reasoning. specialist · OpenRouter · job contract v0 in
  4. Jon returns the contract Jon emits job-contract response JSON: status, actions[], brain_updates[], cost_usd, etc. µVM → Maestro · HMAC-signed
  5. Maestro persists Maestro validates the contract, writes a row to action_log, and appends an entry to /jon/history.html in the brain repo. Postgres + GitHub commit
  6. Customer sees the result A plain HTML page renders the brain + the action log. (Generative dashboard comes later.) static HTML

What the MVP deliberately cuts

CutWhy it's safe to defer
The software factoryHand-write Jon. Proving the workforce loop matters first; the factory automates tool-building later.
11 other specialistsOne specialist proves the contract. The rest are the same shape.
Multi-tenancyOne hard-coded test customer + one seed brain.
Generative dashboardPlain HTML renders brain + log. json-render comes once the loop is real.
Scheduler / shiftsJon fires on demand only.
Real authSingle hard-coded session for the MVP.
ScrapingJon (catalog) doesn't scrape. The scraping service arrives with Max (SEO).

kept in MVP  Maestro does use an LLM from the start — it reads the brain, composes the brief, and decides. Even with one specialist, Maestro is a real orchestrator, not a dumb router.

What the MVP must prove — the failure paths

The happy-path round trip is the easy half. The MVP isn't green until it survives the failures the durable layer exists for (design §11) — these are test cases, not afterthoughts:

Failure injectedRequired behaviour
Job retriedRe-running re-applies nothing already landed — idempotency-key check against the action_items ledger.
Duplicate webhookA re-delivered result is a no-op — inbox dedupe on unique(job_id, event, external_id).
Lost webhookReconciliation reads external truth back and closes the job; no effect lost or double-counted.
Crash before external applyJob resumes from applying; nothing executed, so it executes cleanly.
Crash after apply, before recordReconciliation finds the landed effect and finalizes the record — the hardest case, and the reason the ledger exists.
Projection rebuildA query-hot projection can be dropped and rebuilt from results with identical output.
Brain-commit validationA malformed proposed brain update is rejected at finalize; the canonical record is platform-rendered, not tool-written.

3 · Component map — Impl 1 vs Impl 2

Rows marked muted are identical across both. The colored rows are where the two implementations genuinely diverge.

LayerImpl 1 · hand-wiredImpl 2 · Stripe Projects
Maestro hostRender web service (manual)Render/Fly via projects add
Specialist sandboxSapiom / Blaxel µVMDaytona (<90ms, TS SDK)
DatabaseRender Postgres (manual)Neon via projects add
LLM gatewayOpenRouterOpenRouter (same vendor)
Brain repoGitHub (outside Projects)GitHub (outside Projects)
SecretsInfisicalStripe Secret Store (built in)
Queue / backgroundRender CronInngest via projects add
Object storeCloudflare R2Cloudflare R2 via projects add
Authcustom magic link + CF EmailWorkOS (AuthKit)
ScrapingScrapling service (self-hosted, Python + Playwright)Firecrawl via projects add
ObservabilitySentry + Better StackSentry (catalog) + Better Stack (manual)
DNS / CDNCloudflareCloudflare via projects add
Customer billingStripe API (wired by hand)native Stripe (the platform's reason to exist)

note  Scraping is a service behind an HTTP boundary in both — Scrapling (self-hosted) is the open-source equivalent of Firecrawl (hosted). The Node tool code that calls it is the same; only the base URL + auth change. This keeps the µVM Node-only (decision #13) intact — Scrapling's Python stays inside its own service, never inside a tool µVM.

note  The Queue / background row is the portability seam to watch. Render Cron (Impl 1) and Inngest (Impl 2) must deliver the same platform events to the same job-state-machine code (design §11), behind one scheduler/event interface — with a conformance suite both implementations pass. Otherwise the two impls quietly diverge exactly where it's hardest to see.

4 · The provisioning seam in practice

Same .env shape, two ways to fill it.

# Impl 1 — hand-wired: you paste each value
DATABASE_URL=postgres://...        # from Neon/Render dashboard
OPENROUTER_API_KEY=sk-or-...       # from openrouter.ai
SANDBOX_API_KEY=...                # from Sapiom/Blaxel
GITHUB_TOKEN=ghp_...               # brain repo PAT
SCRAPER_URL=https://scrape.internal # self-hosted Scrapling

# Impl 2 — Stripe Projects: provision, then pull
stripe projects add neon/postgres
stripe projects add openrouter
stripe projects add daytona
stripe projects add firecrawl
stripe projects env --pull           # writes the same .env keys
stripe projects billing update --limit 200 --provider daytona

The last line is a real win: per-provider spend caps from one command directly address the cost-runaway item on the design's "still on the grill" list.

5 · Repo layout — one repo, shared code

Single repository. The app is shared; only thin per-implementation provisioning configs differ.

tenwhy/
├── app/                      # shared — identical across impls
│   ├── maestro/              # orchestrator (LLM): reads brain, composes brief, fires specialists
│   ├── specialists/
│   │   └── jon/              # catalog specialist (MVP); later extracted to tenwhy-tools/
│   ├── contract/             # job-contract v0 types + validator
│   ├── brain/                # GitHub brain client (read docs, append history.html)
│   └── web/                  # MVP: plain HTML render of brain + action_log
├── infra/
│   ├── impl1/                # Render blueprint, Sapiom config, Scrapling service
│   └── impl2/                # .projects/ state, Stripe Projects manifest
├── seed/
│   └── brain/                # hand-authored sample brain for the test customer
└── AGENTS.md                 # orients agents to the doc set + this plan

6 · Build phases

Ordered, not timed. Each phase ships on both implementations before the next begins — that's what keeps them honest.

  1. The loop, in-process Maestro + Jon in one process, no sandbox yet. Job contract v0. Postgres action_log + brain history.html. Plain HTML view. Deployed on both. both · proves the round trip
  2. Add the sandbox Jon moves into a sandbox — the first real divergence. Impl 1 = Sapiom/Blaxel · Impl 2 = Daytona. Same job-contract wire on both. impl 1: Sapiom · impl 2: Daytona
  3. Second specialist + Maestro routing Add Max (SEO) → Maestro now genuinely chooses between specialists. Brings in the scraping service: Scrapling (Impl 1) / Firecrawl (Impl 2). both · Maestro selection logic earns its LLM
  4. Generative dashboard Replace the plain HTML view with json-render driven by Maestro's dashboard.json. Wire real auth (WorkOS / magic link). both · customer-facing surface
  5. The factory Stand up Executor + Reviewer so new specialists are built, not hand-written. Tools become CLI + Skill repos under tenwhy-tools/. both · the supply side
  6. Multi-tenancy + billing Per-customer provisioning, RLS, the hiring flow, and customer billing (native Stripe shines in Impl 2). both · product-ready

7 · Open verification

Not blockers for Phase 1, but needed before Impl 2 hardens.

ItemStatus
Daytona sandboxverified <90ms boot · dedicated kernel/fs/net per sandbox · TypeScript SDK + REST · open-source · ephemeral + snapshot. Strong µVM analog; the TS SDK fits our Node world.
Stripe Projects catalogneeds auth Install stripe plugin install projects + stripe login, then stripe projects catalog to confirm exact service refs (Daytona, Neon, WorkOS, OpenRouter, Firecrawl, Inngest, Sentry).
Better Stack in catalog?verify If present, it moves inside Projects and the Impl 2 observability asymmetry disappears. If not, it's wired manually (like the GitHub brain).
Stripe Projects paid-tier countriesverify Paid provider provisioning is limited to certain countries — confirm coverage before committing Impl 2 to production.
Sandbox egress enforcementverify Confirm Sapiom and Daytona enforce the job's allow-listed endpoints at the network layer. This is the runtime half of the security boundary (design decision #32) — if egress isn't network-enforced, "no tool spends money wrongly" has no teeth.

reminder  Stripe Projects is a provisioning + credential + billing layer, not a runtime. It never runs Maestro or the agents — it just makes sure the right vendors are provisioned and the right keys are in .env. The architecture in the design is unchanged by it.