A Flue advisor Worker living inside the homezero repo: born by onboarding Finish, authoring its own identity, walking the live status ladder the app already renders, and reporting "born" through the existing completion callback. Agent + Convex + web, one branch.
~/Projects/Zero/homezero/.claude/worktrees/advisor-flue (branch feature/advisor-flue). Per project rule, use Opus for any test/verify subagent.
| Fact | Source | Consequence 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:18 | Flue 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.ts | Add 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,105 | Replace 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:167 | KEEP. 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:3 | Flue 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.md | New 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:37 | Name trap — never put advisor data there. |
anthropic/claude-opus-4-8 via the gateway. No GPT-5.5/Sonnet/Llama.userId (the identity.subject string). There is no household table; buyers is the per-user record.waking → studying_market → authoring → scouting → ready, written as a step on the advisor's teamMembers row. Coarse teamMemberStatus (planned|birthing|born|failed|disconnected) is UNCHANGED.Zero — one persona for every household. First-run authors its per-user understanding (IDENTITY.md), NOT a name. Never "Homer" (legacy spike).yearnings.md, the same standing questions for every user (not invented per-advisor, not derived from onboarding answers). Shared to start; personalize later.homezero-flue (never homezero); own flue/wrangler.jsonc; deploy with wrangler deploy from flue/ — never opennextjs-cloudflare.internalMutation called with the deploy key; markBorn/cellRegistry.register/cellTemplateFiles are KEPT. Validators are EXACT — type after observing real shapes; conditional fields v.optional.agents table (real-estate directory). The per-house agent is the home agent, never "twin" (out of scope here).flue/.dev.vars + wrangler secret (already gitignored): ANTHROPIC_API_KEY, GATEWAY_TOKEN, AUTH_PASSWORD, CONVEX_URL, CONVEX_DEPLOY_KEY. Never commit keys. Don't commit to main — the worktree is feature/advisor-flue.| File | Responsibility | Status |
|---|---|---|
The Flue Worker — self-contained subproject at flue/ | ||
flue/package.json | Its own deps (@flue/runtime, agents, valibot, wrangler, vitest) + build/deploy scripts. Isolated from the Next app. | create |
flue/wrangler.jsonc | Worker 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.ts | Flue config + cloudflare-shell Workspace adapter. | create |
flue/src/app.ts | Provider registrations (gateway) + Hono routes + dev chat. Ported from the spike. | create |
flue/src/agents/advisor.ts | The advisor agent: route guard, first-run vs configured branch, instructions, tools. | create |
flue/src/advisor/{store,convexStore,identity,tools,prompts}.ts | Store seam + Convex impl, Workspace identity helpers, tools, prompt blocks. | create |
flue/test/*.test.ts · flue/scripts/smoke.sh | Vitest units (store/identity/tools) + end-to-end smoke. | create |
| Convex + web — existing homezero files | ||
convex/validators.ts | Add advisorStep enum (the 5 ladder literals). | modify |
convex/schema.ts:186-201 | Add optional step field to teamMembers. | modify |
convex/team.ts | New setAdvisorStep internalMutation (modeled on advanceMember); surface step through getForUser (already returns full rows). | modify |
lib/awakening.ts | Map step → ladder label/copy/dot. Keep pure (no Convex imports — edge-test constraint). | modify |
app/(app)/home/page.tsx:166-218 · app/(app)/shared.tsx | Render the granular step in the roster strip / awakening hero. | modify |
convex/doorbell.ts | New ringAdvisor internalAction (direct POST to the Worker). Retire ringDoorbell. | modify |
convex/intakes.ts:96-120 · convex/admin.ts:325 | Swap the ringDoorbell schedule for ringAdvisor, gated to covered markets. | modify |
convex/testModules.ts | Register any new convex module touched by tests. | modify |
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.
flue/ as an isolated Worker; advisor replies on Opushomezero-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).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
flue/package.json with "type":"module" and the Flue deps. This package.json is independent of the repo root's.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.
// 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.
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
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.git add flue && git commit -m "feat(flue): advisor Worker scaffold — homezero-flue, opus, gateway, gated"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.cd flue && npx flue add sandbox cloudflare-shellsrc/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.# 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 -30createAgent 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.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"internalMutation with the deploy keycallConvex(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.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.
callConvex implemented with that shape, recorded in NOTES.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.
curl …/debug/convex?fn=cellRegistry:list returns rows. Remove the debug route (or keep gated) before P6.git add flue/src/advisor/convexClient.ts flue/src/app.ts
git commit -m "feat(flue): Worker→Convex admin client (deploy-key internal calls)"step ladder (Convex + web)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.
setAdvisorStep mutationadvisorStep)step)setAdvisorStep)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.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.
npx vitest run convex/team.test.ts · Expect: FAIL.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() }).
npx vitest run convex/team.test.ts · Expect: PASS. getForUser already returns full rows, so step reaches the client with no query change.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"RosterMember now carries step?: string (extend the type at awakening.ts:7). stepCopy(step): string mapping the 5 literals to user-facing lines ("Getting to know your market…", etc.), used by the roster strip + awakening hero.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).
npx vitest run lib/awakening.test.ts · Expect: FAIL.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.
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.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"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.
npm test · Expect: all green, including the existing board/team/intakes tests (no regressions from the schema change).git add convex/testModules.ts && git commit -m "test(convex): register modules; full suite green" --allow-emptyEverything 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.
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).Assert setStep records the last step per userId and markBorn records (intakeId, advisorCell). (Same pattern as the spike plan's store test.)
cd flue && npx vitest run · Expect: FAIL.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.
npx vitest run · Expect: PASS.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)"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.set_step('authoring') records to MemoryStore; set_step('banana') rejects. isConfigured flips false→true after writeIdentity (FakeWorkspace, Map-backed, matching Task-2 shape).
npx vitest run · Expect: FAIL.Validate step against the exact 5 literals; bind userId via closure (model never supplies it). isConfigured = IDENTITY.md exists & .trim().length>0.
npx vitest run · Expect: PASS.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"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.
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.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."
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.)
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\"}}"}'
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'.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"Second and later messages read the authored IDENTITY.md and chat in persona — no re-config, no status writes.
POST a normal question to the same $U ("what should I watch on utility costs?").
set_step / no IDENTITY.md rewrite; a warm 1-3 sentence answer consistent with the authored persona.POST onboarding answers to a brand-new $U2; confirm full first-run (per-instance isolation).
git add flue/src/app.ts && git commit -m "feat(flue): dev chat surface + verify configured branch"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.
ringAdvisor internalAction + repoint the scheduleringAdvisor; retire ringDoorbell)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.ringAdvisorCopy 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.
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.
setAdvisorStep.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.
npm test · Expect: PASS (incl. the updated existing assertion).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"ADVISOR_WORKER_URLAdd 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).
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).
intakes.doorbell (operator signal). Default: delete ringDoorbell + retry consts now; leave the record field until confirmed.git add flue/wrangler.jsonc convex/doorbell.ts
git commit -m "chore: advisor route + env; retire ringDoorbell retry machinery"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.
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
SMOKE OK — $U: ladder→ready, status=born, advisorCell set, persona replied, exit 0.git add flue/scripts/smoke.sh && git commit -m "test: end-to-end advisor birth smoke"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.
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.
| Phase | What ships | De-risk / integration |
|---|---|---|
| Memory | A 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. |
| Dream | A 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. |
| Spec requirement (Slice 1) | Task(s) |
|---|---|
| Advisor on a DO, addressed by household identity, behind gateway | T1 (instance id = userId) |
| First-run authors IDENTITY.md + yearnings into the Workspace | T2, T8, T9 |
| Live status ladder (waking→…→ready) surfaced live on /home | T4 (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 born | T7/T8 (markBorn via finish_birth), kept intakes.markBorn |
| Chat wired to the same agent | T1 (dev), T10 (configured) |
| Opus-only; Pi-only; separate Worker; no agents-table | Global constraints + T1 |
| Births gated to covered markets | T11 (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.