Day 23 — 'constraint reasoning is upstream of retrieval.'
First day back from the four-day pause. External feedback from GPT analyzing a werbos chat trace named a gap the operator immediately recognized — constraint extraction belongs UPSTREAM of retrieval, not as post-hoc correction in the model's prose. One sitting later, werbos.intent_compiler is shipped: a small primitive with strict JSON output + four worked examples that wraps any tool with upstream constraint compilation + downstream result filtering. Wired into web_search, fetch_top_news, fetch_stocks. The canonical shore-fishing trace is fixed: 'shore fishing in Cabo San Lucas' now returns shore-focused results instead of offshore charter content. The meta-primitive the rest of the architecture stands on for accuracy.
“constraint reasoning is upstream of retrieval, not downstream.”
First day back from the four-day pause (Days 22-26 didn’t count per the calendar memory; Day 23 resumes today). External input named a gap the operator immediately recognized. One sitting later, the gap is filled — by a primitive the rest of the architecture now stands on.
How the day started
The operator pasted a long-form GPT analysis of a werbos chat
trace. The trace: user asked “shore fishing in Cabo San Lucas”;
the chat model called web_search("best beach fishing spots cabo san lucas"); retrieval brought back offshore sportfishing content
(Gordo Banks, charters, marlin tournaments); the model
post-hoc-corrected in prose “these are not necessarily beach
fishing spots.”
GPT’s diagnosis, in one line:
“Constraint reasoning is downstream of retrieval, not upstream of it.”
The operator’s reaction:
“once i realized what GPT was talking about i realized we have a gap.”
This is the input-layer-gap insight from Day 22 morning, reapplied. We named the gap for vague-brief → structured-spec (the studio). GPT applied it to vague-user-message → structured-tool-call. Same architectural pattern; different surface.
What landed
werbos/intent_compiler.py (9d75e3a) — the meta-primitive.
ToolConstraintsdataclass withquery,must_include,must_exclude,geography,modality,audience,timing,extras. The shape future tools serialize their input space against.compile_intent(user_message, tool_name, tool_description, model='')— one LLM call with a strict JSON output template + four worked examples (shore fishing, vegan dinner, crypto news, beginner trail runs) that force the LLM to do the inference step — “shore” implies exclude “offshore” / “charter” / “deep-sea” even though the user didn’t say it. That inference IS the value.filter_results(raw, constraints)— downstream bookend. Drops items hitting must_exclude; re-ranks must_include matches first. Handles dicts / dataclasses /to_dict()instances /reprfallback.resolve_intent_model(explicit='')— precedence: explicit kwarg >WERBOS_INTENT_MODELenv > user.md## Intent compiler- model:> chat-session’s default model. Default is the chat model itself (cheap; ~5-15s overhead per tool call). Operator opts up toqwen2.5:14bwhen stricter parsing is worth +30-60s per call.- 26 offline regression tests including the canonical shore-fishing trace assertion. JSON-parser robustness (fences, preamble/postamble, missing keys, type drift, invalid JSON), filter behavior (drop + rerank + dataclass handling), model-resolution precedence, graceful fallback when the LLM is unreachable or returns garbage. All pass.
Wired into the first three tools:
web_search(also9d75e3a) —_SEARCH_SCHEMAextended with optionaluser_message. Tool description tells the model to pass it for any query with constraints. Live test: “shore fishing in Cabo San Lucas” → compiled query"shore fishing Cabo San Lucas beach access surf casting public beach entry"+ exclude[offshore, charter, deep-sea, sportfishing boat, marlin charter]→ DDG returned shore-focused results: “Surf Fishing in Cabo, A Shore Thing” / “Cabo San Lucas Shore Fishing | InCabo Travel” / “Your 2026 Guide to Public Beach Access in Cabo San Lucas”. The canonical trace is fixed.fetch_top_news(1067455) — over-fetches 3x when constraints are present, filters by HN title./news 5 AI news not about crypto→ top 5 are all AI-adjacent (LLM training, AI coding, etc.), no crypto stories.fetch_stocks(1067455) — upstream merges UPPERCASE ticker-shapedmust_includeitems into the symbol list (intent_compiler does symbol extraction from natural language). Downstream drops symbols whose ticker OR company name matchesmust_exclude. Live test:/stocks SPY QQQ NVDA "AI tickers, not the ETFs"→ intent_compiler EXTRACTEDC3.AIas an additional ticker (it’s literally an AI company). ETF post-filter is weak when company names don’t contain the category term — operator can be more explicit (not SPY not QQQ).
/news and /stocks slash handlers now heuristically detect
natural-language vs structured args. Leading int on /news is
still the count; the rest becomes the constraint phrase. All-
uppercase short-alpha tokens on /stocks are still the symbol
list; lowercase words trigger natural-language mode.
The architectural insight that hardened
Day 22 evening: “the skill library IS the capability surface.” Day 23 sharpens it:
For local models, the catalog is necessary but not sufficient. The operator’s intent has to compile correctly INTO the catalog or the size of the catalog doesn’t matter.
A 200-skill bundle + tool-use dispatch + curated breadth all degrade if the model picks the WRONG things from the catalog because the user’s constraints aren’t extracted upfront. The intent_compiler is the meta-primitive the rest of the architecture relies on for accuracy.
Saved as memory intent-compiler-for-tools-2026-05-25. The
launch line that fell out:
“werbos doesn’t get more out of the model by making it smarter; it gets more out of the model by asking it better questions.”
That’s the intent_compiler in one sentence.
AHAs
- External feedback is signal when the operator immediately recognizes the gap. GPT’s analysis didn’t tell us anything the input-layer-gap memory hadn’t already named for the studio. But applying the same lens to a different surface (tool calls) was the non-obvious move that needed an outside voice to articulate. Worth saving as a meta-pattern: the outside voice often re-applies an insight the inside team has only used in one place.
- Few-shot prompting is the small-model superpower. The first version of the system prompt without worked examples produced conservative constraint extraction — got the obvious stuff, missed the inferential exclusions. Adding four explicit worked examples made llama3.1:8b do the inference step (“shore implies NOT offshore”) consistently. That gap-between- versions is what most “the model is dumb” complaints actually are — prompt design, not model capability.
- Graceful fallback is a feature, not a wart. When the LLM is
unreachable / returns garbage / times out, compile_intent
returns a ToolConstraints with
query = user_messageand empty constraint fields. Downstream code can proceed as if the compiler never ran — current behavior preserved. Adding intent_compiler made the pipeline strictly better, not more fragile.
Numbers worth keeping
- 2 commits (intent_compiler primitive + 3-tool wiring)
- 26 new offline regression tests (all pass)
- 3 tools intent-compiler-aware so far (web_search, fetch_top_news, fetch_stocks); each cost ~30 min to wire
- ~5-15s overhead per tool call (default llama3.1:8b compile); operator-configurable upgrade path to qwen2.5:14b
- 0 breaking changes — the
user_messagekwarg is optional on every wrapped tool; absent it, behavior is identical to before
Afternoon — intent_compiler into 3 more tools (ARCH.3 closure)
(57bf191) The 9am “wire it everywhere” list got executed:
dispatch_skill— chat model emitsdispatch_skill(name="weather", args="95814", user_message=…)and compile_intent enriches args upstream.polish— style-edit LLM seesEXTRACTED CONSTRAINTS:block in its user message.build_project— closes the ARCH.3 loop the Day 22 morning called out. The vague brief → structured spec gap gets the same upstream-compile treatment.
Four distinct tools now share one constraint-extraction contract. Same operator phrase, same constraints, every surface. The results compose. The audit trail (via the trace_id pipeline below) shows it.
Afternoon — +8 more prompt skills (Track C)
(de4ba27) /timer, /focus-30, /meditate, /tarot,
/haiku, /quote, /password-gen, /unit-convert. Skill
count: 59 user-facing skills. Two of these — /password-gen
and /unit-convert — got reviewed harder by the security loops
than any other skill in werbos. See late late night below.
Evening — trace_id primitive + glasshouse sibling
The Day 23 evening’s big architectural move: cross-layer
observability becomes a first-class primitive (a42585c).
Operator framed it earlier: “frontier model debugging is
opaque; with werbos you ask glasshouse for trace <id> and see
the whole chain.” Five hours later that’s a real build.
New in werbos:
werbos/tracing.py—start_trace()mints a UUIDv7 trace_id, binds it to acontextvars.ContextVarso every awaitable downstream inherits the binding without a kwarg.record_trace_event(layer, event, payload)appends one JSONL row to~/werbos/traces/<YYYY-MM-DD>.jsonl. Best- effort — every call site istry/except Exception: passso instrumentation NEVER takes down a turn.werbos/server/slash.pyrecordsslash/dispatchat the dispatcher boundary.werbos/tools/ollama_chat.pyrecordsllm/completionorllm/completion_failedwith model + host + duration_ms + lengths.werbos/intent_compiler.pyrecordsintent_compiler/compile_intentwith the extracted constraints. The trace shows the operator EXACTLY which constraints werbos derived from their input before any tool fired.
New sibling repo: glasshouse. Private GitHub repo
(jasonsutter87/glasshouse). Pure stdlib reader for
~/werbos/traces/. Three subcommands:
glasshouse trace <id>— markdown timeline renderglasshouse list [--day]— recent traces, one-line summaryglasshouse stats— counts per layer / per event / per day
Read-only. The writer (werbos.tracing) and the reader (glasshouse) speak a stable on-disk JSONL contract; either can be upgraded independently. The sibling family is now 8 long, with glasshouse being the diagnostic surface the rest of the family didn’t have.
pitts the agent loop
mcculloch the eval
partyparrot the live wire
pfm memory at rest
observe the event layer
watchtower alerts / triggers
warden rule enforcement
beacon signals / state
glasshouse the trace inspector ← NEW
Launch line:
“Frontier model debugging is opaque. With werbos you ask glasshouse for trace
<id>and see the whole chain.”
Late night — security pipeline Loop 1
Operator invoked the full pipeline against the Day 23 NEW surface: Purple → Black → Red → Archaeologist.
- Purple Loop 1 — 12 findings. 2 HIGH (glasshouse OOM on pathological trace file; trace_id contextvar leaks across sequential turns in the same asyncio.Task), 4 MED, 4 LOW.
- Black Loop 1 — 12 more, 3 fresh HIGHs: glasshouse follows symlinks → arbitrary file read; trace JSONL written mode 0o644 world-readable; a bypass of Purple’s HIGH-P1 fix (get_trace loads full traces dir even for single-trace lookup — the per-line cap doesn’t save you if you have months of data).
- Red Loop 1 — 24/24 unique findings remediated across
72e8fe9/03e9f79/4661d80(werbos) +8b5f2eb/e150147/70a546f(glasshouse). werbos +80 tests (4438 → 4518 in target files). glasshouse +23 tests (20 → 43). - Archaeologist Loop 1 — 15 findings on 25-50yr survival.
Three must-fix landed inline: trace JSONL
_schemaheader (glasshouse silently skips rows withouttrace_id, so the header is invisible to live tooling but legible to a 2050 archaeologist opening the file cold); inline RFC-9562 UUIDv7 fallback so observe.ids disappearing doesn’t kill the trace pipeline; worked## Intent compilerexample inresolve_intent_modeldocstring. Verdict: DURABLE.
Loop 1 total: 24 security findings + 3 durability fixes shipped across 6 commits in two repos.
Late late night — security pipeline Loop 2
The realization mid-evening:
the fix code is itself new attack surface.
wrap_untrusted_textis a new envelope primitive;_mint_trace_id_inlineis a new RFC-9562 implementation;_MODEL_NAME_REis a new regex validator. None of these shipped through Purple/Black review before Loop 1 landed them.
So Loop 2 reviewed the patches that landed in response.
- Purple Loop 2 — 10 patch-quality findings. Headlines:
/password-genuses the LLM to MINT passphrases (HIGH — distribution collapse + training-set memorization; operator treats output as real credentials); UUIDv7 inline minter non-monotonic within a millisecond (violates RFC 9562 §6.2 and defeats glasshouse’s 8-hex-prefix promise). - Black Loop 2 — 8 more, including the highest-impact
Loop-2 finding:
_scrub_catalog_textstrips Cc but NOT Cf (HIGH — BIDI override, zero-width space, U+2028 line-separator all survive the catalog scrubber and land in the model’s tool description). The Red Loop 1 patch that INTRODUCED_scrub_catalog_textfailed to delegate to the canonicalstrip_format_codepointsprimitive that already handles this correctly inwerbos/session/untrusted.py. - Red Loop 2 — 14 unique findings remediated across
795e063/cbcf30a/9d3bedf(werbos) +e310d41/6c89ea3(glasshouse). ~534 LOC net. werbos +49 tests (4509 passing), glasshouse +7 tests (50 passing). - Archaeologist Loop 2 — 8 findings, 3 SIGNIFICANT. All
three landed inline:
eff_large_wordlist.LICENSE.txtsibling file with EFF / CC-BY 3.0 / SHA-256 / 7776-word attribution; tightened wordlist corruption check from< 1000to7776 ± 32(a 1500-word truncation would have PASSED the old check and silently dropped entropy from 77.5 → 63 bits); externalized ticker blocklist towerbos/util/ticker_blocklist.txtwith operator override at~/werbos/ticker_blocklist.txt. Plus the GOOD-finding’s 1-LOC future-proof:strip_format_codepointsnow drops any general-category starting withCexceptCn, soCo(private-use codepoints — a smuggling vector) andCs(surrogates) are stripped without an explicit add. Verdict: DURABLE.
Loop 2 total: 22 findings remediated across 5 commits in both repos.
The Loop-2 fixes worth keeping in mind
/password-genno longer asks the LLM to mint passphrases. Newwerbos/util/passphrase.pyusessecrets.choice()over a bundled 7776-word EFF “large” diceware wordlist. Each phrase reports actual entropy in bits; the slash handler bypassesollama_chatentirely. The “LLM as RNG for security-critical output” anti-pattern is now physically impossible for this skill.- UUIDv7 inline minter now RFC 9562 §6.2 Method 1 compliant.
12-bit monotonic counter in
rand_akeyed on(last_ts_ms, counter), thread-safe viathreading.Lock. Same-ms minted IDs sort in mint order. glasshouse’s 8-hex-prefix lookup promise (in the README) is now actually true. /unit-convertrefuses medication-dose patterns. Operator asks for50 mg/kg insulin to mLand gets a “consult a pharmacist” refusal instead of a hallucinated number. Software-as-medical-device territory; the refusal is the right posture before EU/UK launch.- slash dispatcher uses
trace_turncontext manager. Thestart_trace+finally clear_tracepattern broke nested scheduler → slash trace handoffs.clear_traceis now DEPRECATED.
The meta-pattern that closed Day 23
A two-loop pipeline turned into a default. Every fix that ships
in response to a Purple/Black review introduces NEW code that
those reviews never saw. Loop 2 isn’t paranoia — it’s how the
fix surface gets reviewed at all. And the second loop found
two HIGH findings (/password-gen LLM minting +
_scrub_catalog_text Cf-blindness) that Loop 1 couldn’t have
caught because the relevant code didn’t exist yet.
Saved as future operating procedure: any security pipeline that ships ≥100 LOC of fix code earns a second pipeline against the fixes themselves. Cost: ~3 hours of agent time. Yield: two HIGH findings the first pass missed by definition.
Quote of the day (late update)
“the fix is the new attack surface.”
The realization mid-Loop-2 when Black flagged
_scrub_catalog_text as a HIGH. The function Red INTRODUCED to
fix Purple Loop 1’s P7 had a hole bigger than the original
finding. The Loop-2 pipeline paid for itself in one finding.
Updated numbers worth keeping
- 22 commits today (3 feat + 6 Loop 1 + 5 Loop 2 + 8 earlier intent_compiler core)
- 2 security loops of Purple → Black → Red → Archaeologist
- 46 unique findings remediated (24 Loop 1 + 22 Loop 2)
- +158 tests across both repos (werbos 4438 → 4509 passing in target files; glasshouse 20 → 50 passing)
- 8th sibling repo shipped (glasshouse v0.1.0)
- 59 user-facing skills + 4 intent-compiler-aware tool surfaces
- 2 Archaeologist verdicts: DURABLE (both loops). All must-fixes landed inline.
- 0 Critical / 0 High security findings outstanding
What’s next (Day 24+)
- Real-LLM smoke from the live chat surface — operator validates the shore-fishing trace + the new tracing pipeline from the dashboard, not just command-line smoke.
- Glasshouse v0.1 → v0.2 —
.pfmchat archive join so operator-facing user+assistant turns appear in the timeline next to per-layer trace rows. /commitupgrade still queued (Phase B from the May-21 asks).- Distribution rails (NX.1 + A.5 day 5) — still deferred, still the v1 ship gate. Two security loops have bought enough confidence that the public-install story is closer than it was 24 hours ago.
Loop 3 — final scored Archaeologist verdict: TIMELESS @ 97/100
After the two security loops closed, operator asked for a numeric score. The Archaeologist’s prior verdicts were textual; the path to TIMELESS was named but not walked.
8 outstanding Archaeologist findings landed inline (6950f9c
werbos + 6f1a155 glasshouse):
- L1-F2 paths.py cross-repo invariant docstring
- L1-F4 intent_compiler model-coupling block comment + new
compile_intent_smoke_test() - L1-F6
safe_record(layer, event, payload)helper centralizing thetry/except Exception: passpattern - L1-F7
Row.ts_dtalways returns aware-UTC datetime - L1-F9
_iter_jsonl_pathsglobs the date-shape pattern - L2-F4
clear_traceemitsDeprecationWarningat call time - L2-F6
wrap_untrusted_textsource escape now lossless backslash - L2-F7
_NAME_REsecurity/style comment split
Plus the GOOD-finding 1-LOC future-proof: strip_format_codepoints
now drops ANY C-prefix category except Cn (so private-use
codepoints Co and surrogates Cs are stripped without explicit
listing). 16 new tests verify each fix.
The Archaeologist re-scored
DURABLE → TIMELESS. Score: 97/100.
The 3 missing points:
- R1 (-2pts) intent_compiler system prompt is a load-bearing 110-line English program — unavoidable LLM-as-parser cost
- R2 (-1pt) emoji glyphs in
_LAYER_GLYPHare font-dependent in 2050 — presentation choice - R3 (~-0.5pt rounded in)
_NEWLINE_RE/_LINEBREAK_REregex duplicated within one repo
“The codebase has crossed the threshold where its OWN durability primitives (schema header, safe_record, smoke test, deprecation warning, lossless escape) are now load-bearing for the durability story. That is the mark of TIMELESS code: the code knows what year it is and tells you.”
Full Loop 3 review at
security-review/2026-05-27-archaeologist-d23-l3-final-v1.md.
The full Day 23 closure
Three pipelines closed today:
| Loop | Findings | Verdict |
|---|---|---|
| Loop 1 (Day-23 new surface) | 24 unique | DURABLE → fixes landed |
| Loop 2 (the fix code from Loop 1) | 14 unique | DURABLE → fixes landed |
| Loop 3 (Archaeologist leftovers) | 8 unique | TIMELESS @ 97/100 |
46 security findings + 19 durability findings remediated. +165 tests. 22 commits. Zero Critical / Zero High outstanding.
Day 23 closes with the input layer filled, the observability layer shipped, the security posture re-validated under three adversarial pipelines, AND the 25-50yr-survival score on the record. werbos is the boringest it has ever been — which, for an AI tool in 2026, is the highest praise available.