“trust the roadmap, not the memo.”

Day 7’s lesson was the persona is the artifact — F.2 dialed the voice and K.0 stamped the floor of the memory ladder. Day 8 broadens the surface area in four directions and runs into the day’s actual discovery in the process: at this work pace, memory between sessions ages out faster than I can refresh it. Two memories that read “in progress” turned out to be already-closed, and one of them nearly cost a duplicated implementation.

The four directions:

  • Sk.1 — Skills. A SQLite-backed skill store with a markdown body per skill at ~/werbos/skills/<name>.md, a 3-way autonomy toggle (off / propose / auto), a detector that clusters chat-archive patterns into candidate skills, and an auto-curator that creates matched skills + purges stale ones. The auto-curator publishes warden Alerts when the mode flips, because silent drift is the failure mode for self-modifying systems.
  • A.5 — Packaging. Three days of a five-day sprint to make pip install werbos real. Day 1 collapsed eight sibling repos into git+https SHA pins, bundled the SPA dist as package data, and bumped license format to PEP 639. Day 2 shipped werbos init — scaffolds ~/werbos/ with chats/ images/ personas/ skills/ tasks/ subdirs, copies a templated user.md for the operator to fill in, probes Ollama for reachability, and prints actionable install hints when it isn’t running. Day 3 wired user.md through ContextBuilder so werbos actually KNOWS the operator across all sessions; emits a KIND_USER_PROFILE_LOADED audit event with {path, sha256, byte_size} (metadata only — never the body). Day 4 (Docker) and Day 5 (real PyPI publish) are queued; Day 5 is blocked on the visibility decision for the four phase-2 sibling repos.
  • Vision — /see. A werbos.vision package wrapping macOS screencapture (region / full / window modes), and a REPL /see slash that captures, base64-encodes, and attaches the PNG to the live ChatSession’s pending-images buffer. The user’s next typed message becomes the prompt; the next AI turn drains the buffer onto its UserMessage. Without --vision-model the image still attaches but the text-only model can’t see it — the slash surfaces that fact in its response. Lazy imports keep the chat module’s startup time low for users who never touch /see.
  • Persona ladder grows a named-rung surface. The composer mirror chip moved into Settings as an operator-global default (one place to set “my baseline”; per-chat overrides still work). Private-chat mode shipped end-to-end (server + API + CLI + REPL + SPA — turns flagged ?private=1 archive nothing). And the html persona — a voice-layer character mined from tom.html’s actual style (single-file, no build pipeline, CSS custom properties, semantic markup, system fonts, textContent over innerHTML) — landed alongside a new --persona-name <slug> flag that resolves packaged personas by name. Wheel-friendly: post-pip install werbos, the persona file lives inside the package and there’s no obvious on-disk path to point --persona-file at.

Past midnight — the day-8 queue captures

Two docs(backlog) commits at 00:13 and 00:55 captured the day’s shape: Sk.1 skills + 3-way autonomy toggle, private chat, save-current, persona-slider Settings mockup. The shape held.

Morning — Sk.1, private chat, Settings cleanup

Sk.1 shipped in three commits (49161ec, cd92678, ace6073). Day 1: werbos.skills.store (SQLite, one row per skill, body on disk at ~/werbos/skills/<name>.md, integrity via body_sha256, operator gates via enabled), the werbos.skills.loader (reads the body file by name, returns SkillFileError on tamper), a discovery ranker (rank_skills over keyword triggers), and the /skill REPL slash (list / show / fire). Day 2 added the 3-way mode toggle in the store + a chat-side SkillProvider that ranks the user’s last message against enabled skills, reads the top-3 bodies, and returns a single markdown chunk to append as a SystemMessage after the persona prompt. Day 3 closed the auto- curator surface: detector.detect_from_archives clusters chat patterns by Jaccard over N-grams; autocurator.AutoCurator applies candidates + stale-skill purges; werbos skills scan exposes the surface to the operator. Mode flips fire warden Alerts via LoggingAlertSink because auto-mode is loud by design.

Settings cleanup ran in parallel. 0395b2d moved the per-chat mirror chip out of the composer header and into Settings as a global default — the operator has ONE place to set “my baseline,” not seventy-five tabs each remembered separately. e1fc1bf deleted the now-redundant chip. 7573c4a shipped private-chat mode end-to-end (?private=1 query param; chat.private_mode session attribute; archive write skipped at finalize; the SPA gets a “private” badge). And four small fixes — the hooks-rule violation in ChatView (725ae59), the chat-header padding clipping the Settings FAB (8ceace5), window.confirm becoming the proper ConfirmModal (8579b09), and the composer chip removal — closed the day’s UI dust.

Mid-morning — persona discipline + dogfood UX

The “hi” bug. llama3.1:8b on a content-thin prompt confabulated “I’ll answer in plain text from now on. That’s correct, the result of the calculation is 7.” The model had seen the calculator tool in its registry + the “evaluating math” example in the discipline paragraph, and filled space by inventing a calculator result that never happened. 0074eef added an anti-fabrication clause to the default persona; f5dbf07 tightened it with concrete “hi!” counter-examples. The discipline is type-shaped — do not reference a tool result that wasn’t produced in this turn. 6dd146a shipped a persona_slider eval suite to gate the discipline going forward.

1691623 fixed five chat-UX bugs from the day’s dogfood — chess/checkers boards collapsing to 1-row layouts (1f5655c, 980c9c7), and a handful of mobile-scroll regressions.

Afternoon — packaging opens + ergonomics

5a7e347 fixed blackjack AI freezing mid-hand (the AI loop needed an explicit yield point for hands the dealer played out silently). 00f17cf shipped ? / /help as a quick-reference card in the chat REPL — slashes grouped by surface (chat, eval, notebook, skills, vision, etc.). e7c57e9 opened the packaging sprint: pyproject consolidates all eight siblings as git+https SHA pins, bundles werbos/ui/dist/** as package_data, license format updated for PEP 639. 08c7aee extracted the blackjack AI loop to a library and added two new personalities (the dogfood loop on the games surface continues to feed the v5+ training roadmap).

Evening — A.5 days 2-3, vision, persona-name

7fb0e42 (A.5 day 2): werbos init scaffolds ~/werbos/ and its standard subdirs idempotently, copies the bundled default persona if personas/default.md is missing, writes a templated user.md if it doesn’t exist, probes Ollama at http://localhost:11434/api/tags with a 2s timeout, and prints SUCCESS / CONNECTION_REFUSED / TIMEOUT messages with actionable install hints. --strict flag for CI; the K.1 project flow preserved at werbos init project. 47 new tests; 2299 passing.

e3848f6 (A.5 day 3): werbos.personas.user_profile with a UserProfile dataclass (text, byte_size, sha256, path). ContextBuilder injects user.md AFTER persona + project_context and BEFORE history. SessionRegistry’s session config gains user_profile_path; serve CLI plumbs it. KIND_USER_PROFILE_LOADED event records metadata; never the body. 16 KB cap; over-cap returns truncated profile + warning flag.

fb789b5 bumped the pitts pin past UserMessage.images (a pre-vision-work blocker — 153 pre-existing session-test failures cleared in one bump).

b2e417b shipped /see: werbos.vision.capture.capture_screen returns a CaptureResult(path, image_b64, size_bytes, mode) from shelling out to macOS screencapture with mode-specific flags (-i -x for region, -x for full, -W -x for window). ESC cancellation detected via missing/empty file. werbos.vision package re-exports the public surface; the chat REPL’s _handle_see_slash attaches captures to the session’s pending- images buffer. New session methods: attach_pending_image, pending_image_count, clear_pending_images. New _build_vision_provider factory mirrors the serve-CLI pattern. 33 new tests across capture / slash / pending-images.

aa76b62 then c1c6e91. The day’s discovery, in two commits. The first shipped a UI-side mid-stream tool-call detector to ChatView, based on a memory note that said “mid-stream JSON detection is open.” The second reverted it once I checked the roadmap and saw the work was already shipped server-side at werbos.session._stream_filter.filter_tool_call_json, wired into ChatSession.step_stream. The SPA never sees raw { tokens because the server filter swallows them and emits a PhaseEvent("tool_pending", "calling <name>...") in their place. Memory was three days stale; the roadmap was current. Lesson logged.

3d91f10 shipped the html persona + the --persona-name flag. Persona file mined from tom.html’s actual style — the canonical single-file reference in this repo at 489 lines, zero dependencies. Anti-patterns named explicitly: no Vite, no Tailwind CDN, no localStorage by default, no speculative abstractions. Precedence: --persona-file > --persona-name > --persona > built-in default. Unknown name fails loudly via SystemExit (typo = error, not silent fallback).

AHAs

  • Memory is a snapshot; the roadmap is the source. The K.0 memo said “Day 1 shipped, Day 2+ left.” git log on the same branch said all three days closed end-to-end on 2026-05-08. The streaming follow-ups memo said item 1 was open; the ROADMAP at line 570 said [shipped 2026-05-08]. At the work pace of multiple sprints/day, captured “what’s next” notes age out within hours. Habit: when a sprint memo claims in-progress, verify against the roadmap and git before acting.
  • Self-modifying systems must be loud. Sk.1’s auto-mode fires warden Alerts on every mode flip. Silent automation is the failure mode — when the curator decides to delete a skill the operator wrote, the operator hears it. The LoggingAlertSink default makes this work without configuration.
  • The wheel format is a constraint that shapes design. A.5 day 2’s werbos init exists because once werbos is pip-installed, there’s no source tree to give the user a template by example. The init command IS the template. Same shape applies to --persona-name: post-install, there’s no obvious filesystem path for --persona-file, so the surface has to load by slug from the package data.
  • The user.md audit shape mirrors a B2B compliance story. KIND_USER_PROFILE_LOADED records {path, sha256, byte_size} but never the body. Same shape as Tom’s KIND_TOM_ATTESTATION (history_length: 0, notebook_size_bytes, turn_index). Auditors get an envelope they can verify; the contents stay on the operator’s box. Local-first as your DATA, not your compute — applied to the audit surface itself.
  • The single-file HTML is the design vocabulary. The html persona pinned style by mining a real artifact (tom.html), not by inventing rules. :root color tokens, box-sizing: border-box universally, grid-template-areas for top-level layout, 14px base + 1.5 line-height, border-radius 14 / 999 / 6-8 vocabulary. Future single-file demos will share these decisions because the persona will keep them in scope.
  • The CLI has fingers now. /see makes screen capture a chat-native primitive. The next move is hooking VisionContextProvider to the user’s preferred vision model (per-agent routing per feedback_local_first_definition); the bytes stay on the box, the inference can route to whatever fits. Day 8’s vision work landed the local half of that loop.

Numbers worth keeping

  • 28 commits on main between 2afc5db (00:13) and 3d91f10 (19:33).
  • ~1903 → 2360 tests (+457 net across cli/session/server/ personas/skills/vision/cli flag tests). Pre-existing schedule-test calendar-rot (4 failures) accumulated as today’s date crossed 2026-05-09T18:00:00Z.
  • 3 sprint days closed: A.5 days 1-3 of 5. Sk.1 closed fully across days 1-3.
  • 1 commit reverted same-session (the duplicated UI stream filter). Net new files added by the revert: 0.
  • HEAD: 3d91f10.

Continuation — late evening (6pm to past midnight)

“protocol first, runtime second — for the third time today.”

The day was supposed to close at the 3d91f10 HEAD. It didn’t. Six more commits landed between 22:31 and 00:13 — cross-midnight by 13 minutes — and they’re notable enough to be their own block: Soc.1 closes (TwitterAdapter, the fourth and hardest of the four social bridges), P.1 opens AND ships its first two days back-to-back (file parsers — Phase 13’s first sprint, the missing “werbos can read non-image files” capability), and a games sweep that picked up real AI for everything on the games roadmap except Risk.

Soc.1 day 5 — TwitterAdapter (73cf87e)

The fourth and final platform on the SocialAdapter Protocol. Three security envelopes layer in — more than any of the prior three adapters carry combined:

  • OAuth 1.0a request signing (HMAC-SHA1 over an RFC-5849 base string) for outbound DM sends. Twitter’s DM endpoints require User Context auth, not Bearer.
  • CRC handshake (HMAC-SHA256 of a crc_token under consumer_secret, base64) for webhook registration + revocation pings.
  • Webhook signature verification (HMAC-SHA256 of body under consumer_secret, base64, sha256= prefix) on every inbound POST.

Plus a paid-API gate: Twitter’s basic AAA tier with DM endpoints runs $100/mo as of 2026, so the adapter ships behind an env-flag the operator flips when ready. The runtime contract is identical to the other three; only the wire-format work was harder.

docs/integrations/twitter-setup.md walks through Developer Account, App creation under a Project, AAA tier upgrade, Account Activity API webhook registration, and CRC verification — the parts a generic “twitter API” tutorial doesn’t cover because most tutorials stop at Bearer-token search APIs.

The Soc.1 sprint closes with this commit. Four platforms across five days, one Protocol, ~2300 lines of net-new integration code. The morning roll-up commit f8f7ad6 landed the README Status table + architecture-doc subsystem section for the Soc.1 v1 surface; this commit drops the fourth adapter into that frame.

P.1 days 1-2 — file parsers (9487e09, 4381fde)

Phase 13 opens. werbos already SEES via /see + VisionContextProvider (images), READS code + chat archives via the existing tool surface, and HEARS (planned) via the audio stack. P.1 fills the gap for everything else the user hands it: HTML, JSON, JSONL, TOML, Markdown, plain text, CSV, TSV, DOCX, PPTX, ZIP / TAR / GZ archives, PDF, XLSX. Twelve formats; 10 stdlib-only, 2 (PDF + XLSX) gated behind a parsers optional- dependency extra (pip install werbos[parsers]).

The pattern is the same one the orchestration plane and social bridges use: protocol first, runtime second. One FileParser Protocol in werbos/parsers/base.py; format-specific implementations layer in independently. Three discovery surfaces share the same registry:

  • /parse <path> REPL slash (operator surface).
  • /parse <url> REPL slash, day 2 (operator surface, remote). Fetches via a new web_fetch_raw helper that mirrors web_fetch’s SSRF posture — same DNS-pinned transport (M2 + R2 closed) — but returns raw bytes + Content-Type up to a 50 MB cap so the registry can dispatch by mime, falling back to URL- path extension when the server reports application/octet-stream or omits Content-Type.
  • parse_file pitts.Tool (agent surface — model invokes when the user references a file path).
  • Auto-trigger inside ChatSession, day 2: a new attachments= parameter on step() / step_stream() takes a tuple of AttachmentSpec (lightweight NamedTuple — clean layering over werbos.integrations.Attachment). A new _run_attachment_interlude mirrors _run_vision_interlude exactly: runs BEFORE the vision interlude (so a PDF + screenshot in one turn composes as [ATTACHMENT CONTEXT] then [VISION CONTEXT] then UserMessage), inserts extracted text as a SystemMessage immediately before the latest UserMessage, and the post-stream history filter preserves it across turns the same way [VISION CONTEXT] is preserved. Per-attachment failures degrade to per-attachment failure notes — a corrupt PDF in a 2-attachment turn never aborts the user’s reply.

Day 2 also wires the social bridges through: SocialMessageRouter._route_to_session translates non-image Attachment records into AttachmentSpec and forwards them through the new channel. Telegram and WhatsApp file shares now reach the model as parsed text automatically; Slack will too once its file_share event path lands (Day 6+ on the Soc.1 roadmap).

Default-deny sandbox at every entry: 50 MB per-file cap, path-traversal anchored to operator-allowed roots when set, zip-bomb defense (depth + total-bytes + per-entry caps). Optional-dep gracefulness: missing pypdf / openpyxl raises ParserUnavailable("pip install werbos[parsers]") at parse time, not import time, so the registry boots cleanly without them.

126 new tests across the two days (96 day 1 + 30 day 2). New audit event KIND_ATTACHMENT_PARSED (metadata only — no body), new PhaseEvent("parsing") / PhaseEvent("parsing_failed") for streaming UI placeholders.

Games — connect4 oracle + UI AI sweep (e4b3a2a, f4eee92)

Two parallel pieces of games tooling that have nothing to do with the chat surface but were the visible polish on the games roadmap.

werbos/games/connect4_oracle.py — a real Connect 4 negamax solver to replace the depth-3 labeler that produced the random-level v1-c4 LoRA. Negamax + alpha-beta + transposition table (with bound-flag) + center-first move ordering, optional iterative deepening for time-bounded play. Strength: 5/6 vs heuristic_policy as X at depth 5; 100% vs random at depth 4. Latency on 16GB M1 from empty board: depth 8 in 68ms, depth 10 in 545ms; iterative deepening hits depth 10 inside a 1s budget. Drop-in for scripts/build_c4_corpus.py when the v2-c4 retrain slot opens — replace oracle_move(board, mark, depth=3) with negamax_move(board, mark, depth=10) and the generic-rationale failure mode goes away.

blackjackAi.ts / poker.ts / chessAi.ts / checkersAi.ts in the React side — five games picked up real AI. New “perfect” 7th blackjack personality with exact dealer-outcome enumeration over the remaining-by-rank deck (10-bucket collapse: A, 2..9, T) with bound-flag transposition; computes EV(stand/hit/double) and argmaxes; conditions on the no-dealer-BJ rule (BlackjackView settles naturals on deal, so when up=A or T the BJ-completing hole is excluded from the hole-card distribution). 15/15 vs the single-deck S17 chart, ~0.5ms per decision. Poker gets monteCarloEquity() exported plus a rewritten pickAiAction over uniform-range Monte-Carlo. Chess + checkers get strength ladders. The companion is now playable across every game on the roadmap except Risk.

AHAs (continuation)

  • The protocol-first pattern is now the project’s design vocabulary. Three sprints (orchestration plane Phase 11, social bridges Phase 12, file parsers Phase 13) all opened with the same shape: define the Protocol, ship a default-deny sandbox, layer runtimes in independently, audit-event with metadata-only payload. The third application of a pattern is when it stops being a pattern and starts being a habit.
  • Day 1’s “no auto-trigger yet” was a load-bearing constraint. P.1 day 1 deliberately did not auto-parse on attachment receipt — only on explicit operator/agent ask. That kept the day-1 scope finite, surfaced sandbox + soft-import + recursion bugs in isolation, and let day 2 wire the auto-path on top of a proven base. Same as how the orchestration plane shipped the store + handler + dispatch in three separate sprints rather than one mega-PR.
  • web_fetch_raw is two helpers stacked: one to pin, one to fetch. The agent’s existing web_fetch has a 2 MiB safety cap that’s right for prose but wrong for files. Rather than parameterize and risk widening that surface, P.1 day 2 added a sibling _pinned_request_raw (50 MB cap, raw bytes + headers) next to _pinned_request. The SSRF guard layer is reused; the body-handling is per-call-site. Refactoring rule: don’t loosen a hardened helper for a softer use case; build a sibling.
  • Twitter’s three-envelope security is the price of legacy. OAuth 1.0a + CRC + HMAC-SHA256 webhook-sig is more crypto surface than the other three adapters combined, and it exists because the Account Activity API was bolted onto a 2010-era REST surface. The pattern: when an integration’s auth shape is more elaborate than the function it performs, the platform is paying for backwards compatibility, and you’ll feel that cost in the adapter. Documented in docs/integrations/twitter-setup.md so the next operator doesn’t re-derive it.
  • werbos can now READ. The clean way to say what Phase 13 v1 is: take any file the user hands you (HTML, doc, sheet, archive, PDF), turn it into model-readable text, audit the shape but not the body. Together with vision (images), code reading (chat archives + repo trees), and the planned audio stack, the sensory surface starts to feel complete.

Numbers worth keeping (continuation)

  • 6 commits between f8f7ad6 (22:31) and 4381fde (00:13) — f8f7ad6, 73cf87e, 9487e09, e4b3a2a, f4eee92, 4381fde. Cross-midnight by 13 minutes; this block reads as Day 8’s late session, not Day 9’s morning, because the work thread is continuous from 3d91f10 forward.
  • +~270 net tests across the block: 30 (Twitter) + 96 (P.1 day 1) + 30 (P.1 day 2) + 20 (connect4 oracle) + TS-side game-AI tests. Full sweep: 2360 → 2631 passing (4 pre-existing schedule calendar-rot failures unrelated).
  • 2 sprints’ status changed: Soc.1 closes (5/5 days). P.1 ships 2/N (day 1 + day 2; Day 3 is the v1 polish slot — content-sniffing for binary headers, OCR fallback for image-only PDFs).
  • 0 new runtime dependencies for the parser core (stdlib only for the 10 default formats). Optional pypdf>=5 and openpyxl>=3.1 gated behind the parsers extra; PDF + XLSX raise ParserUnavailable("pip install werbos[parsers]") at parse time when missing.
  • 3 protocols, one shape: Tool (orchestration / agent surface), SocialAdapter (Phase 12), FileParser (Phase 13). Each one ships in roughly the same skeleton — protocol + registry + default-deny + audit envelope — and the third one took a third of the design effort the first one did.
  • HEAD: 4381fde.