On 2026-05-25 the Sleeper cognition stack underwent a single-day maintenance run that worked through all 18 briefs in ~/SLEEPER_TODO/SLEEPER_TODO.md. The run resolved four of the five rough edges enumerated in sleeper-cognition-stack, hardened the perimeter on the fifth, and added eleven new connectors and capabilities across ~/sfl-hook/, ~/chat/, ~/thoughts/, and ~/github/write/. The shape of the stack is materially different now: each capture surface has a typed home for its output, the parallel pollers have collapsed into a push pipeline with the cron as fallback, and the wiki schema is enforced at autocommit time so silent producer-consumer drift is caught before it ships.
sfl-hook stopped running Claude as a subprocess
The 153-line server.js that powered chat replies in SFL no longer spawns claude -p ... --dangerously-skip-permissions per request. It now calls the Anthropic SDK directly (claude-sonnet-4-6) with a cached system block. loadConfig fails closed on missing SFL_WEBHOOK_SECRET or ANTHROPIC_API_KEY (no more silent fail-open behaviour). The nginx location /sfl-hook block is restricted to Cloudflare egress IPs via cloudflare-allow.conf + deny all, with an exact-match carve-out for /sfl-hook/health so the iOS ConnectionTester can reach it from carrier IPs. Per-request structured logs land in ~/sfl-hook/data/requests.jsonl (10 MB rotation × 5 files); "did that reply land?" is tail -1. This was the closest the stack had to an arbitrary-code-execution path; it is gone.
Articles push to thoughts-sync; the cron is a safety net
sleeper-articles now fires POST /sync/ingest (fire-and-forget, Bearer-authed via ARTICLES_API_KEY) at every status='ready' transition — once for each of the three extractor paths (article, video, X post). thoughts-sync exposes the receiver on loopback port 3013 with GET /sync/health reporting last_push_at / last_cron_at / ideas_known. The original 5-minute cron stays, so a transient webhook failure self-heals on the next tick. Latency from "SFL bookmark accepted" to "raw/ written" dropped from up to 5 minutes to seconds. The two-parallel-SFL-pollers seam (sleeper-cognition-stack §1 fix #2) is resolved in the smallest-fix direction the survey recommended.
The wiki schema has an enforcement layer
~/thoughts/sync/check-schema.cjs validates wiki-index.json against the canonical wiki-index.schema.md on every autocommit cycle, immediately after build-wiki-index.cjs runs. The build chain — needs-refold.cjs → build-wiki-index.cjs → check-schema.cjs → git commit — refuses to commit when any required key is missing, any non-nullable field is null, or health.dead_links is non-empty. The producer also emits health.field_coverage to surface structural drift in the index itself. A deliberate-drift test (renaming summary to summary_text in the producer) was verified to block the commit; restoring the field unblocked it. The wiring contract is now explicit in the schema: chat-backend reads wiki-index.json off disk; remote consumers (iOS, Claude.ai) go through wiki-mcp at port 3007. Do not blur the two.
The fold-queue is observable
~/thoughts/sync/needs-refold.cjs runs first in the autocommit chain and emits sync/needs-refold.json listing every raw/ (and ~/Dropbox/raw/) entry that is either new to the .processed-raw-files ledger or whose recorded sha differs from the file's current sha. The build then injects health.refold_queue: {new, changed, missing} into wiki-index.json so the depth of unprocessed work is visible at-a-glance via the same retrieval path the chat backend already reads. At run-end the queue was 204 entries deep — confirming the survey's ~195-entry estimate from sleeper-cognition-stack §1 fix #5.
Each capture surface now has a typed downstream home
The biggest behavioural shift. Three new auto-routing pipelines turn captured material into structured rows the rest of the stack can find:
- Articles → tasks: when an article transitions to
starred=true AND read=true, a second LLM pass over the body proposes 0-5 verb-led actions.surface='task'actions createsleeper-tasksdrafts withresponsible=petteranddoc.source_article_idback-references;surface='sfl_meta'actions post to the SFL Worker against a named project. Off viaACTION_EXTRACTION_ENABLED=false. - Tasks → chat: when
sleeper-tasksfires its existing APNs push for a sleeper comment, handoff to petter, or task completion, it also inserts a loopback-onlyassistant-role message into the chat thread carrying thewordly_id+ event kind. The chat backend's/messages/systemendpoint is gated at both the express middleware (loopback +X-Internal: 1) and the nginx perimeter (deny all). Off by default viaTASK_TO_CHAT_ENABLED. - Write → everywhere:
POST /write/api/routeclassifies every Write document into{task, article-pointer, wiki-fold, memory-fact, sfl-meta, note, keep-as-document}with confidence + rationale; write-sync's push hook auto-dispatches when confidence ≥ 0.85, replacing the doc body with a four-line pointer to the destination (sleeper-tasks, sleeper-articles,~/thoughts/raw/, sleeper-memory). The original body stays indocument_revisionsfor undo. Off by default viaROUTING_ENABLED.
Three killswitches default-off so Petter trusts the new behaviour before flipping each on. All three were live-verified end-to-end before commit, then test artifacts cleaned up.
The chat agent has new MCP, new search, new memory affordances
articles-mcpon port 3009 (3008 is signal-cli) exposesarticles_search,articles_get_by_id,articles_recentso the chat agent can drill into the full markdown body of a saved article instead of riffing from the 240-char context summary. Registered with the localclaudeCLI assleeper-articles.GET /find?q=...and thesleepy findCLI verb expose the existing in-process RRF+rerank pipeline across all six corpora (wiki, memory, chat, tasks, articles, sfl), tracking per-corpus failures asdegraded[].- After every assistant reply, the result of the fire-and-forget
POST /memory/extractis captured into a newextracted_memoriesJSON column on the assistant message. On the next turn, those{id, kind, text}entries are prepended to the prompt with a tightened tool hint routing "forget that" / "drop that fact" tosleeper-memory forget <id>. - Assistant message rows now carry a
citationsJSON column distilled from the same retrieval blocks that get embedded in the prompt. Server-side; iOS chip rendering is the remaining half, blocked on the Xcode toolchain.
The daily brief is real
POST /briefs/generate + GET /briefs/today + a 06:30 node-cron gather signal from articles (last-24h starred-unread), tasks (todo aging >7d), memory (rows touched last 24h), the wiki-index (articles whose mtime moved), and the local messages table, then compose a 6-section Markdown brief via claude-sonnet-4-6 with persona prefix + cached system block. Empty sections are omitted. After generation it fires POST sleeper-tasks /notify for the silent-refresh APNs push that iOS already handles. The iOS side currently still mocks; swapping to a real client is a one-file change on the Mac.
Write gained two AI surfaces beyond /feedback
POST /write/api/history-diffreturns a short Claude-written summary of what changed between twodocument_revisionsrows for one path. One non-streamed call, cached system block, audit row per call.POST /write/api/relatedtakes a{wordly_id | path, max?}and returns the handful of other synced docs most worth opening next — in-process cosine-on-TF picks3×maxcandidates from the live sync corpus, one Claude rank pass writes a ≤12-word reason per pick, hallucinated wordly_ids are dropped.
Both go through the shared read-only corpus.ts module so write-sync remains the single writer.
What did not get done
YouTube transcripts (G11) have full wiring — youtube-transcript.ts wrapper, poller integration, backfill script — but the upstream youtube-transcript npm library currently returns "transcript disabled" for nearly every video because YouTube tightened anti-scraping. The graceful fallback path is verified; transcripts will start landing without further changes once the upstream stabilises or a working library arrives. The SFL repo audit (G4) filed three follow-up sfl meta entries against the dormant sfl project for Petter to triage — the verdict ("neglected, production stable, no near-term roadmap") sits in ~/github/sfl/STATUS.md with a one-line edit caveat. The iOS halves of G6 (real brief client) and G9 (citation chips) remain blocked on the Xcode toolchain. SFL-meta dispatch from G18 is recognised by the classifier but not auto-dispatched in v1 — coordination with the SFL Worker's auth is deferred; sfl meta add still works.
How the run was scoped
The 18 briefs were accepted in their priority order and worked through one at a time, each with build / live-verify / commit / push before moving on. D0 in the run's decision log collapsed the standard per-goal Frame/Plan/Starter/Critic ceremony because the SLEEPER_TODO briefs are themselves pre-framed (Goal / Why / Context / Concrete proposal / Files / Success criteria); the trade-off — losing the adversarial cross-check — was offset by close-gate code review and live verification on every shipped goal. Total commits across phareim/sfl-hook, phareim/sleeper, phareim/sleeper-chat, phareim/sfl, phareim/write, and phareim/thoughts: 27. All pushed to origin.