Brains compose by typed extension, not containment. Each layer is a parser; layers combine with the same combinators (<*>, >>=, <|>) the per-cave record uses one altitude lower.
-- Each brain = a typed extension of the one above unbasicBrain :: Brain Agency ccBrain :: Brain (Franchise CityCave) -- extends unbasicBrain penrithBrain :: Brain (Cave Penrith) -- extends ccBrain -- Composition: left wins on invariants, right wins on specificity answerKevin :: Query → Answer answerKevin = unbasicBrain <> ccBrain <> penrithBrain
This is not containment. CC living inside unBasic on the filesystem is an implementation choice. The model says: each client brain is a typed extension of unBasic. Future clients (DTC, non-franchise) extend the same way:
otherClientBrain :: Brain (DirectClient Foo) -- also extends unbasicBrain
The carpaccio plan defines the cave record as caveParser = CaveContext <$> profileParser <*> salesParser <*> …. Brain composition is the same shape, one altitude up:
agentRuntime = AnswerContext <$> unbasicRules -- always-on invariants <*> activeSkill -- selected by query <*> clientContext -- City Cave <*> instanceContext -- Penrith <*> liveQuery -- the input
query >>= chooseSkillEight layers in a partial order. Specificity wins on narrowing; invariants are downward-closed; promotion is deliberate.
Anthropic guardrailsCLAUDE.md · .claude/rules/.claude/skills/<skill>/SKILL.mdmeta-ads-manager · email-triage · monthly-reporting · franchise-navigator · gbrain-retrieval.knowledge/playbooks/ · research/knowledge/clients/city-cave/_global/ + _franchise/.knowledge/clients/city-cave/penrith/.claude/settings.jsonthe query + current stateA semantic grammar where non-terminals carry meaning, not just syntactic categories. Each layer extends the grammar with productions specific to its altitude. Queries parse by traversing all layers in narrowing order.
<request> ::= <data-q> | <draft-q> | <decision-q> | <coaching-q> | <report-q> <data-q> ::= "what is" <metric> "for" <subject> <draft-q> ::= "draft" <comm-type> "to" <person> <comm-type> ::= "reply" | "follow-up" | "summary" | "report" <voice> ::= direct + no-fluff + sentence-short + ai-as-collaborator <delivery> ::= must-cite-sources · must-not-auto-send · must-log
<subject> ::= <franchise> | <cave> -- extends unBasic <subject> <franchise> ::= "City Cave" | "the network" <metric> ::= <unbasic-metric> | "float volume" | "membership" | "gift card sales" | "NDIS leads" <escalation> ::= <issue> → "Jess Altmann" → {Jeremy, Tim, BDC} <voice-cc> ::= <voice> + brand-words-only · wellness-register
<cave> ::= "Penrith" | "Kevin's cave" <person> ::= "Kevin" | "Kevin Lee" <sign-off> ::= "Sincerely," ↵ "Kevin Lee" ↵ <full-sig-block> <voice-penrith>::= <voice-cc> + numbered-when-multi-point + paper-trail-required + timely-or-he-chases <page-rule> ::= ads ::= local? "Penrith page" : "City Cave main page" + NEVER geo-redirect-to-main-homepage <ndis-rule> ::= ndis-leads ::= retargeting-only + NEVER email-outreach <territory> ::= {Richmond, Liverpool, Ed-Park, Minchinbury, west-open} + if-purchased(t) → STOP ads-in(t)
A query like "draft Kevin a reply about the ROAS on the Penrith-page custom campaign" is parsed by traversing all three layers:
parse "draft reply about ROAS Penrith-page custom to Kevin" = unBasic : <draft-q> = "draft" + <comm-type>("reply") + "to" <person>("Kevin") + CityCave : <metric>("ROAS") on <campaign-type>("Penrith-page-custom") + Penrith : <person> = Kevin Lee · use <sign-off>("Sincerely, Kevin Lee") · apply <voice-penrith> (numbered, paper-trail) · apply <page-rule> (already Penrith page — confirm) · the campaign exists at ROAS 12.34
The Answer is constrained by all productions matched in narrowing order. The skill generating the reply only needs to satisfy the composed grammar — it doesn't need to know which layer each constraint came from.
Markdown + YAML frontmatter + wikilinks is a typed graph. No Neo4j until Slice 5+ proves cross-cave query volume. Orange = Slice 0 critical path. Dashed = exists as seam/stub.
graph TB
subgraph L1[Layer 1 · Constitutional · unBasic]
CLAUDE[CLAUDE.md
org constitution]
RULES[.claude/rules/]
HOOKS[.claude/settings.json
hooks]
end
subgraph L2[Layer 2 · Skills · unBasic]
S_GBRAIN[gbrain-retrieval]
S_EMAIL[email-triage]
S_REPORT[monthly-reporting]
S_FRAN[franchise-navigator]
S_META[meta-ads-manager]
S_ANSWER[answer-cave-query
NEW for slice 0]
end
subgraph L3[Layer 3 · Knowledge · agency-wide]
PB[knowledge/playbooks/]
RS[knowledge/research/]
end
subgraph L4[Layer 4 · Client · City Cave]
GLOBAL[_global/
brand · voice · services]
FRANCHISE[_franchise/
HQ · Jess · Jeremy · Tim · BDC]
end
subgraph L5[Layer 5 · Instance · Penrith]
PROFILE[profile.md
+ relationship block]
EMAILS[emails/
synced thread]
NOTES[notes.md]
ADSEAM[ads-context.md
EMPTY SEAM]
SALES[sales-history.md]
INV[invoicing.md]
ADH[ad-history.md]
CHLOG[changelog.md]
end
subgraph L7[Live]
Q[query / email / call]
A[Answer / draft]
end
CLAUDE --> RULES --> HOOKS
HOOKS -.invariants.-> L2 & L3 & L4 & L5
L2 -.reads.-> L3 & L4 & L5
GLOBAL -.narrows.-> PROFILE
FRANCHISE -.narrows.-> PROFILE
PROFILE -.specificity.-> Q
Q --> S_ANSWER
S_ANSWER --> A
classDef hot fill:#f97316,stroke:#f97316,color:#0a0a14,font-weight:700
classDef seam stroke-dasharray: 5 5,opacity:0.5
class HOOKS,PROFILE,S_ANSWER hot
class ADSEAM,SALES,INV,ADH seam
A real email lands; the runtime traverses the cascade; a draft comes out; a human approves; the capture loop updates the relationship intel; the reply sends.
sequenceDiagram
autonumber
participant K as Kevin Lee
participant GM as Gmail
(unbasic.com.au)
participant ET as email-triage
participant AR as Agent Runtime
participant L1 as L1 invariants
participant L5 as Penrith folder
participant GB as gbrain-retrieval
participant FN as franchise-navigator
participant SA as answer-cave-query
participant H as Human
(Max/Joe/Brooke)
participant CL as Capture loop
participant GIT as Git
K->>GM: emails a question
GM->>ET: new msg event
ET->>AR: classify · owner-query?
AR->>L1: load invariants
AR->>L5: load Penrith folder + Kevin profile
L5-->>AR: intel + history + campaigns
AR->>GB: retrieve relevant chunks
GB-->>AR: ranked passages + citations
AR->>FN: voice + escalation route
FN-->>AR: register (formal · numbered · sign-off)
AR->>SA: compose draft
SA-->>AR: candidate reply + citations
AR->>H: present draft (NEVER auto-send)
H-->>AR: edit / approve / reject
AR->>CL: propose diff to profile + changelog
CL->>H: show diff
H-->>CL: approve
CL->>GIT: commit
AR->>GM: send approved reply
The boundary between archive and brain. Applicative composition gives you a populated record. Monadic composition gives you a relationship that understands — state threading through every interaction.
graph LR
Q[query] -->|parsed| ANS[Answer + voice]
ANS -->|human edits| EDIT[edited reply]
EDIT -->|diff vs
relationship-intel| DIFF[proposed diff]
DIFF -->|human approve| COMMIT[git commit]
COMMIT -->|writes to| L5[Penrith profile
+ changelog]
L5 -->|next query| Q
classDef hot fill:#f97316,stroke:#f97316,color:#0a0a14,font-weight:700
classDef store fill:#141428,stroke:#f97316,color:#f1f1f5
class DIFF hot
class L5 store
When does an L5 observation graduate to L4?
if observed_at(layer_5) for ≥3 distinct caves and consistent_signal: propose_promotion(layer_5 → layer_4) human_review(promotion)
Default: stay at the layer where the pattern lives. Promotion is a deliberate, reviewed event. Without it, everything sediments at L5 and L4 never accumulates.
Not two cadences — two algebras. The monthly heartbeat is an Applicative product (parallel, commutative). The continuous nerve is a Monadic fold (sequential, stateful, order-dependent).
graph TB
subgraph Heartbeat[Heartbeat · Applicative · monthly · platform-scheduled]
META[Meta scheduled reports
→ inbox] -->|parse email| ADH[ad-history.md]
GADS[Google Ads scripts
→ Sheets] -->|parse rows| GADH[ad-history.md cont.]
AA[Agency Analytics
scheduled → inbox] -->|parse email| REPORTS[reports/]
MT[MyTime CSV / Sheets export] -->|parse| SALES[sales-history.md]
INVS[Invoicing Sheet] -->|parse| INV[invoicing.md]
end
subgraph Nerve[Continuous Nerve · Monadic · per event]
INBOX[unbasic.com.au inbox] -->|gws watch| ETN[email-triage]
ETN -->|append| EMAILS[emails/]
ETN -->|trigger| QUERY[query → answer cycle]
QUERY -->|capture loop| INTEL[relationship intel
+ changelog]
end
GWS[gws CLI · universal ingest layer
gmail + sheets · 19 scopes]
GWS -.feeds.-> Heartbeat
GWS -.feeds.-> Nerve
Heartbeat -.->|derived ⟷ source
distinction critical| Nerve
classDef hot fill:#f97316,stroke:#f97316,color:#0a0a14,font-weight:700
class INTEL,GWS hot
Slice 0 is the thinnest vertical that answers one real past Kevin query. The agent-vs-human time delta is the business case for everything that follows.
| In Slice 0 | Out (later slice) | |
|---|---|---|
| Cave folders | _global/voice-and-tone.md, _franchise/profile.md, penrith/profile.md w/ relationship block from Kevin's brief, penrith/emails/ from real history, notes.md, empty ads-context.md seam |
Per-cave fan-out (Slice 6+) |
| Skills | answer-cave-query (new) · gbrain-retrieval (existing) · franchise-navigator (existing) |
Continuous email-triage nerve (Slice 1+) · monthly-reporting cron (Slice 4) |
| Data sources | unbasic.com.au mailbox (read-only, unblocked by today's GCP workshop) · past Kevin emails | MyTime (Slice 2) · invoicing (Slice 1) · Meta + Agency Analytics (Slice 3) |
| Two clocks | Nerve only · query on demand | Heartbeat (Slice 4) |
| Capture loop | Proposed but not auto-firing — manual edit | Live & gated (Slice 4) |
| Approval UI | Git directly (technical users only) | Thin intake for non-git users (Slice 4) |
| Ads automation | Empty seam at ads-context.md, never wired |
Post-Max-chat, post-Slice-3 |
| Graph DB / Convex | None | Slice 5+ if cross-cave query volume warrants |
| Brain promotion (L5→L4) | None | As patterns emerge |
answer-cave-query. Agent-vs-human time delta recorded.cc-brain imports unbasic-brain as a dep. Same pattern for any future client. Decide at Slice 5.info@unbasic.com.au? Per-person with a filter for cave-owner senders? The auth is the prerequisite; the routing decision is the actual question.