Topology
2026-05-20
City Cave · Slice 0 · Topology

The brain
is a parser.

Compositional semantic grammar · hierarchy of rules · the agent runtime as interpreter · Penrith as the thin vertical slice
SubjectPenrith / Kevin Lee LensParser combinators · grammar cascade Source.planning/2026-05-19-city-cave-brain.md §6b
01 · Composition

unBasic CityCave Cave

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

What each layer contributes

Layer · unBasic
Agency core
Voice (direct, no fluff). Frameworks (campaign analysis, audience segmentation, content optimisation). Agent hierarchy. Invariants (no auto-send, citations required, audit trail). Capability skills.
Layer · CityCave franchise
Franchise grammar
Service offerings. Brand-voice constraints (City Cave global). Franchise SOPs. Head-office relationship (Maddi · Jess Altmann · Jeremy · Tim · BDC). Territory governance.
Layer · Penrith instance
Owner intelligence
Kevin Lee. Active campaign portfolio (LSM × 4 dirs, gift cards, NDIS, custom Penrith page, Mother's Day). Relationship intel. Territory map. Hard rules: Penrith-page only, no geo-redirect, NDIS retargeting only.

The composition operator

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
02 · Hierarchy of rules

The cascade.

Eight layers in a partial order. Specificity wins on narrowing; invariants are downward-closed; promotion is deliberate.

⇩ Invariants · cannot be narrowed away
L0
Cosmic Anthropic guardrails
Helpful, harmless, honest. Not overridable by any brain.
L1
Constitutional CLAUDE.md · .claude/rules/
unBasic doctrine. No auto-send. Cite numerical claims. Client success first. Transparency. Immutability in code.
⇩ Compositional · narrow on the way down
L2
Skill .claude/skills/<skill>/SKILL.md
Domain-specific procedure rules. meta-ads-manager · email-triage · monthly-reporting · franchise-navigator · gbrain-retrieval.
L3
Knowledge knowledge/playbooks/ · research/
Agency-wide patterns, frameworks, prior art. Read by skills.
L4
Client knowledge/clients/city-cave/
Franchise voice, SOPs, escalation routes, head-office relationship. _global/ + _franchise/.
L5
Instance knowledge/clients/city-cave/penrith/
Owner preferences, history, relationship intel, per-cave campaigns. Kevin Lee.
⇩ Situational · per-invocation
L6
Hooks .claude/settings.json
Pre/post-tool validators. Audit log. Brand-guidelines check. Bash audit.
L7
Live the query + current state
The incoming email, question, or interaction.

Three cascade laws

01
Specificity wins on narrowing
L5 ("Kevin wants Penrith page") narrows L4 ("local-page when local"). Lower wins. Specialisation precedes generalisation.
02
Invariants are not narrow-able
L1's "no auto-send" can't be overridden by L5 even if Kevin asked. Invariants form a downward closure — every lower layer must satisfy every higher invariant.
03
Promotion is deliberate
A pattern at L5 only becomes an L4 rule via human-reviewed promotion across ≥3 instances. Default: stay at the layer where the pattern lives.
03 · Compositional semantic grammar

What each layer produces.

A 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.

unBasic layer · productions

<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

City Cave (franchise) layer · productions

<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

Penrith (cave) layer · productions

<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)

What "compositional" buys you

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.

04 · Static topology

The filesystem is the graph.

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
Static topology · solid arrows = data/rule flow · dotted = invariant enforcement / specificity narrowing
05 · Dynamic flow

One query, end to end.

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 dramatic point is step 14 · the constitutional invariant from L1 enforced by the L6 hooks before any send-side tool fires. Remove this step and the project ends early.
06 · The capture loop

Applicative Monad.

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
Without this loop, the Penrith folder is a static snapshot of 20 May 2026 and decays from that date forward. With it, every interaction is a training signal that compounds.

The promotion rule

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.

07 · Time

Two clocks.

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 runs the Nerve only. Heartbeat sources come on in Slices 1–3.
Pivot · 2026-05-20
Heartbeat sources arrive in inbox + sheets — not via APIs.
Meta MCP blocked on Business Support approval; Google Ads has no official MCP/CLI equivalent. Workaround: schedule reports natively in each platform → reports auto-email to a team inbox, or Google Ads scripts auto-write to a shared Sheet. Claude reads from gws — same 19-scope auth, same universal layer. No app review, no token rotation, no testing-mode expiry. Same parser shape, different envelope.

Write discipline

08 · Slice 0

What's in. What's out.

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 0Out (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
Slice 0 · "Done" criterion
One real past Kevin Lee query answered by answer-cave-query. Agent-vs-human time delta recorded.
That delta is the business case. If it's not material, the architecture is wrong before fan-out.
09 · Ultrathink · non-obvious

Ten things a less thorough read misses.

01
The agent runtime is a parser interpreter
Improving the brain = improving the grammar. Adding a capability = adding a production. Each Skill is a typed grammar fragment with a result type.
02
Composition is narrowing, not override
Lower layers narrow scope; they don't contradict invariants. Hard constraints (no auto-send) are downward-closed.
03
Higher layers cache, lower layers re-read
Constitutional rules load once per session. Per-cave intel reads per query. Same shape as Skills' progressive disclosure — one altitude up.
04
Carpaccio test = composition test
Slice 5's "cave #2 with zero new combinator code" is exactly the test the composition model must pass. If composition is right, the cave is just data.
05
Failed parse = next slice's roadmap
Log expected-set on every failure. Weighted by query frequency, that log is the auto-prioritised ingest backlog. Without it, slice prioritisation is hand-picked — worse.
06
Applicative→Monad = archive→brain
The Monad (capture loop) is what makes the system learn. Without it: sophisticated search. With it: compounding intelligence.
07
CC brain could be its own repo
Brain-as-library is the long-term factoring. cc-brain imports unbasic-brain as a dep. Same pattern for any future client. Decide at Slice 5.
08
Promotion rule is required
Without "when does L5 graduate to L4," everything sediments at L5. L4 never accumulates. Need: ≥3 instances consistent + human review.
09
Write discipline: source vs derived
Two clocks only works if heartbeat-files are re-derivable and nerve-files are not. Mixing corrupts the parser. Folder layout must encode the discipline.
10
The approval UI is the unsolved prerequisite
Capture loop assumes a human approves a diff. Brooke won't PR. Until the approval surface lands (Slice 4 latest), the loop is half-built. Stopgap: git direct, tech users only.
10 · Open questions

For the Max chat.

Q1
Brain factoring
Keep CC nested in unBasic? Or extract to its own repo when patterns harden? Affects Slice 5+ shape.
Q2
Promotion mechanism
Who decides L5 → L4? Same approver as capture loop? Different cadence?
Q3
Approval UI
Git direct vs thin intake. Brooke/Max/Maddi access pattern? Sequenced into which slice?
Q4
ads-context.md contract
What fields does Max's ads-doll expect to read? Defining the schema now (even though wiring is later) lets ads-doll author against a stable interface.
Q5 · unblocked by today's workshop
Mailbox identity for the email nerve
Once Joe + Max are gws-authed, whose mailbox does the nerve actually read? Shared 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.
The one-paragraph version
The brain is a parser. Layers compose by typed extension (unBasic ⊕ CC ⊕ Penrith), each contributing productions to a single semantic grammar that the agent runtime walks against any live query. Specificity wins on narrowing; invariants are not overridable; promotion to higher layers is deliberate. Slice 0 runs the continuous nerve only — inbox-driven query → draft → human-approval → capture-loop — with the four heartbeat data sources stubbed and the ads seam empty. Success = one real past Kevin Lee query answered, agent-vs-human time delta recorded. That delta is the business case for everything that follows.