HomeZero · Agent Model on Flue · Implementation Plan · 2026-06-23
Bite-sized, test-first tasks that take the advisor from a write-less birth shell to a living agent: it writes the board, grows a brain, runs on rhythm, and greets the buyer each morning. Every task has exact paths and real code.
For agentic workers: REQUIRED SUB-SKILL — use superpowers:subagent-driven-development to implement this plan task-by-task (fresh implementer per task on Opus, task review between, broad review at the end). Source spec: advisor-agent-spec.
Goal: Make the advisor agent operational — author the buyer's board, keep a private brain + journal, run five scheduled beats, and send a daily WhatsApp morning briefing.
Architecture: One Cloudflare Durable Object per buyer (/agents/advisor/:userId) in homezero/flue. Capabilities are agent-level defineTools paired by convention to native Flue base skills (build-time SKILL.md source). The agent's whole mind lives in ONE per-buyer Turso "brain DB" = its filesystem (a files table: IDENTITY, journal, playbooks) + its brain (yearnings/metrics/params) + its session log — addressable by userId from any libSQL client. Flue's flue_* engine tables stay in the DO; Convex stays the shared, app-served projection. Rhythm comes from extend() over DO alarms.
Tech stack: Flue (@flue/runtime, @flue/cli) on Cloudflare Workers/DO · Cloudflare Agents SDK (agents) · Convex (nautical-rabbit-780) · Turso/libSQL · Valibot · Vitest + convex-test · Anthropic Opus 4.8.
files table: IDENTITY.md, JOURNAL.md, skill PLAYBOOK.mds) + its brain (yearnings/metrics/params) + its session log (transcript mirror, for the dream). Addressed by the instance id (advisor = userId, market = north-fork-valley) and reachable from any libSQL client — the basis for SSH/TUI operability. Flue's own engine tables (flue_sessions, flue_session_entries, flue_runs, …) stay in the DO's SQLite — that's how the agent RUNS; the Turso brain is what it KNOWS./Users/pete/Projects/Zero/homezero/ (flue/ + convex/ + app/) — its OWN git repo. Branch off homezero's main (feature/advisor-loop), PR, squash-merge. NOT the specs meta-repo worktree.@flue/runtime ^1.0.0-beta.2 / @flue/cli ^1.0.0-beta.1. Tool shape is the LEGACY defineTool({ name, description, parameters: v.object({…}), async execute(args, signal){ return string } }) — the existing 4 advisor tools already match. DO NOT migrate to {input, run}.export const cloudflare = extend({ base: (Base) => class extends Base {…} }) from the agent module. Use onStart() + scheduleEvery(seconds, 'method') + schedule(when,'method'). NEVER call setAlarm directly (Flue owns alarm()).anthropic/claude-opus-4-8, thinkingLevel: 'medium' (unchanged). Every delegated implementer/reviewer subagent runs on Opus.internal* writers via deploy-key callConvex(fnPath, args) / callConvexQuery(fnPath, args), fnPath = "module:fn". Don't call buyer-facing public fns from the DO.TURSO_*). The ADVISOR uses a per-buyer Turso db named from the userId, provisioned at birth in a shared group — env TURSO_API_TOKEN (provision), TURSO_ORG, TURSO_ADVISOR_GROUP, TURSO_ADVISOR_GROUP_TOKEN (db auth); the URL is DERIVED from userId so there's no per-buyer secret. Store fns take c: Client so vitest injects createClient({ url: ':memory:' }); the Worker resolves @libsql/client/web via a lazy cloudflare:workers import.read, write, edit, grep, glob, bash — that run against the agent's SessionEnv (pluggable via the sandbox field; the default is an EPHEMERAL in-memory fs that doesn't persist, so we replace it). We supply a custom SessionEnv backed DIRECTLY by the per-buyer Turso files table: readFile/writeFile/edit/readdir/stat/exists/rm are Turso queries, and exec answers grep/glob/find as Turso content/path queries. ONE filesystem — Turso IS the disk; no in-memory buffer, no hydrate/flush, no just-bash. Arbitrary scripting bash (sed/awk/run programs) is NOT supported (the advisor doesn't need it; the market can opt into a real shell later). Do NOT build bespoke fs tools — the agent uses read/write/edit/grep on JOURNAL.md / <skill>/PLAYBOOK.md directly.skill_params + a per-skill PLAYBOOK.md (a row in the Turso files table, e.g. board/PLAYBOOK.md) the agent reads with its native read/grep tools and the dream rewrites with edit. The static base SKILL.md stays a build-time source file registered via createAgent({ skills }) — we do NOT depend on Flue re-discovering a rewritten skill at runtime; all learned state lives in the brain DB.flue/.dev.vars (gitignored) + Worker secrets / Convex env. Never commit or echo: ADVISOR_TURSO_*, TURSO_*, CONVEX_DEPLOY_KEY, AUTH_PASSWORD, ANTHROPIC_API_KEY, WhatsApp creds.homezero/flue (not the experiments/ sandbox); @flue beta.2 keeps the legacy tool shape (no migration); FlueFs has readdir + parent-creating writeFile; DO classes (FlueAdvisorAgent) are generated by flue build from flue.config.ts (target: 'cloudflare'); @flue is currently NOT installed in homezero/flue (Task 1 installs it). Self-improvement re-routed off unproven runtime skill-rediscovery (see Global Constraints).Outcome: a newly-onboarded buyer's board is populated by an agent-authored card with a real question, and zeroing a home wakes the advisor to brief it. Kills the empty-board bug. Ships on its own.
flue/node_modules/@flue at beta.2; confirmation that the legacy tool shape builds and the existing market+advisor vitest suite passes. Every later task assumes this.cd /Users/pete/Projects/Zero/homezero/flue && bun install. Then read the resolved version: cat node_modules/@flue/runtime/package.json | grep '"version"' — expect 1.0.0-beta.2. If it resolves a higher beta whose defineTool rejects parameters/execute (a ToolLegacyDefinitionError at build), pin exact "@flue/runtime": "1.0.0-beta.2" in package.json and re-install. The repo's tools MUST keep building unchanged.bunx flue build (uses flue.config.ts, target: 'cloudflare') and bunx tsc --noEmit. Expected: build emits FlueAdvisorAgent/FlueMarketAgent with no tool-shape errors.bun run test (vitest). Expected: existing suite green (market store + advisor factory + convex tests). Record the pass count as the baseline.feature/advisor-loop: git commit -m "chore(flue): install @flue beta.2, confirm legacy tool shape builds".callConvex(fnPath,args) from advisor/convexClient.ts; makeAdvisorTools(store,{userId}) closure pattern (existing). Convex board.authorHomeCard (internalMutation, args {userId, slug, body, ask?, stage?, authorCell?, placedBy?}, board.ts:340-384), board.pinAsk (board.ts:402-410), board.agentCards (internalQuery, {userId}, board.ts:263-295).makeAdvisorTools — author_home_card, pin_ask, read_board — all closure-bound to userId.board-tools.test.ts, build the tool set with a fake callConvex spy injected, call author_home_card with {slug:'123-main', body:'…', ask:{prompt:'Town or land?', control:'this-or-that', options:['Town','Land']}}, and assert the spy was called with ('board:authorHomeCard', {userId:'u1', slug:'123-main', body:'…', ask:{…}, placedBy:{by:'advisor'}}). Mirror the existing advisor tool test style (inject the seam, assert the Convex call).makeAdvisorTools:
const author_home_card = defineTool({ name: 'author_home_card', description: 'Author or update a home card on this buyer\'s board…', parameters: v.object({ slug: v.string(), body: v.string(), ask: v.optional(askSchema), stage: v.optional(v.string()) }), async execute({ slug, body, ask, stage }) { await callConvex('board:authorHomeCard', { userId, slug, body, ask, stage, placedBy: { by: 'advisor' } }); return `card authored for ${slug}`; }, });Add
pin_ask (→ board:pinAsk with {cardId, ask}) and read_board (→ callConvexQuery('board:agentCards',{userId}), returns the cards JSON as a string). Define askSchema as the valibot mirror of boardCardAsk (prompt, control: v.picklist(['yn','this-or-that','chips','knob']), optional options, why).feat(advisor): board write tools (author_home_card, pin_ask, read_board).author_home_card throws if the slug has no listing — the react prompt only authors for a real zeroed home, so that's the intended guard.createAgent((ctx)=>({…, skills})) config field; the SKILL.md import declaration (@flue/runtime/types/skill-md.d.ts).board skill that teaches card authorship + the one-question rule, paired to the Task 2 tools.board/SKILL.md — frontmatter name: board, description: "Compose and place a home card on the buyer's board; ask exactly one good question."; body = the playbook: when to use author_home_card, how to write a card body from the home + the buyer's lens, the rule "exactly one question, drawn from the strongest open yearning," the ask.control options, and "never overwhelm — one card, one question."skills.test.ts, import the agent factory and assert its config skills array contains an entry with name === 'board'. (Skill registration is config — assert on the returned config object, the same way the existing factory-shape test asserts model/tools.)advisor.ts:
import boardSkill from '../advisor/skills/board/SKILL.md'; // …inside createAgent return: skills: [boardSkill],Confirm
tsconfig/flue build resolves the .md import (the runtime ships the module declaration; if tsc complains, add /// <reference types="@flue/runtime/skill-md" /> or the project's *.md ambient decl).feat(advisor): native board skill (SKILL.md) registered on the agent.activate_skill tool. The learned/self-tuning layer comes later (Task 14); this is the static base playbook.marketDoorbell.ringMarket pattern (marketDoorbell.ts:13-36) and the ctx.scheduler.runAfter(0, internal.marketDoorbell.ringMarket, {}) call sites in stars.ts.internal.advisorDoorbell.ringAdvisorZero({ userId, slug }) — an internalAction POSTing to /agents/advisor/${userId} with { message: JSON.stringify({ kind:'zero', slug }) }; fired alongside the market ring, scoped to the zeroing buyer.advisorDoorbell.test.ts, stub ADVISOR_WORKER_URL + AUTH_PASSWORD and a fetch spy; run ringAdvisorZero({userId:'u1', slug:'123-main'}); assert the POST URL = ${url}/agents/advisor/u1, the Bearer header, and the body = {message: JSON.stringify({kind:'zero', slug:'123-main'})}. Mirror the market doorbell test.marketDoorbell.ts to advisorDoorbell.ts; change args to { userId: v.string(), slug: v.string() }, the URL to /agents/advisor/${userId}, and the body kind to { kind:'zero', slug }. Keep the env-unset skip guard.stars.ts, at BOTH if (created) branches that ring the market, add (scoped to the buyer's userId — read it from the star/intake context already present in the handler):
await ctx.scheduler.runAfter(0, internal.advisorDoorbell.ringAdvisorZero,
{ userId, slug });
Verify userId + slug are in scope at each site (the handler resolves the buyer + the listing slug; if slug isn't already derived, pull it from the saved/pending row).feat(convex): advisor zero-doorbell fired on zero alongside the market ring.ADVISOR_INSTRUCTIONS (prompts.ts) and the message-kind convention ({kind:'zero', slug} arrives inside message).kind:'zero' wake, activate the board skill, brief the home through the buyer's lens, and author exactly one card with one question.ADVISOR_INSTRUCTIONS contains the react contract: the string mentions parsing kind, that on 'zero' it must call author_home_card once with a single ask, and that it activates the board skill first. (A content assertion is the right test for prompt copy — assert the load-bearing phrases are present.)When your message contains kind:"zero" with a slug:
1) activate_skill("board");
2) read_board to see what's already there;
3) brief that home through THIS buyer's lens (concise);
4) author_home_card ONCE — body = the brief, plus exactly one
ask drawn from the buyer's strongest open question.
Never author more than one card or more than one question per zero.feat(advisor): react-beat prompt — author one card + one question on a zero.auth() in the Next route → the signed-in userId); the Worker endpoint POST {ADVISOR_WORKER_URL}/agents/advisor/${userId} gated by AUTH_PASSWORD (Bearer).userId — the SAME DO instance the doorbell wakes — without ever exposing AUTH_PASSWORD to the browser.userId server-side, and forwards to the DO with the Bearer. This unifies the chat DO with the doorbell DO (both keyed on userId) — the random web-xxxx id is retired.auth() → {userId:'u1'} and a fetch spy; POST a message to the route; assert it forwards to ${ADVISOR_WORKER_URL}/agents/advisor/u1 with the Bearer and the message body, and returns the streamed/JSON response. Assert a signed-OUT request → 401.const { userId } = auth(); if (!userId) return 401; then fetch(`${process.env.ADVISOR_WORKER_URL}/agents/advisor/${userId}`, { method:'POST', headers:{ Authorization:`Bearer ${process.env.AUTH_PASSWORD}`, 'Content-Type':'application/json' }, body: JSON.stringify({ message }) }) and pipe the response back./api/advisor/chat (drop the web-xxxx localStorage mint). Step 5. Run tests — PASS. Step 6. Commit: feat(advisor): chat keyed to authenticated userId via server proxy.homezero Worker + Convex; smoke with a live onboarding.Outcome: the advisor stands up its own Turso, seeds yearnings at birth, learns from 👍/👎, and shows a "what I'm learning about you" panel. Behavior starts to personalize.
sandbox into createAgent)
Test: homezero/flue/test/advisor/brainStore.test.ts + advisorFs.test.ts
Modify: homezero/flue/.dev.vars + wrangler.jsonc (advisor Turso group env)
store.ts pattern (lazy cloudflare:workers import; fns take c: Client so vitest injects :memory:); the Turso Platform API; Flue's SandboxFactory/SessionEnv seam (the sandbox field on createAgent).brainStore.ts: advisorDbName(userId) (sanitize → lowercase alnum/dash, ≤58), provisionAdvisorDb(userId) (idempotent group-create), getAdvisorTursoClient(userId), ensureBrainSchema(c), brain CRUD (upsertYearning, listYearnings, reweightYearning(c,key,delta), writeMetric, readParams(c,skill), writeParam), session-log (appendSessionEntries, readRecentSessions). advisorFs.ts: a SandboxFactory whose createSessionEnv returns a SessionEnv backed DIRECTLY by the Turso files table — readFile/writeFile/readdir/stat/exists/rm are queries, and exec answers grep/glob/find from Turso content/path queries. ONE filesystem — no buffer, no hydrate, no just-bash. Flue's NATIVE read/write/edit/grep/glob tools run straight on Turso; arbitrary scripting bash is unsupported (not needed). No bespoke fs tools.TURSO_API_TOKEN (provision), TURSO_ORG, TURSO_ADVISOR_GROUP, TURSO_ADVISOR_GROUP_TOKEN (db auth). URL DERIVED: libsql://${advisorDbName(userId)}-${TURSO_ORG}.turso.io — no per-buyer secret.c=createClient({url:':memory:'}); await ensureBrainSchema(c); assert tables files, yearnings, skill_metrics, skill_params, sessions exist (idempotent). Files round-trip: saveFiles(c,{'IDENTITY.md':'hi'}); expect((await loadFiles(c))['IDENTITY.md']).toBe('hi'). Brain: round-trip a yearning; reweightYearning(c,'town-vs-land',+0.3) clamps [0,1]. advisorDbName('user_2AbC') → valid Turso name.user_id column anywhere — the DB IS the user:
CREATE TABLE IF NOT EXISTS files( path TEXT PRIMARY KEY, content TEXT, updated_at TEXT); -- the filesystem CREATE TABLE IF NOT EXISTS yearnings( key TEXT PRIMARY KEY, prompt TEXT, confidence REAL DEFAULT 0.2, weight REAL DEFAULT 1, status TEXT DEFAULT 'open', source TEXT, last_touched TEXT); CREATE TABLE IF NOT EXISTS skill_metrics( skill TEXT, metric_key TEXT, value REAL, n INTEGER, window TEXT, updated_at TEXT, PRIMARY KEY(skill,metric_key,window)); CREATE TABLE IF NOT EXISTS skill_params( skill TEXT, param_key TEXT, value TEXT, updated_at TEXT, PRIMARY KEY(skill,param_key)); CREATE TABLE IF NOT EXISTS sessions( turn_id TEXT PRIMARY KEY, started_at TEXT, role TEXT, content TEXT); -- transcript mirrorThe SessionEnv's
writeFile upserts (INSERT … ON CONFLICT(path) DO UPDATE); readFile = SELECT content … WHERE path=?; readdir splits stored paths on / for the prefix.provisionAdvisorDb(userId) POSTs https://api.turso.tech/v1/organizations/${TURSO_ORG}/databases with { name: advisorDbName(userId), group: TURSO_ADVISOR_GROUP }, Bearer TURSO_API_TOKEN; HTTP 409 = success. getAdvisorTursoClient(userId) builds the URL + auth = TURSO_ADVISOR_GROUP_TOKEN. Test with a fetch spy.advisorFs.ts: a SandboxFactory whose createSessionEnv returns a SessionEnv over the buyer's brain client — readFile/writeFile/readdir/stat/exists/rm as queries. exec recognizes the grep tool's rg … command and the glob tool's find … command → runs SELECT path,content FROM files WHERE content LIKE/GLOB ? (or path GLOB), formats path:line:text; any other command → a clear "shell unsupported" ShellResult. Wire it: createAgent(() => ({ …, sandbox: advisorFs(userId) })) — lighting up native read/write/edit/grep/glob straight on Turso. Test: seed a files row → env.readFile returns it; env.exec('rg -- water .') over two files returns the matching path:line.turso group create homezero-advisors) + group token; set the four env values. Step 7. Commit: feat(advisor): per-buyer brain DB + Turso-backed sandbox under the native file tools.flue_* engine tables stay in the DO.provisionAdvisorDb, getAdvisorTursoClient, ensureBrainSchema, the brain client's files-table read/write, upsertYearning (Task 7); the onboarding answers in the birth message.IDENTITY.md into the files table (NOT the DO workspace), and seeds yearnings. isConfigured/check_identity/write_identity now read/write the IDENTITY.md row in files; seedYearnings(c, answers) (no userId column — the DB is the user).:memory: brain client: write_identity('# Who I am…') → the IDENTITY.md row in files holds it; check_identity → configured:…; empty db → unconfigured. Seed: assert the 7 universal-core keys (budget-comfort, town-vs-land, fixer-appetite, owned-water, timeline, who-its-for, commute) at confidence 0.2, status 'open' + a custom yearning from a non-core answer.identity.ts (IDENTITY_PATH='IDENTITY.md') from WorkspaceFileSystem to a files-table read/write on the brain client. Add seedYearnings(c, answers) to brainStore.ts (universal core as a constant {key,prompt}[]; known answers → custom yearnings). In finish_birth: await provisionAdvisorDb(userId); const c = await getAdvisorTursoClient(userId); await ensureBrainSchema(c); await seedYearnings(c, answers) (idempotent).feat(advisor): birth provisions the brain DB; IDENTITY + yearnings live in Turso.files table — so identity is externally readable from the brain db like everything else. The DO workspace is no longer the source of truth for advisor data.board.agentCards returns each card's ask.reactedAt/ask.answer (board.ts) — the reactions signal.brain_read (list yearnings/params), brain_write (upsert yearning / write param), read_reactions (derive 👍/👎 + answers from agentCards); native skills brain (the Turso playbook, reusable by the market) and reactions (signal→yearning-update playbook).:memory: client + a fake agentCards payload, assert read_reactions returns a normalized list ({slug, reaction:'up'|'down'|null, answered}) and brain_write persists a yearning reweight.userId, using the brain client resolved in-Worker / injected in test). Write brain/SKILL.md (when to read vs write, the schema, never expose raw private data to the buyer) and reactions/SKILL.md (a 👍 raises the relevant yearning's confidence, a 👎 lowers/flips it; an answer resolves it). Register both in advisor.ts skills array.feat(advisor): brain + reactions skills and tools.callConvex; yearnings (Task 7).internal.board.setLearningPanel({ userId, items: [{label, confidence}] }) + a project_learning tool that pushes a PUBLIC-SAFE confidence subset (labels + bars only, never the private prompt/why) for the board's "what I'm learning about you" panel.setLearningPanel with two items; query it back scoped to the user; assert only label+confidence are stored (no private fields).project_learning tool that maps open yearnings → {label, confidence} and calls it.feat(advisor): project public-safe learning panel to Convex.Outcome: the advisor runs on its own clock — hourly tend, nightly scout + dream — the session log mirrors to the brain DB, and the dream closes the self-improvement loop on the full transcript.
extend from @flue/runtime/cloudflare; base CloudflareAgentLike (onStart, scheduleEvery, schedule).scheduleEvery(3600,'tend'), and nightly schedule(cronString,'scout') + schedule(cronString,'dream') + schedule(cronString,'morningBrief') — plus stub methods that dispatch into the agent. Each beat method is named so the SDK can invoke it on alarm.scheduleEvery/schedule calls; run onStart(); assert scheduleEvery(3600,'tend') was registered and the three nightly beats scheduled. (Idempotency: calling onStart twice doesn't double-register — guard with a state flag.)export const cloudflare = extend({ base: (Base) => class extends Base { async onStart() { if (this.state?.rhythmOn) return; await this.scheduleEvery(3600, 'tend'); await this.schedule('0 13 * * *', 'scout'); // nightly UTC await this.schedule('30 13 * * *', 'dream'); await this.schedule('0 14 * * *', 'morningBrief'); this.setState({ ...this.state, rhythmOn: true }); } async tend(){ /* Task 12 */ } async scout(){ /* Task 13 */ } async dream(){ /* Task 14 */ } async morningBrief(){ /* Task 15 */ } }, });
onTurnEnd-style hook to the extension: read new rows from the DO's own flue_session_entries (via this.sql) since the last watermark, and appendSessionEntries(c, entries) into the buyer's Turso sessions table; advance the watermark in this.state. This gives the dream (Task 14) the full recent transcript AND makes it browsable from any libSQL client. Test the mirror against a fake this.sql returning two entries → assert both land in the Turso sessions table once (idempotent on the watermark).feat(advisor): rhythm via extend/scheduleEvery + session-log mirror to Turso.read/write/edit/grep) over the Turso fs (Task 7); read_board (Task 2).memory base skill (a SKILL.md) teaches the agent to keep JOURNAL.md with its native tools — read it, edit/write to append a tagged line, grep it to recall. The tend beat = a short housekeeping turn (read board, tidy clusters, append to JOURNAL.md what moved).:memory: brain client: simulate the agent's write then edit on JOURNAL.md (append a slug-tagged line); assert the files row holds both lines; assert env.exec('rg -- 123-main .') finds the tagged line. (No journal tool to test — exercise the native fs path.)skills/memory/SKILL.md (build-time base skill: what's worth journaling, tag entries by home slug, how to append with edit, the dream prunes). The tend() beat dispatches a bounded model turn with the memory + board skills available — the model uses native read/edit on JOURNAL.md.feat(advisor): tend beat + memory skill (journal via native file tools).TURSO_DATABASE_URL/_AUTH_TOKEN — both DOs share the Worker env; open a READ client on the market's listings); yearnings (Task 7); author_home_card (Task 2); board.agentCards to exclude already-on-board homes.scout_query() — structural rank of unzeroed market listings by active yearnings — and the scout beat that authors ≤N scout cards with placedBy:{by:'advisor'}. Phase-2 swaps ranking to vector_top_k.scout_query returns homes ranked by yearning fit, excludes slugs already on the board, and caps at N.scout_query opens a read client on the market db (injected in test), runs a structural SELECT scored against the buyer's open yearnings, filters out on-board slugs; write scout/SKILL.md (rank by yearnings, surface a few not many, cite the "why"). The scout() beat runs once nightly, authors the cards.feat(advisor): nightly scout beat reading the market brain.homezero-flue Worker.reweightYearning, writeMetric, writeParam, readParams, readRecentSessions); read_reactions (Task 9); the NATIVE file tools over Turso (Task 7).writeParam + rewrite the skill's PLAYBOOK.md using the native read/edit tools (it's a row in the files table); (3) summarize/prune JOURNAL.md with edit; (4) board re-org. NO bespoke playbook tools — the beats grep/read their <skill>/PLAYBOOK.md natively.board/PLAYBOOK.md files row + two sessions rows; assert readRecentSessions returns the transcript and the SessionEnv readFile('board/PLAYBOOK.md') returns the playbook for the dream to rewrite.dream() beat: readRecentSessions (full transcript since the last dream) + read_reactions → reweightYearning; compute the board skill's engagement metric → writeMetric + writeParam; dispatch a bounded model turn that rewrites each tunable skill's PLAYBOOK.md from its metric + the transcript using the native read+edit tools; summarize JOURNAL.md. The react/scout/tend prompts gain "grep/read your <skill> PLAYBOOK.md before acting".feat(advisor): nightly dream — read full session log, reweight yearnings, self-tune playbooks.sessions), tunes params + rewrites playbooks (Turso files) — the agent's whole learning loop lives in its one addressable db. No dependency on Flue runtime skill-rediscovery.Outcome: a simple daily WhatsApp morning briefing — one short, friendly check-in. Stubbed and tunable, board-card fallback if a buyer isn't WhatsApp-wired.
read_board, scout results, yearnings; the persona constraint (home shopper, light check-in, don't overwhelm).morningBrief beat that composes ONE short message — greeting → the single best overnight thing (one scout find OR one board change) → one gentle question → board link — and hands it to the channel (Task 16). Quiet night → a one-liner or skip.morning/SKILL.md (the tone + the one-focus rule + don't-overwhelm). morningBrief() = a bounded model turn that produces { text, skip }, then calls sendMorning(userId, text) (Task 16).feat(advisor): morning-brief beat composes a short daily check-in.author_home_card for the fallback; WhatsApp creds (env, gitignored).sendMorning(userId, text) — if the buyer has a number + opt-in, send via the WhatsApp Cloud API; else fall back to authoring a "good morning" board card so it always works.sendMorning POSTs to the WhatsApp API with the text. With a buyer-without-number, assert it calls author_home_card (the board fallback) instead and does NOT call WhatsApp.whatsapp.ts: resolve the buyer's number/opt-in; if present, fetch the Cloud API send endpoint with the env creds (a template-less session message is fine for the stub — note Meta's 24h-window/template requirement as a follow-up TODO in the code); else write the board card. Keep it simple — winged, tunable later.feat(advisor): simple WhatsApp morning send with board-card fallback.Execute with subagent-driven-development: a fresh implementer subagent per task (Opus), a task review (spec compliance + quality) after each, a broad whole-branch review at the end. Work on feature/advisor-loop in the homezero repo; open a PR, squash-merge per milestone (1A is independently shippable).
author_home_card, read_board, brain_read/write, read_reactions, scout_query, project_learning) + the NATIVE file tools (read/write/edit/grep/glob) for journal/playbooks, and Convex fns (board:authorHomeCard, board:pinAsk, board:agentCards, board:setLearningPanel, advisorDoorbell:ringAdvisorZero) are used identically across tasks.flue_session_entries for the session-log mirror (Task 11) reaches a Flue-internal table — pinned to beta.2; if its shape shifts, fall back to the agent appending its own turn summaries to the Turso sessions table.Market agent (Phase 2) is captured separately in the market spec — not built here.