Build plan for the spec. AutoDream 4-phase consolidation + a Karpathy wiki for the advisor and the market agent, grounded in Flue + Cloudflare. · 2026-06-26
For agentic workers: execute task-by-task with superpowers:subagent-driven-development — fresh subagent per task, review between. Steps are ☐ checkboxes. Every code step shows real code; every test step shows the command + expected result. Pin model opus on every implementer and reviewer (per Pete's standing rule). The plan covers all six spec phases (A–F) in one document; ship and review phase by phase.
Goal: Give both HomeZero agents a nightly four-phase dream that maintains a private, backlinked wiki of what each is learning — the advisor about a buyer's search, the market about a place fed by many signal-gated sources — with everything public rendered as a projection of a wiki page.
Architecture: Extract the generic memory machinery into a shared src/brain/ module both agents import (separate tables in separate DOs — shared code, never shared data). Each agent's wiki lives in its DO's native SQLite files table, reached via Flue's SandboxApi (native read/write/edit/grep). The dream reads the day from Flue's durable event stream (readEvents) and rewrites pages + reweights yearnings. The market additionally ingests in two tiers — broad cheap stubs vs deep per-property enrichment gated on attention.
Tech stack: Flue beta.5 (@flue/runtime)Cloudflare Durable Objects + DO SQLitevalibotvitest + node:sqliteConvex (projections)
Every task's requirements implicitly include these. Exact values, copied from the spec + repo.
EventStreamStore.readEvents(path, {offset, limit}), path agents/<type>/<id>. NEVER read the internal flue_session_entries table (the drift that bricked v6 DOs).files table via the SandboxApi (native read/write/edit/grep/glob). No external store. The wiki = wiki/index.md + wiki/<slug>.md pages.Sql (src/sqlh.ts): synchronous sql.exec(query, ...binds).toArray(), VARIADIC binds. Tests inject fakeSqlStorage() (test/helpers/fakeSqlStorage.ts, node:sqlite).src/brain/ holds only generic mechanism; each agent keeps its own seeds + body prose. No agent ever reads another's tables.listings.verdict/valueFactors/deepDive, boardCards, market_reports, learningPanels.read. The wiki is additive; publishers render from it.npx vitest run in flue/ (single file: npx vitest run test/<path>); the suite is green today (233/233) — keep it green. Deploy: npx flue build --target cloudflare → npx wrangler deploy -c dist/homezero_flue/wrangler.json.flue/, a Worker).src/brain/ + index→pages + session-read + validate advisor dream (Tasks 1–9)Extract the generic memory machinery out of src/advisor/ into src/brain/ with zero advisor behavior change (pure refactor, tests stay green), then add the new wiki + session-read primitives, migrate the advisor to index→pages, and validate its dream runs live.
src/brain/files.ts (files-table helpers)src/brain/files.tssrc/advisor/brainStore.ts (re-export from new module)test/brain/files.test.tswriteFileRow/readFileRow/listFilePaths/deleteFileRow/fileExists/searchFiles/saveFiles/loadFiles(sql, …) — identical signatures to today's brainStore.ts versions.brainStore.ts:419–516) verbatim into src/brain/files.ts, keeping import type { Sql } from '../sqlh'.brainStore.ts, replace the moved bodies with export { writeFileRow, readFileRow, listFilePaths, deleteFileRow, fileExists, searchFiles, saveFiles, loadFiles } from '../brain/files';test/brain/files.test.ts: fakeSqlStorage() → create the files table, round-trip writeFileRow/readFileRow, assert searchFiles returns 1-based line hits and listFilePaths('notes') prefix-scoping.npx vitest run test/brain/files.test.ts test/advisor — Expected: PASS (advisor suite unchanged).refactor(brain): extract files-table helpers to src/brain/files.tssrc/brain/yearnings.ts (generic yearning CRUD)src/brain/yearnings.tssrc/advisor/brainStore.ts (re-export; keep advisor-specific UNIVERSAL_CORE_YEARNINGS + seedYearnings)test/brain/yearnings.test.tsYearning, UpsertYearning, upsertYearning, listYearnings, reweightYearning, and new generic seedCoreYearnings(sql, core: ReadonlyArray<{key;prompt}>, now?). Consumes: nothing new.Yearning, UpsertYearning, upsertYearning, listYearnings, reweightYearning (brainStore.ts:28–137, 287–316) into src/brain/yearnings.ts.seedYearnings):
// Seed/refresh a fixed core list idempotently: confidence/status set only on a
// brand-new key; on re-seed they're omitted so upsertYearning's COALESCE preserves
// any value the agent has since reweighted. source defaults to 'core'.
export async function seedCoreYearnings(
sql: Sql,
core: ReadonlyArray<{ key: string; prompt: string }>,
now: string = new Date().toISOString(),
): Promise<void> {
const existing = new Set(sql.exec(`SELECT key FROM yearnings`).toArray().map((r) => String(r.key)));
for (const c of core) {
const fresh = !existing.has(c.key);
await upsertYearning(sql, { key: c.key, prompt: c.prompt, source: 'core',
...(fresh ? { confidence: 0.2, status: 'open' } : {}) }, now);
}
}brainStore.ts re-export the moved symbols; rewrite advisor seedYearnings to call seedCoreYearnings(sql, UNIVERSAL_CORE_YEARNINGS, now) then run its existing onboarding-answers parsing (keep that block as-is — it's advisor-specific).test/brain/yearnings.test.ts: seed a 2-key core twice with a reweightYearning between; assert no duplicates and the reweighted confidence survives re-seed (mirror birth-brain.test.ts's idempotency case).npx vitest run test/brain/yearnings.test.ts test/advisor/birth-brain.test.ts — Expected: PASS.refactor(brain): extract yearning CRUD + seedCoreYearningssrc/brain/schema.ts (ensureBrainSchema)src/brain/schema.tssrc/advisor/brainStore.ts, src/agents/advisor.ts (import path only)ensureBrainSchema(sql) creating files, yearnings, skill_metrics, skill_params, sessions (verbatim DDL from brainStore.ts:49–90).ensureBrainSchema into src/brain/schema.ts; re-export from brainStore.ts.src/agents/advisor.ts:100 (onStart → ensureBrainSchema(sql)) working via the re-export (no edit needed; optionally repoint the import).npx vitest run test/advisor — Expected: PASS.refactor(brain): extract ensureBrainSchema to src/brain/schema.tssrc/brain/sandbox.ts (makeBrainSandboxApi) + brainTools.tssrc/brain/sandbox.ts, src/brain/brainTools.tssrc/advisor/advisorFs.ts, src/advisor/tools.ts (delegate)test/brain/sandbox.test.tsmakeBrainSandboxApi(sql): SandboxApi (today's makeAdvisorSandboxApi body, verbatim — it's already generic); makeBrainTools(getClient, agentId): ToolDefinition[] returning [brain_read, brain_write] from tools.ts:277–342. (T8 widens this to makeBrainTools(getClient, agentType, agentId) and adds recall_session; this task lands the 2-arg form and updates the advisor call site, T8 migrates both.)makeAdvisorSandboxApi + its path/grep/glob helpers (advisorFs.ts:38–251) into src/brain/sandbox.ts as makeBrainSandboxApi; advisorFs.ts re-exports makeAdvisorSandboxApi = makeBrainSandboxApi and keeps its advisorFs(userId) factory.brain_read/brain_write definitions into a makeBrainTools(getBrainClient, agentId) factory in src/brain/brainTools.ts (lift verbatim from tools.ts:277–342, swapping the closure's ctx.userId for the passed agentId). makeAdvisorTools spreads makeBrainTools(getBrainClient, ctx.userId) into its returned array in place of the inline defs.test/brain/sandbox.test.ts: over fakeSqlStorage() + the files table, write IDENTITY.md via writeFile and read it back; assert exec("rg -- water .") returns path:line:text.npx vitest run test/brain test/advisor/scout.test.ts test/agent.test.ts — Expected: PASS (tool names unchanged: the agent.test.ts tool-name list still matches).refactor(brain): extract sandbox + brain_read/brain_write toolssrc/brain/wiki.ts — wiki primitives over the files tablesrc/brain/wiki.tstest/brain/wiki.test.tswriteFileRow/readFileRow/listFilePaths (Task 1). Produces: writeWikiPage(sql, {slug,title,body,links}), readWikiPage(sql, slug), listWikiSlugs(sql), addBacklink(sql, slug, linkTo), rebuildIndex(sql): Promise<string>; consts WIKI_PREFIX='wiki', WIKI_INDEX='wiki/index.md'.import type { Sql } from '../sqlh';
import { writeFileRow, readFileRow, listFilePaths } from './files';
export const WIKI_PREFIX = 'wiki';
export const WIKI_INDEX = `${WIKI_PREFIX}/index.md`;
export interface WikiPage { slug: string; title: string; body: string; links?: string[]; }
const pagePath = (slug: string) => `${WIKI_PREFIX}/${slug}.md`;
export async function writeWikiPage(sql: Sql, p: WikiPage, now = new Date().toISOString()) {
const links = (p.links ?? []).filter(Boolean);
const linkLine = links.length ? `links: ${links.map((s) => `[[${s}]]`).join(' ')}\n` : '';
await writeFileRow(sql, pagePath(p.slug), `# ${p.title}\nslug: ${p.slug}\n${linkLine}\n${p.body}\n`, now);
}
export const readWikiPage = (sql: Sql, slug: string) => readFileRow(sql, pagePath(slug));
export async function listWikiSlugs(sql: Sql): Promise<string[]> {
return (await listFilePaths(sql, WIKI_PREFIX))
.filter((p) => p !== WIKI_INDEX && p.endsWith('.md'))
.map((p) => p.slice(WIKI_PREFIX.length + 1, -3));
}
export async function addBacklink(sql: Sql, slug: string, linkTo: string, now = new Date().toISOString()) {
const body = await readWikiPage(sql, slug);
if (body === null || body.includes(`[[${linkTo}]]`)) return;
await writeFileRow(sql, pagePath(slug), `${body.trimEnd()}\nlinks: [[${linkTo}]]\n`, now);
}
export async function rebuildIndex(sql: Sql, now = new Date().toISOString()): Promise<string> {
const slugs = (await listWikiSlugs(sql)).sort();
const lines = ['# Wiki index', ''];
for (const slug of slugs) {
const body = (await readWikiPage(sql, slug)) ?? '';
const t = body.match(/^#\s+(.+)$/m);
lines.push(`- [[${slug}]] ${t ? t[1].trim() : slug}`);
}
const content = lines.join('\n') + '\n';
await writeFileRow(sql, WIKI_INDEX, content, now);
return content;
}test/brain/wiki.test.ts: write two pages with a link; assert listWikiSlugs excludes index, addBacklink is idempotent, and rebuildIndex emits one - [[slug]] Title line per page and excludes the index itself.npx vitest run test/brain/wiki.test.ts — Expected: PASS.feat(brain): wiki primitives (pages, backlinks, lean index)src/brain/session.ts — read the durable event streamsrc/brain/session.tstest/brain/session.test.tssessionStreamPath(agentType, id): string = `agents/${agentType}/${id}`; readSessionEvents(sql, agentType, id, opts?): Promise<unknown[]> via Flue's EventStreamStore.readEvents.agents/<type>/<id> over the DO's own ctx.storage.sql returns the recorded conversation. The d.mts shows SqliteEventStreamStore(sql) with readEvents(path,{offset,limit}) → {events?,nextOffset,closed}; verify it's reachable from @flue/runtime (or its subpath) and pin the import before coding against it. The public signature below is fixed; only the construction line is finalized at this step.fakeSqlStorage(), createStream + appendEvent a couple events, then readEvents(path,{offset:'-1'}) and confirm the round-trip + the export path. Record the confirmed import.import type { Sql } from '../sqlh';
import { SqliteEventStreamStore } from '@flue/runtime'; // pinned in the spike
export const sessionStreamPath = (agentType: string, id: string) => `agents/${agentType}/${id}`;
// Read up to `limit` events from this agent's OWN durable stream. The store reads
// the same SQLite Flue persists the conversation to; we never touch flue_session_entries.
export async function readSessionEvents(
sql: Sql, agentType: string, id: string,
opts?: { offset?: string; limit?: number },
): Promise<unknown[]> {
const store = new SqliteEventStreamStore(sql as never);
const res = await store.readEvents(sessionStreamPath(agentType, id), {
offset: opts?.offset ?? '-1', limit: opts?.limit ?? 100,
});
return (res as { events?: unknown[] }).events ?? [];
}test/brain/session.test.ts: round-trip via the store as in the spike; assert readSessionEvents returns the appended events and sessionStreamPath('advisor','u1') === 'agents/advisor/u1'.npx vitest run test/brain/session.test.ts — Expected: PASS.feat(brain): read the durable event stream (readEvents wrapper)src/brain/contract.ts — the 4-phase DREAM prosesrc/brain/contract.tstest/brain/contract.test.tsDREAM_CONTRACT: string — the shared four-phase instruction (orient · gather signal via the session stream · consolidate the wiki + reconcile yearnings · prune&index = lint). Each agent's prompt embeds it, then appends body-specific lines.DREAM_CONTRACT as a joined-string block stating the four phases verbatim from spec §4: (1) Orient — read wiki/index.md + recently-touched pages; (2) Gather signal — read the day from the durable session (the agent's recall_session/native read), plus touched pages; (3) Consolidate — author/update wiki pages, weave backlinks, reconcile yearnings via brain_write (small defensible moves, ≤1 new); (4) Prune & index — rebuild wiki/index.md, flag contradictions/orphans/stale, sign with one dated line. Private: no cards, no messages.test/brain/contract.test.ts: assert DREAM_CONTRACT contains "Orient", "Gather signal", "Consolidate", "Prune", "wiki/index.md", and "no board cards".npx vitest run test/brain/contract.test.ts — Expected: PASS.feat(brain): shared 4-phase DREAM contract prosesrc/advisor/prompts.ts (DREAM PASS → embed DREAM_CONTRACT), src/advisor/skills/memory/SKILL.md (index→pages discipline)src/agents/advisor.ts (add recall_session tool wired to readSessionEvents)test/advisor/dream.test.ts (still green), test/agent.test.ts (tool list += recall_session)DREAM_CONTRACT (T7), readSessionEvents (T6), rebuildIndex/writeWikiPage (T5). Produces: advisor dream that reads the session stream, rewrites wiki/ pages + wiki/index.md, reconciles yearnings, writes project_read.prompts.ts:71–86) with one that embeds DREAM_CONTRACT and adds the advisor body lines: pages are buyer themes/areas/homes; the public read is project_read. Update the memory SKILL.md to describe wiki/index.md + pages (replacing the monolithic JOURNAL.md guidance) and note the dream is the one rewrite pass.recall_session tool to makeBrainTools (so both agents get it): input v.object({ limit: v.optional(v.number()) }), run → JSON.stringify(await readSessionEvents(await getBrainClient(agentId), agentType, agentId, { limit })). Pass agentType into makeBrainTools.test/agent.test.ts's expected tool-name array to include recall_session (keep it sorted).npx vitest run test/advisor test/agent.test.ts — Expected: PASS (the dispatch test is unaffected; the factory test reflects the new tool).feat(advisor): 4-phase dream over an index→pages wiki + recall_sessionnpx flue build --target cloudflare && npx wrangler deploy -c dist/homezero_flue/wrangler.json.POST /agents/advisor/<userId> with Authorization: Bearer $AUTH_PASSWORD, body {"message":"{\"kind\":\"dream\"}"} (read AUTH_PASSWORD from flue/.dev.vars; never echo it).wiki/index.md rebuilt, ≥1 wiki page updated, ≥1 yearning last_touched advanced, learningPanels.read refreshed in Convex. Harden the empty-wiki first-dream path in the prompt if the run thrashes.fix(advisor): guard the empty-wiki first dreamsrc/advisor/prompts.ts (TEND/SCOUT/ZERO-WAKE passes)src/advisor/skills/memory/SKILL.mdwiki/<area|theme|home>.md pages with backlinks, instead of appending to a single journal.prompts.ts:40–69) so step "append a JOURNAL line" becomes "update the relevant wiki page (home/area/theme) and backlink it" — using the native write/edit tools over wiki/<slug>.md. Keep slug-tagging.npx vitest run test/advisor test/agent.test.ts — Expected: PASS (prose-only change; the factory/dispatch tests are unaffected).feat(advisor): working beats ingest the search wikisrc/advisor/prompts.ts (author_home_card + project_read guidance)body and project_read summary are drawn from the home's wiki page + the index — same Convex shapes (board:authorHomeCard, board:setRead), now grounded in the wiki.author_home_card, and project_read after, summarizing the index — explicitly public-safe (no private page internals pasted).npx vitest run test/advisor — Expected: PASS.feat(advisor): render board card + read from wiki pagessrc/agents/market.ts (onStart → ensureBrainSchema(this.ctx.storage.sql))test/market/brain-schema.test.tsensureBrainSchema (T3). Produces: the market DO's SQLite now also has files + yearnings (alongside listings/parcels/...), co-resident in one DO.market.ts onStart (before the arm-once guard, matching the advisor pattern), call await ensureBrainSchema(this.ingestSql()) so the brain tables exist on every boot.test/market/brain-schema.test.ts: over fakeSqlStorage(), run ensureBrainSchema + the market ensureSchema; query sqlite_master and assert files, yearnings, listings, market_reports all present.npx vitest run test/market/brain-schema.test.ts — Expected: PASS.feat(market): brain tables (files + yearnings) on the market DOsrc/market/yearnings.ts (MARKET_CORE_YEARNINGS)src/agents/market.ts (seed in onStart, idempotent)test/market/yearnings.test.tsseedCoreYearnings (T2). Produces: the 6 place-yearnings (price-drivers, moving-areas, seasonality, honest-vs-over, supply-trajectory, whats-changing) at confidence 0.2, idempotent across boots.MARKET_CORE_YEARNINGS: ReadonlyArray<{key;prompt}> with the six keys + prompts from spec §5.market.ts onStart, after ensureBrainSchema: await seedCoreYearnings(this.ingestSql(), MARKET_CORE_YEARNINGS).test/market/yearnings.test.ts: seed twice with a reweightYearning between; assert 6 yearnings, no dupes, reweight preserved.npx vitest run test/market/yearnings.test.ts — Expected: PASS.feat(market): seed 6 place-yearnings at provisioningsrc/market/marketFs.ts (thin wrapper over makeBrainSandboxApi)src/agents/market.ts (add sandbox, makeBrainTools, skills)src/market/skills/memory/SKILL.md, src/market/skills/brain/SKILL.mdtest/market/agent-shape.test.tsmakeBrainSandboxApi (T4), makeBrainTools (T4), wiki tools. Produces: the market agent now has the native file tools over its files table, brain_read/brain_write/recall_session, and memory+brain skills.marketFs(slug): SandboxFactory mirroring advisorFs (resolve the DO sql via the market's context; reuse makeBrainSandboxApi).defineAgent config (market.ts:58–63): add sandbox: marketFs(ctx.id), spread makeBrainTools(getMarketBrainClient, 'market', ctx.id) into tools, and skills: [marketMemorySkill, marketBrainSkill] (write the two SKILL.md files; brain SKILL describes place-yearnings, memory SKILL describes the wiki).test/market/agent-shape.test.ts mirroring test/agent.test.ts: assert the new tool names appear and the instructions still contain the sweep/drain/metrics kinds.npx vitest run test/market/agent-shape.test.ts — Expected: PASS.feat(market): native FS, brain tools, memory+brain skillssrc/market/prompts.ts (sweep/drain/metrics each end by noting what they saw)wiki/log.md or the relevant page) of what changed.MARKET_INSTRUCTIONS a closing step: "note what you observed this pass as a dated, slug-tagged line via your native edit tool (the dream consolidates it)."npx vitest run test/market — Expected: PASS (prose-only).feat(market): working beats journal for the dreamingestSource(kind, tier, payload)src/market/ingest/source.ts (registry + dispatcher)test/market/ingest-source.test.tstype Tier = 1 | 2; interface Connector { kind: string; tier: Tier; fetch(ref): Promise<Record<string,unknown>>; weave(sql, slug, data): Promise<void> }; registerConnector(c); ingestSource(sql, kind, ref): Promise<void> (looks up the connector, fetches, weaves into the wiki page ref.slug).fetch/weave are per-connector). ingestSource resolves the connector by kind, calls fetch(ref), then weave(sql, ref.slug, data) (which calls writeWikiPage/addBacklink).test/market/ingest-source.test.ts: register a fake connector whose fetch returns {n:1} and weave writes a page; assert ingestSource produces the page and that an unknown kind throws.npx vitest run test/market/ingest-source.test.ts — Expected: PASS.feat(market): source-agnostic connector seam (ingestSource)src/market/prompts.ts (sweep), src/market/tools.ts (a stub_wiki helper or extend store_listings)test/market/tier1-stub.test.tswriteWikiPage (T5). Produces: after a sweep, every for-sale listing has a thin wiki/<slug>.md stub (raw Zillow facts, not yet woven) — cheap, broad.stubListingPages(sql, listings) helper that writes a stub page per swept listing (title = address, body = price/beds/baths/lot, no backlinks). Call it from store_listings (or a new step in the sweep flow).test/market/tier1-stub.test.ts: run the helper over 2 fake listings; assert two wiki/<slug>.md rows exist with the raw facts.npx vitest run test/market/tier1-stub.test.ts — Expected: PASS.feat(market): Tier-1 sweep stubs a wiki page per listingsrc/market/ingest/deep.ts (deepIngest(sql, zpid, opts))src/market/prompts.ts (drain), src/market/tools.ts (a deep_ingest tool the drain calls)test/market/tier2-gate.test.tsingestSource (T16), the day-one connectors (T19–21). Produces: deepIngest runs the Tier-2 connectors for ONE property and weaves its page; a per-pass budget caps calls. The gate: only invoked for a zeroed listing (drain), never broadly.deepIngest(sql, zpid, { slug, budget }): for each registered Tier-2 connector, if budget remains, await ingestSource(sql, kind, { zpid, slug }); decrement budget; tally. Expose a deep_ingest tool (input v.object({ zpid: v.string() })) the drain calls before publish_briefing.deep_ingest({zpid}) → then publish_briefing renders verdict/valueFactors from the now-woven page.test/market/tier2-gate.test.ts: register a spy Tier-2 connector; run a sweep (Tier-1) over 3 listings and assert the connector's fetch was NOT called; then call deepIngest for one zpid and assert it WAS called exactly once and the budget bounded it.npx vitest run test/market/tier2-gate.test.ts — Expected: PASS.feat(market): signal-gated Tier-2 deep ingest on drainsrc/market/ingest/connectors/county.tstest/market/connectors/county.test.tsConnector (kind:'county') whose fetch({zpid,slug}) resolves the parcel + pulls sale history/assessed/zoning (direct Worker fetch to Delta EagleWeb), weave appends a "County record" section + backlinks comparable sales.const ENDPOINT + a parse fn), reusing the egress path proven for parcels. The test mocks fetch — no network in CI.county connector: fetch takes an injectable doFetch = globalThis.fetch, hits the parcel/sales endpoint, returns { saleHistory: [{date,price}], assessed, zoning, acres }; weave writes those into the listing's wiki page and adds [[<comparable-slug>]] links for recent nearby sales.test/market/connectors/county.test.ts: inject a fake doFetch returning a canned EagleWeb payload; assert the page gains real sale prices and a comp backlink.npx vitest run test/market/connectors/county.test.ts — Expected: PASS.feat(market): county records connector (real sale comps)src/market/ingest/connectors/maps.ts, .../water.tstest/market/connectors/maps.test.ts, .../water.test.tsmaps (kind:'maps'): drive-times + nearby (Places API, MAPS secret) → page "Proximity" section. water (kind:'water'): CDSS REST decree/priority/irrigation → page "Water rights" section + reweight owned-water-relevant notes.fetch shape as Task 19. maps.fetch reads the API key from env (MAPS); water.fetch hits the CDSS structures/water-rights REST endpoint by location. Each weave writes its section to the listing's wiki page.doFetch returning a canned payload; assert the page gains the Proximity / Water section. (Endpoints confirmed against live APIs at impl; tests are offline.)npx vitest run test/market/connectors — Expected: PASS.feat(market): Maps + Colorado water-rights connectorssrc/market/ingest/connectors/news.ts (Tier-1 market-wide + Tier-2 property mentions), .../civic.ts (Tier-1)test/market/connectors/news.test.ts, .../civic.test.tsnews ingests local-paper items into a market-wide wiki/news.md theme page (Tier 1) and links property/street mentions onto listing pages (Tier 2); civic ingests council/county agenda items into wiki/whats-changing.md.doFetch (RSS/scrape; per-connector isolation — a failing source degrades to "no enrichment," never throws out of ingestSource). Browser-Render is the fallback for scrape-hostile sites (note in the connector, wire later).npx vitest run test/market/connectors/news.test.ts test/market/connectors/civic.test.ts — Expected: PASS.feat(market): local news + civic-minutes connectorsdream() beat + schedulesrc/agents/market.ts (onStart schedule + dream() method)test/market/dream-beat.test.tsadvisor.dream()). Produces: dream() → dispatch({agent:'market', id, input:{kind:'dream'}}); onStart schedules 0 12 * * * under the arm-once guard.onStart, add await this.schedule('0 12 * * *', 'dream'); (after the metrics schedule, inside the arm-once guard). Add the dream() method mirroring advisor.ts:126–133 with agent:'market'.test/market/dream-beat.test.ts mirroring test/advisor/dream.test.ts (FakeBase + vi.mock('@flue/runtime') dispatch spy + getDurableObjectIdentity mock); assert one dispatch with {agent:'market', id, input:{kind:'dream'}}.npx vitest run test/market/dream-beat.test.ts — Expected: PASS.feat(market): nightly dream beat (0 12 UTC)src/market/hygiene.ts (pruneStaleListings, clearResolvedPending)test/market/hygiene.test.tspruneStaleListings(sql, now): Promise<number> deletes homeStatus != 'FOR_SALE' AND last_seen_at < now-30d; clearResolvedPending(sql): Promise<number> deletes pending_resolution rows with a matching parcel_listings link. Both return the deleted count.test/market/hygiene.test.ts: seed a FOR_SALE row, a 31-day-stale off-market row, a resolved pending + an unresolved pending; assert prune keeps FOR_SALE, deletes the stale, clears the resolved, keeps the unresolved.npx vitest run test/market/hygiene.test.ts — Expected: PASS.feat(market): guarded raw-data hygiene SQLsrc/market/prompts.ts (add a kind:"dream" block embedding DREAM_CONTRACT)src/market/tools.ts (expose a prune_brain tool wrapping T23)test/market/agent-shape.test.ts (instructions contain "dream")DREAM_CONTRACT (T7), hygiene (T23). Produces: the market dream — orient/gather (read the session stream)/consolidate the place wiki + reconcile place-yearnings/prune&index, then call prune_brain (the hygiene) and sign with a tally line.kind:"dream" branch to MARKET_INSTRUCTIONS: embed DREAM_CONTRACT, add market body lines (pages are listings/neighborhoods/themes/news; the private market-read feeds the next metrics beat), and a final step "call prune_brain and record the prune tally in the dream's index line."prune_brain tool (input v.object({})) → runs pruneStaleListings + clearResolvedPending, returns the counts string.test/market/agent-shape.test.ts: instructions contain "dream", and prune_brain is in the tool list.npx vitest run test/market — Expected: PASS.feat(market): DREAM PASS — 4-phase + lint + hygienetest/brain/contract.test.ts (both prompts embed DREAM_CONTRACT)ADVISOR_INSTRUCTIONS and MARKET_INSTRUCTIONS contain DREAM_CONTRACT — so the shared four phases can't silently drift apart.DREAM_CONTRACT; assert each includes it.npx vitest run test/brain/contract.test.ts — Expected: PASS.test(brain): both agents embed the shared dream contractnpx vitest run — Expected: all green (≥ the 233 baseline + the new tests).npx flue build --target cloudflare && npx wrangler deploy -c dist/homezero_flue/wrangler.json.sweep then a drain on a zeroed listing (confirm Tier-2 fired only on the zeroed one), then trigger {"kind":"dream"} on the market DO.wiki/index.md rebuilt lean, ≥1 listing page woven with comps backlinks, ≥1 place-yearning moved, the prune tally line written, and the next metrics read reflects the dream's private take.| Check | Result |
|---|---|
| Spec coverage | §2 memory→Tasks 1–8; §4 four phases→T7,8,24; §5 two wikis→T8,10,13,17; §6 ingest+gate→T16–21; §7 reconcile/projections→T11,17,18; §8 shared module→T1–5; §9 build phases→all; §10 validation→T26,27. No uncovered section. |
| Type consistency | Sql, SandboxApi, Connector/Tier, WikiPage, seedCoreYearnings, readSessionEvents, deepIngest used identically across tasks. makeBrainTools(getClient, agentType, agentId) arity fixed at T8 and reused at T14. |
| Known-at-impl flagged | T6 (event-stream construction) and the connector endpoints (T19–21) are the only externally-verified bits — each carries a verification step + an offline mocked-fetch test. No logic placeholders. |