HomeZero · plan
For agentic workers Implement task-by-task with superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Each task ends in an independently verifiable deliverable. Work in the worktree ~/Projects/Zero/homezero/.claude/worktrees/advisor-flue (branch feature/advisor-flue). Per project rule, use Opus for any test/verify subagent.
Goal
A real signup → a Flue advisor Worker self-authors its identity from the onboarding answers, walks waking→studying_market→authoring→scouting→ready live on /home, calls intakes.markBorn, then chats in that authored persona — replacing the cells doorbell→zero-mother birth.
Architecture
The advisor is a separate Cloudflare Worker (Flue/Pi/Opus) inside the homezero repo at flue/, with its own package.json + wrangler.jsonc, deployed independently of OpenNext. Instance id = Clerk userId. A durable Workspace holds IDENTITY.md; first-run vs configured is decided by whether it exists. The agent writes to the existing Convex (dev:nautical-rabbit-780) via the deploy key through one injectable AdvisorStore seam. The status ladder reuses the existing teamMembers/team.getForUser/lib/awakening.ts//home pipeline — extended with one granular step field, not a new table.
Tech stack
Flue (@flue/runtime beta) · Pi · Cloudflare Workers + Durable Objects · cloudflare-shell Workspace · AI Gateway (claudeflare-gateway) · Anthropic Opus 4.8 · Convex (deploy-key writes) · TypeScript/ESM · Vitest + convex-test.
Scope
In (Plan 1): the Worker, self-config, the live step ladder, configured chat, the birth-trigger swap, smoke. Plan 2 (committed, next): memory · dream · Slack-staff (auto) · WhatsApp (optional). Later specs: GPT-5.5, market & home agents, Turso (one DB per agent), production auth, teardown rework. Pete owns: residential-IP egress.
Spec
flue-architecture.html. Grounded in a 8-reader map of the live homezero repo (identity model, doorbell, resolver, /home, deploy, Convex idioms, advisor representation).

Ground truth (from the repo, not assumptions)

FactSourceConsequence for the plan
Identity is the flat Clerk userId (identity.subject); no household table. buyers is the unique per-user record (index by_user).schema.ts:78,98, devAuth.ts:18Flue instance id = userId. The spec's "householdId" is the Clerk userId.
A live status ladder already exists: teamMembers(role advisor) → team.getForUser → lib/awakening.ts → /home renders it reactively.team.ts:28, home/page.tsx:92,166, awakening.tsAdd a granular step field to the advisor's row. Do not build a new advisorStatus table.
Birth trigger = ringDoorbell (POSTs to wa-bridge → zero-mother), scheduled UNCONDITIONALLY at intakes.ts:96 (before the covered block at :105), and at admin.ts:325 (gated on args.ring, not coverage). The roster seeds only for covered markets at intakes.ts:105.doorbell.ts:12, intakes.ts:96,105Replace with a direct POST to the Worker and MOVE it inside the covered block (deliberate change — births gate to covered markets).
intakes.markBorn is the completion callback the newborn cell calls out-of-band (flips status→born, wires buyers.advisorCell, advances roster).intakes.ts:167KEEP. The Worker calls it at the end of self-config. This is what flips the UI to done.
App deploys via OpenNext (Worker name homezero, wrangler.jsonc). No /app/api routes.package.json:12, wrangler.jsonc:3Flue is a separate Worker (own name/route/config), never deployed through opennextjs.
Agents write Convex via the deploy key calling internalMutations (cells use npx convex run; a Worker uses an admin HTTP client). Object validators are exact.agents.ts:7, cells/advisor/TOOLS.mdNew writes are internalMutations; the Worker calls them with admin auth. Type validators after observing real shapes.
The agents table is the real-estate-agent directory, not AI agents.agents.ts:5, schema.ts:37Name trap — never put advisor data there.

Global constraints — every task inherits these

File map

FileResponsibilityStatus
The Flue Worker — self-contained subproject at flue/
flue/package.jsonIts own deps (@flue/runtime, agents, valibot, wrangler, vitest) + build/deploy scripts. Isolated from the Next app.create
flue/wrangler.jsoncWorker homezero-flue, its own route + DO migration (FlueRegistry + FlueAdvisorAgent — both required by Flue's CF target), AI binding, Workspace + worker_loaders bindings.create
flue/flue.config.ts · flue/src/cloudflare.tsFlue config + cloudflare-shell Workspace adapter.create
flue/src/app.tsProvider registrations (gateway) + Hono routes + dev chat. Ported from the spike.create
flue/src/agents/advisor.tsThe advisor agent: route guard, first-run vs configured branch, instructions, tools.create
flue/src/advisor/{store,convexStore,identity,tools,prompts}.tsStore seam + Convex impl, Workspace identity helpers, tools, prompt blocks.create
flue/test/*.test.ts · flue/scripts/smoke.shVitest units (store/identity/tools) + end-to-end smoke.create
Convex + web — existing homezero files
convex/validators.tsAdd advisorStep enum (the 5 ladder literals).modify
convex/schema.ts:186-201Add optional step field to teamMembers.modify
convex/team.tsNew setAdvisorStep internalMutation (modeled on advanceMember); surface step through getForUser (already returns full rows).modify
lib/awakening.tsMap step → ladder label/copy/dot. Keep pure (no Convex imports — edge-test constraint).modify
app/(app)/home/page.tsx:166-218 · app/(app)/shared.tsxRender the granular step in the roster strip / awakening hero.modify
convex/doorbell.tsNew ringAdvisor internalAction (direct POST to the Worker). Retire ringDoorbell.modify
convex/intakes.ts:96-120 · convex/admin.ts:325Swap the ringDoorbell schedule for ringAdvisor, gated to covered markets.modify
convex/testModules.tsRegister any new convex module touched by tests.modify
P0

Scaffold the Flue Worker + de-risk the two unknowns

3 tasks

Stand the Worker up as its own subproject, prove it replies on Opus, and pin the two genuinely-unknown APIs before any feature code: Flue's tool/Workspace calls, and how a Worker writes a Convex internalMutation with the deploy key.

TASK 1

Scaffold flue/ as an isolated Worker; advisor replies on Opus

createflue/package.json, flue/flue.config.ts, flue/wrangler.jsonc, flue/src/app.ts, flue/src/agents/advisor.ts, flue/tsconfig.json
createflue/.dev.vars (gitignored) — secrets
Produces
A deployed Worker homezero-flue exposing POST /agents/advisor/:userId (Flue routing) behind a password gate, replying on Opus through the gateway. Model the provider registrations + route guard on the proven spike at the ABSOLUTE path /Users/pete/Projects/Zero/.claude/worktrees/flue-spike/experiments/flue-spike/src/{app.ts,agents/hello-world.ts} (a separate worktree — NOT under this build tree, so use the absolute path to read it).
Step 1 — Init the subproject + Flue deps
cd ~/Projects/Zero/homezero/.claude/worktrees/advisor-flue
mkdir -p flue && cd flue
npm init -y && npm pkg set type=module
npm i @flue/runtime valibot agents hono
npm i -D @flue/cli wrangler vitest
Expect: flue/package.json with "type":"module" and the Flue deps. This package.json is independent of the repo root's.
Step 2 — Port app.ts (providers + gateway + dev chat) and advisor.ts (Opus, static persona)

Model on the spike's two registrations (app.ts:12-26) — a gw provider (workers-ai via the gateway) AND registerProvider('anthropic', { baseUrl: GW('anthropic'), apiKey: env.ANTHROPIC_API_KEY }) — plus the password route guard. Advisor returns { model:'anthropic/claude-opus-4-8', instructions:'You are the HomeZero advisor…' } for now (Task 8 makes it dynamic). Account id in GW() = 5a6fef07a998d84ec047ef43d0543342, gateway claudeflare-gateway.

Step 3 — wrangler.jsonc for a SEPARATE Worker
// flue/wrangler.jsonc
{ "name": "homezero-flue", // NEVER "homezero"
  "compatibility_date": "2026-05-18",
  "compatibility_flags": ["nodejs_compat"],
  "ai": { "binding": "AI" },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["FlueRegistry", "FlueAdvisorAgent"] }] }
// Workspace binding name comes from Task 2's generator; route added in Task 12.
Step 4 — Secrets, build, deploy

Put ANTHROPIC_API_KEY, GATEWAY_TOKEN, AUTH_PASSWORD in flue/.dev.vars; set them as Worker secrets with wrangler secret put.

npx flue build --target cloudflare
npx wrangler deploy --config dist/*/wrangler.json
Verify: curl -s -X POST https://homezero-flue.<subdomain>.workers.dev/agents/advisor/user_test -H "Authorization: Bearer $AUTH_PASSWORD" -H 'content-type: application/json' -d '{"message":"hi"}' → 202; the stream yields Opus text. 401 without the bearer.
Step 5 — Commit
git add flue && git commit -m "feat(flue): advisor Worker scaffold — homezero-flue, opus, gateway, gated"
TASK 2

Pin the Flue Workspace + tool API

readflue/node_modules/@flue/runtime types
createflue/NOTES-flue-api.md (gitignored scratch)
Produces
Confirmed signatures for: defining a tool (defineTool), attaching tools to createAgent, and reading/writing a Workspace file (FlueFs.readFile/writeFile). Already known from the types: the factory IS async-capable (createAgent(ctx => Promise<AgentRuntimeConfig>)) but its context is only { id, env, payload } — there is NO Workspace handle on the factory, so first-run detection must be a first-turn init tool (Task 9), not a factory read. Tasks 6–9 depend on these.
Step 1 — Add the Workspace sandbox
cd flue && npx flue add sandbox cloudflare-shell
Expect: a generated src/cloudflare.ts, a flue.config.ts edit, new deps (@cloudflare/shell, @cloudflare/codemode) and a worker_loaders binding (e.g. LOADER) — the durable Workspace REQUIRES it. Add the Workspace + worker_loaders bindings to flue/wrangler.jsonc before Task 8/9 rely on IDENTITY.md persistence.
Step 2 — Grep the installed types
# NOTE: @flue/runtime types are .d.mts, not .d.ts
grep -rn "defineTool\|createAgent\|ToolDefinition" node_modules/@flue/runtime/dist/*.d.mts | head
grep -rni "readFile\|writeFile\|FlueFs\|AgentCreateContext" node_modules/@flue/runtime/dist/*.d.mts | head -30
Expect: createAgent in dist/index.d.mts; defineTool in dist/tool-*.d.mts; FlueFs.readFile/writeFile + AgentCreateContext {id,env,payload} in dist/types-*.d.mts. Record verbatim in NOTES.
Step 3 — Commit config
git add flue/flue.config.ts flue/src/cloudflare.ts flue/wrangler.jsonc
git commit -m "chore(flue): add cloudflare-shell Workspace; pin tool/workspace API"
TASK 3

Spike: Worker → Convex internalMutation with the deploy key

createflue/src/advisor/convexClient.ts
read/Users/pete/Projects/Zero/cells/advisor/{TOOLS.md,skills/zero-convex/SKILL.md} (sibling repo — absolute path)
Produces
callConvex(fnPath: string, args: object): Promise<any> — a thin admin client that invokes a Convex internalMutation/internalQuery from inside the Worker using CONVEX_URL + CONVEX_DEPLOY_KEY. Consumed by convexStore.ts (Task 7) and the birth payload path.
Step 1 — Confirm the admin-call mechanism

A Worker can't shell out npx convex run (the cells idiom). Confirmed path (the Convex CLI's own run.js does exactly this): ConvexHttpClient from convex/browser (Worker-safe, no Node deps) → client.setAdminAuth(CONVEX_DEPLOY_KEY) → client.mutation(makeFunctionReference('team:setAdvisorStep'), args). Pin the exact setAdminAuth + function-reference signatures against the installed convex package.

Expect: callConvex implemented with that shape, recorded in NOTES.
Step 2 — Implement + prove against a harmless internal read

Implement callConvex. Prove it by calling an existing internalQuery that needs no setup — e.g. cellRegistry:list (public) or intakes:getById with a known id — from a temporary /debug/convex route, gated by the password.

Verify: curl …/debug/convex?fn=cellRegistry:list returns rows. Remove the debug route (or keep gated) before P6.
Step 3 — Commit
git add flue/src/advisor/convexClient.ts flue/src/app.ts
git commit -m "feat(flue): Worker→Convex admin client (deploy-key internal calls)"
P1

The live step ladder (Convex + web)

3 tasks

Extend the existing roster pipeline with the granular ladder. No new table — a step field on the advisor's teamMembers row, written by an internalMutation, surfaced through the read the app already makes, rendered by the pure mapping module.

TASK 4

Schema + validator + setAdvisorStep mutation

modifyconvex/validators.ts (add advisorStep)
modifyconvex/schema.ts:186 (teamMembers += step)
modifyconvex/team.ts (add setAdvisorStep)
testconvex/team.test.ts (extend)
Produces
export const advisorStep = v.union(v.literal('waking'),v.literal('studying_market'),v.literal('authoring'),v.literal('scouting'),v.literal('ready')); teamMembers.step?: advisorStep; internal.team.setAdvisorStep({ userId: v.string(), step: advisorStep }) — patches the role=='advisor' row's step + updatedAt, modeled on advanceMember (team.ts:106) which does NOT regression-guard.
Step 1 — Failing test (convex-test)

Follow the verbatim idiom from board.test.ts:1-7: convexTest(schema, modules), seed an advisor row via t.run((ctx)=>ctx.db.insert('teamMembers',{userId:'user_1',role:'advisor',label:'Your advisor',status:'birthing',order:0,createdAt:0,updatedAt:0})), call t.mutation(internal.team.setAdvisorStep,{userId:'user_1',step:'authoring'}), assert the row's step==='authoring' via t.run. Add a rejection test for an unknown step.

Run: npx vitest run convex/team.test.ts · Expect: FAIL.
Step 2 — Add the validator, schema field, mutation

Add advisorStep to validators.ts (with a prose comment, house style). Import it in schema.ts (imports block lines 3-24) and add step: v.optional(advisorStep) to the teamMembers table. Implement setAdvisorStep in team.ts: find the by_user_role row (userId, 'advisor'); ctx.db.patch(row._id,{ step, updatedAt: Date.now() }).

Run: npx vitest run convex/team.test.ts · Expect: PASS. getForUser already returns full rows, so step reaches the client with no query change.
Step 3 — Commit
git add convex/validators.ts convex/schema.ts convex/team.ts convex/team.test.ts
git commit -m "feat(convex): advisorStep ladder field + setAdvisorStep on teamMembers"
TASK 5

Render the ladder on /home (pure mapping + UI)

modifylib/awakening.ts (step → label/copy/dot)
modifyapp/(app)/home/page.tsx:166-218 · app/(app)/shared.tsx
testlib/awakening.test.ts (or the existing awakening test)
Consumes
RosterMember now carries step?: string (extend the type at awakening.ts:7).
Produces
stepCopy(step): string mapping the 5 literals to user-facing lines ("Getting to know your market…", etc.), used by the roster strip + awakening hero.
Step 1 — Failing pure-function test

In the edge-safe awakening test, assert stepCopy('studying_market') contains "market" and the 5 steps each map to a distinct non-empty line. Keep lib/awakening.ts free of Convex imports (file header constraint).

Run: npx vitest run lib/awakening.test.ts · Expect: FAIL.
Step 2 — Implement mapping + extend RosterMember + render

Add stepCopy + a stepIndex (0-4 for a progress affordance). Extend RosterMember with step?. In home/page.tsx, when the advisor row has a step and status is pre-born, render stepCopy(step) in the awakening hero (page.tsx:180-218) and/or the roster strip subtitle (page.tsx:166-176). Reuse the existing reactive useQuery(api.team.getForUser, …) — no new query.

Run: npx vitest run lib/awakening.test.ts · Expect: PASS. Then npm run dev and, with the hz_dev_user=demo-pete cookie, hand-poke a step via setAdvisorStep and watch /home update live.
Step 3 — Commit
git add lib/awakening.ts lib/awakening.test.ts app/\(app\)/home/page.tsx app/\(app\)/shared.tsx
git commit -m "feat(web): render the granular advisor step ladder on /home"
TASK 6

Register modules; full Convex test pass

modifyconvex/testModules.ts (if a new module was added)
Step 1 — Ensure registration + green suite

If P1 added a new convex module, add "./<file>.ts": () => import("./<file>") to testModules.ts (convex-test won't load it otherwise). Run the whole suite.

Run: npm test · Expect: all green, including the existing board/team/intakes tests (no regressions from the schema change).
Step 2 — Commit
git add convex/testModules.ts && git commit -m "test(convex): register modules; full suite green" --allow-empty
P2

The store seam (testable, then Convex-backed)

2 tasks

Everything the agent writes outward goes through one interface — so the agent logic is unit-testable with no live Convex, and the real impl is a thin shim over the Task-3 admin client + the Task-4 mutation + the kept markBorn.

TASK 7

AdvisorStore interface + MemoryStore (tests) + ConvexStore (real)

createflue/src/advisor/store.ts · flue/src/advisor/convexStore.ts
testflue/test/store.test.ts
Produces
export type AdvisorStep = 'waking'|'studying_market'|'authoring'|'scouting'|'ready'
export interface AdvisorStore {
  setStep(userId: string, step: AdvisorStep): Promise<void>     // → internal.team.setAdvisorStep
  markBorn(intakeId: string, advisorCell: string): Promise<void> // → internal.intakes.markBorn (KEPT)
}
MemoryStore records calls for assertions; ConvexStore wraps callConvex (Task 3).
Step 1 — Failing MemoryStore test

Assert setStep records the last step per userId and markBorn records (intakeId, advisorCell). (Same pattern as the spike plan's store test.)

Run: cd flue && npx vitest run · Expect: FAIL.
Step 2 — Implement both impls

MemoryStore = Maps. ConvexStore.setStep → callConvex('team:setAdvisorStep',{userId,step}); ConvexStore.markBorn → callConvex('intakes:markBorn',{intakeId,advisorCell}). getStore() returns ConvexStore in the Worker, MemoryStore in tests.

Run: npx vitest run · Expect: PASS.
Step 3 — Commit
git add flue/src/advisor/store.ts flue/src/advisor/convexStore.ts flue/test/store.test.ts
git commit -m "feat(flue): AdvisorStore seam — MemoryStore + ConvexStore(setStep, markBorn)"
TASK 8

Status + identity tools

createflue/src/advisor/tools.ts · flue/src/advisor/identity.ts
testflue/test/{tools,identity}.test.ts
Produces
makeAdvisorTools(store, { userId, intakeId }) → set_step({step}) (validates the 5 literals, calls store.setStep) + finish_birth() (no args — calls store.markBorn(intakeId, advisorCell) with advisorCell = 'flue:advisor:' + userId; the advisor is named Zero so there's no name param; advisorCell is vestigial pending the market-agent port). userId + intakeId are closure-bound from the birth payload — the model never supplies them. Plus a check_identity init tool + identity helpers isConfigured/readIdentity/writeIdentity(ws) over the Task-2 FlueFs API. The model writes IDENTITY.md via the built-in Workspace code tool.
Step 1 — Failing tests

set_step('authoring') records to MemoryStore; set_step('banana') rejects. isConfigured flips false→true after writeIdentity (FakeWorkspace, Map-backed, matching Task-2 shape).

Run: npx vitest run · Expect: FAIL.
Step 2 — Implement tools + identity helpers

Validate step against the exact 5 literals; bind userId via closure (model never supplies it). isConfigured = IDENTITY.md exists & .trim().length>0.

Run: npx vitest run · Expect: PASS.
Step 3 — Commit
git add flue/src/advisor/tools.ts flue/src/advisor/identity.ts flue/test
git commit -m "feat(flue): set_step + finish_birth tools, IDENTITY.md helpers"
P3

Self-config first-run

1 task

First message → the model authors IDENTITY.md and walks the ladder via tools, ending with finish_birth (which calls the kept markBorn). Market context comes from the birth payload — no Clerk-gated /api/market-preview call needed.

TASK 9

Branch the agent: first-run vs configured

createflue/src/advisor/prompts.ts
modifyflue/src/agents/advisor.ts (dynamic instructions + tools)
Consumes
isConfigured/readIdentity (T8), makeAdvisorTools + check_identity (T8), getStore() (T7), the session FlueFs (T2). The first POST body — available as ctx.payload in the factory — carries { intakeId, userId, name, where, answers, market:{ marketCell, county, coverage } } (produced by Task 11). The factory context has NO Workspace handle, so first-run detection is the check_identity init tool, not a factory read.
Step 1 — Write the two prompt blocks

FIRST_RUN_INSTRUCTIONS: "You are Zero, this household's HomeZero advisor, being born. The message carries the onboarding answers (place + person) and the resolved market. Call check_identity first to confirm you're unconfigured. Then, in order, calling the named tools: (1) set_step('waking'); (2) set_step('studying_market'), reflect on the resolved market; (3) set_step('authoring'), write IDENTITY.md via the workspace tool — your understanding of THIS user from their answers (who they are, what they want, their constraints). Your name is Zero and your yearnings ship in yearnings.md — do NOT invent either; (4) set_step('scouting'); (5) set_step('ready'); (6) finish_birth, then greet them in one warm line as Zero." CONFIGURED_INSTRUCTIONS(identityMd): "You are Zero, this household's advisor. Your understanding of them: «identity». Warm, concise 1-3 sentences. No tools unless asked."

Step 2 — Dynamic factory (init-tool detection — the factory has no Workspace handle)

Context is only { id, env, payload } — no Workspace. Branch on whether ctx.payload looks like a birth payload (carries intakeId) vs ordinary chat; always expose the tools incl. check_identity:

export default createAgent(async ({ id, payload }) => {
  const userId = id, store = getStore()
  const intakeId = payload?.intakeId            // present only on the birth POST
  return {
    model: 'anthropic/claude-opus-4-8',
    tools: [checkIdentityTool(userId), ...makeAdvisorTools(store, { userId, intakeId })],
    instructions: intakeId ? FIRST_RUN_INSTRUCTIONS : CONFIGURED_INSTRUCTIONS_LAZY,
  }
})

check_identity reads IDENTITY.md from the session FlueFs and tells the model whether it's already configured — so a stray birth payload can't re-author over an existing identity. CONFIGURED_INSTRUCTIONS_LAZY tells the model to call check_identity first and adopt the returned identity. (Pin the init-tool's FlueFs access against Task 2's NOTES.)

Step 3 — Deploy + drive a real first-run by curl
npx flue build --target cloudflare && npx wrangler deploy --config dist/*/wrangler.json
U=user_smoke_$(date +%s)
curl -s -X POST https://homezero-flue.<sub>.workers.dev/agents/advisor/$U \
  -H "Authorization: Bearer $AUTH_PASSWORD" -H 'content-type: application/json' \
  -d '{"message":"{\"name\":\"Pat\",\"where\":\"Paonia, CO 81428\",\"answers\":{\"context\":[{\"label\":\"frugal\"},{\"label\":\"workshop\"}]},\"market\":{\"marketCell\":\"zero-delta-co-market\",\"county\":\"delta-co\",\"coverage\":\"covered\"}}"}'
Expect (stream): the model calls set_step through the ladder, writes IDENTITY.md, calls finish_birth, greets in one line. Because getStore()=ConvexStore, the steps land on the seeded teamMembers row (seed it first for $U via setRoster or the P5 path). Verify in Convex: the advisor row's step advanced and status='born'.
Step 4 — Commit
git add flue/src/advisor/prompts.ts flue/src/agents/advisor.ts
git commit -m "feat(flue): self-config first-run — authors IDENTITY.md, walks ladder, finish_birth"
P4

Configured chat

1 task

Second and later messages read the authored IDENTITY.md and chat in persona — no re-config, no status writes.

TASK 10

Verify the configured branch + dev chat surface

modifyflue/src/app.ts (dev chat page → /agents/advisor/<userId>, no model picker)
Step 1 — Second message to the same userId

POST a normal question to the same $U ("what should I watch on utility costs?").

Expect: no set_step / no IDENTITY.md rewrite; a warm 1-3 sentence answer consistent with the authored persona.
Step 2 — Fresh userId re-births cleanly

POST onboarding answers to a brand-new $U2; confirm full first-run (per-instance isolation).

Expect: new user walks the ladder; the old one stays configured.
Step 3 — Commit
git add flue/src/app.ts && git commit -m "feat(flue): dev chat surface + verify configured branch"
P5

Swap the birth trigger (retire the doorbell)

2 tasks

Replace the ringDoorbell → wa-bridge → zero-mother path with a direct POST to the Worker, fired at the same seam and gated to covered markets.

TASK 11

ringAdvisor internalAction + repoint the schedule

modifyconvex/doorbell.ts (add ringAdvisor; retire ringDoorbell)
modifyconvex/intakes.ts:96-120 (covered-market guard) · convex/admin.ts:325
testconvex/intakes.test.ts (extend; mock fetch)
Produces
internal.doorbell.ringAdvisor({ intakeId }) — re-queries intakes.getById, resolves market, POSTs { intakeId, userId, name, where, answers, market:{marketCell,county,coverage} } to ADVISOR_WORKER_URL with Bearer AUTH_PASSWORD (the Worker's gate). Mirrors the zeroCreated fire-and-forget shape (doorbell.ts:317), not the retry-heavy ringDoorbell.
Step 1 — Implement ringAdvisor

Copy zeroCreated (doorbell.ts:317-350): read process.env.ADVISOR_WORKER_URL + AUTH_PASSWORD; skip-log if unset; load the intake via internal.intakes.getById; POST the payload to {ADVISOR_WORKER_URL}/agents/advisor/{userId}. The Worker's first-run consumes it.

Step 2 — Repoint the schedule, gated to covered

At intakes.ts, move the advisor wake INTO the covered-market block (the same guard as the roster seed at :105) and schedule ringAdvisor instead of ringDoorbell: ctx.scheduler.runAfter(0, internal.doorbell.ringAdvisor, { intakeId }). Out-of-bounds/coverable → no birth (waitlist). For admin.ts:325 (the manual/synthetic-birth ring, gated on args.ring !== false, NOT coverage) just swap ringDoorbell → ringAdvisor and KEEP its args.ring guard — don't impose coverage there. Delete the old ringDoorbell internalAction once both callers are repointed.

Note: deliberate change from today's unconditional doorbell — gating to covered markets enforces gated-markets and guarantees a seeded advisor row exists for setAdvisorStep.
Step 3 — Update the existing test + add coverage cases

First fix the breakage: intakes.test.ts:58-62 currently asserts a scheduled job whose name includes 'ringDoorbell' with args [{intakeId}] — change 'ringDoorbell'→'ringAdvisor' and confirm the scheduled count stays 2 (ringAdvisor + setRoster) for a covered market. Then add: vi.stubGlobal('fetch', vi.fn(...)); covered where → Worker URL POSTed with the resolved market; out-of-bounds where → NO ringAdvisor scheduled, NO POST.

Run: npm test · Expect: PASS (incl. the updated existing assertion).
Step 4 — Commit
git add convex/doorbell.ts convex/intakes.ts convex/admin.ts convex/intakes.test.ts
git commit -m "feat(convex): ringAdvisor — direct POST to the Flue Worker, gated to covered markets"
TASK 12

Wire env + route; decide the doorbell-record fate

modifyflue/wrangler.jsonc (custom route, e.g. advisor.homezero.md) · Convex env ADVISOR_WORKER_URL
decideringDoorbell / markDoorbellResult / doorbellRecord deletion
Step 1 — Give the Worker a stable URL + set Convex env

Add a route to flue/wrangler.jsonc (a custom domain like advisor.homezero.md, distinct from the homezero Worker's www/apex/delta). Set ADVISOR_WORKER_URL + AUTH_PASSWORD on the Convex deployment (npx convex env set).

Step 2 — Retire dead doorbell machinery (gated decision)

The direct POST surfaces its own success/failure, so the bridge-era delivery bookkeeping is obsolete: ringDoorbell + DOORBELL_RETRY_BACKOFF_MS, and — if no operator wants delivery-health — markDoorbellResult + intakes.doorbell field + doorbellRecord/doorbellState. Nothing in app/ reads them. KEEP the unrelated doorbells (ringTeardown, marketEvent, zeroCreated).

Confirm with Pete before deleting intakes.doorbell (operator signal). Default: delete ringDoorbell + retry consts now; leave the record field until confirmed.
Step 3 — Commit
git add flue/wrangler.jsonc convex/doorbell.ts
git commit -m "chore: advisor route + env; retire ringDoorbell retry machinery"
P6

End-to-end smoke

1 task

One scripted run proving the whole vertical: a fresh user's intake → the Worker self-configs → the ladder advances live → markBorn flips the roster → /home shows the advisor born → a chat turn answers in persona.

TASK 13

Smoke script

createflue/scripts/smoke.sh
Step 1 — Script the vertical

Mint U=user_smoke_$(date +%s). Seed an advisor roster row for U via convex run team:setRoster (covered market). POST the onboarding payload to the Worker. Poll team:getForUser (via convex run) until the advisor row's step==='ready' AND status==='born' (timeout 120s). Assert buyers.advisorCell got set (markBorn). POST a follow-up; assert a non-empty persona reply.

bash flue/scripts/smoke.sh
Expect: SMOKE OK — $U: ladder→ready, status=born, advisorCell set, persona replied, exit 0.
Step 2 — Commit
git add flue/scripts/smoke.sh && git commit -m "test: end-to-end advisor birth smoke"
Real browser proof (manual) With hz_dev_user=demo-pete, run the live onboarding wizard for a covered market and watch /home narrate waking→…→ready and settle on the born advisor — the full product moment, no curl.

Plan 2 preview — the full Zero

Built on Plan 1's foundation (same Worker, same Workspace, instance-per-userId). Each phase is independently shippable; none redesigns the vertical. Each gets its own detailed plan when Plan 1 lands — outlined here so the path is committed, not deferred.

PhaseWhat shipsDe-risk / integration
MemoryA memory/*.md store in Zero's Workspace — recalled at the top of each turn, written as it learns. yearnings.md ships as the shared standing questions.Pin the FlueFs list/read API (Task 2 already pins read/write). A recall step in the loop; no Convex change.
DreamA nightly self-wake that folds the day's sessions into durable memory + preps the brief.Pin Flue's schedule() / DO-alarm API. No user message; runs a dream skill over the Workspace.
Slack staff (auto)One #advisors channel, a thread per household. At birth Zero posts its intro (opens the thread) and records thread_ts↔userId; staff replies route to the staff session (Opus).flue add channel slack; a tiny Convex map for thread_ts↔userId; outbound via the Slack SDK (we hold the token).
WhatsApp (optional)Inbound WhatsApp → phone→buyer→userId → the buyer session — only for buyers who gave a number.flue add channel whatsapp; route via the existing buyers.by_phone unique index. Buyer hat stays Opus until GPT-5.5 is wired.
Sequencing Memory → Dream → Slack-staff → WhatsApp. Memory first (the spine the others lean on); dream needs memory; the two channels are independent and can land in either order.

Plan self-review

Spec requirement (Slice 1)Task(s)
Advisor on a DO, addressed by household identity, behind gatewayT1 (instance id = userId)
First-run authors IDENTITY.md + yearnings into the WorkspaceT2, T8, T9
Live status ladder (waking→…→ready) surfaced live on /homeT4 (field+mutation), T5 (render), T7/T8 (store+tools), T9 (walk)
Onboarding Finish → trigger the advisor (replaces doorbell→mother)T11, T12
Completion flips the UI to bornT7/T8 (markBorn via finish_birth), kept intakes.markBorn
Chat wired to the same agentT1 (dev), T10 (configured)
Opus-only; Pi-only; separate Worker; no agents-tableGlobal constraints + T1
Births gated to covered marketsT11 (covered-market guard)

Two unknowns are de-risked as explicit early tasks rather than fabricated: the Flue tool/Workspace API (T2) and the Worker→Convex admin-call mechanism (T3). Everything else cites a real file:line from the repo map.