# City Cave Slice 0 — Topology

> Drafted 2026-05-20 · scope: the thin vertical · subject: Penrith
> Lens: compositional semantic grammar + parser-combinator brain (per `.planning/2026-05-19-city-cave-brain.md` §6b)

---

## 0 · Thesis

**The brain is a parser interpreter.** Skills, rules, knowledge, client folders, and per-instance intel are all productions in a single compositional grammar. The agent runtime walks the grammar against a live input (an email, a question, an interaction) and emits a typed Answer.

There is no separate "City Cave brain" — there is the **unBasic grammar**, **extended** by a **City Cave franchise grammar**, **extended again** by a **per-cave instance grammar**. Composition is by parser combinator. The runtime executes the composed grammar.

This document maps the topology of Slice 0 (the thin vertical for Penrith / Kevin Lee) along five axes:

1. Composition (how the brains compose)
2. Hierarchy (the cascade of rules)
3. Grammar (the productions per layer)
4. Flow (how a query traverses the topology)
5. Time (the two clocks)

---

## 1 · Composition — unBasic ⊕ City Cave ⊕ Cave

### 1.1 The composition model

```
Brain :: ServiceContext → Query → Answer

unbasicBrain   :: Brain Agency
ccBrain        :: Brain (Franchise CityCave)   -- extends unbasicBrain
penrithBrain   :: Brain (Cave Penrith)          -- extends ccBrain

answerKevin :: Query → Answer
answerKevin = unbasicBrain <> ccBrain <> penrithBrain
            -- left to right: each layer narrows the context
            -- right wins on specificity; left wins on invariants
```

This is *not* containment ("CC lives inside unBasic"). It is *extension* — each brain is a typed extension of the one above. The current filesystem layout (CC nested inside unBasic) is an **implementation choice**, not the composition model. Other clients (DTC, non-franchise) would extend unBasic differently:

```
otherClientBrain :: Brain (DirectClient Foo)   -- also extends unbasicBrain
```

### 1.2 What each layer contributes

| Layer | Contributes | Example |
|-------|-------------|---------|
| **unBasic** | Voice, frameworks (campaign analysis, audience segmentation, content optimisation), agent hierarchy (strategic/tactical/operational), security/audit invariants, capability skills | "No auto-send" (D5) · "Citations required on numerical claims" · `meta-ads-manager` skill · `email-triage` skill |
| **City Cave (franchise)** | Service offerings (float / massage / sauna), brand-voice constraints (City Cave global guidelines), franchise SOPs, head-office relationship (Maddi · Jess Altmann · Jeremy · Tim · BDC), franchise-wide commercial rules | "Ads must run from local cave page when local audience" · "Territory expansion requires Jess sign-off" · franchise voice register |
| **Penrith (cave)** | Owner identity (Kevin Lee), per-cave campaign portfolio (LSM × 4 dirs, gift cards, NDIS, custom Penrith-page), relationship intel (cares about / said no to), territory map (Richmond N / Liverpool S / Minchinbury E / west open), local data (MyTime sales, invoicing) | "Sincerely, Kevin Lee — formal sign-off" · "No geo-redirect to homepage" · "NDIS retargeting only" · ROAS 12.34 on Penrith-page custom |

### 1.3 The composition operator

The plan defines `caveParser = CaveContext <$> profileParser <*> salesParser <*> invoicingParser <*> adHistoryParser <*> commsParser <*> relationshipParser`. The brain composition is the same shape, one altitude higher:

```
agentRuntime
  = AnswerContext
  <$> unbasicRules        -- always-on invariants
  <*> activeSkill         -- selected by query
  <*> clientContext       -- City Cave
  <*> instanceContext     -- Penrith
  <*> liveQuery           -- the input
```

Two algebraic facts:
- **Applicative for parallel layers**: when two layers are independent (e.g. `activeSkill` and `clientContext` don't depend on each other), `<*>` is the right operator. Order doesn't matter.
- **Monadic when later layers depend on earlier ones**: e.g. *which* skill to load depends on what the query is about; the query is parsed first, then a skill is bound. This is `query >>= chooseSkill`.

---

## 2 · Hierarchy of rules — the cascade

### 2.1 The cascade

```
┌──── INVARIANTS (cannot be narrowed away) ────────────────────┐
│ Layer 0 — Cosmic        Anthropic guardrails                 │
│ Layer 1 — Constitutional CLAUDE.md + .claude/rules/          │
│                          No auto-send · audit trail · client │
│                          success first · transparency        │
├──── COMPOSITIONAL (narrow on the way down) ──────────────────┤
│ Layer 2 — Skill          .claude/skills/<skill>/SKILL.md     │
│                          domain-specific procedure rules     │
│ Layer 3 — Knowledge      knowledge/playbooks/ · research/    │
│                          patterns, frameworks, prior art     │
│ Layer 4 — Client         knowledge/clients/city-cave/        │
│                          franchise voice, SOPs, escalation   │
│ Layer 5 — Instance       knowledge/clients/city-cave/penrith/│
│                          owner preferences, history, intel   │
├──── SITUATIONAL (per-invocation) ────────────────────────────┤
│ Layer 6 — Hooks          .claude/settings.json hooks         │
│                          pre/post-tool validators, audit     │
│ Layer 7 — Live           the query + current state           │
└──────────────────────────────────────────────────────────────┘
```

### 2.2 Cascade laws

Three laws govern how layers interact:

1. **Specificity wins on narrowing.** Layer 5 ("Kevin wants Penrith page only") is more specific than Layer 4 ("City Cave brand voice"). When narrowing, lower wins.
2. **Invariants are not narrow-able.** Layer 1's "no auto-send" cannot be overridden by Layer 5 even if Kevin said "please auto-send." Invariants form a downward closure: every lower layer must satisfy every higher invariant.
3. **Promotion is deliberate.** A pattern observed at Layer 5 (multiple cave owners hate weekend-only ads) only becomes a Layer 4 rule via human-reviewed promotion. The capture loop writes to Layer 5 by default; promotion to Layer 4 is a separate decision.

### 2.3 The cascade walk

For any query, the runtime walks the cascade in two passes:

**Pass A — accumulate constraints (top down):**
- Load invariants from L0/L1 (always in context).
- Load active skill from L2 (matched on query).
- Load relevant L3/L4/L5 knowledge (matched on entities mentioned).
- Activate hooks at L6.

**Pass B — generate answer (constrained):**
- Generate a candidate answer that satisfies the accumulated constraint set.
- Hook validators reject violations.
- Output goes to user.

This is the same shape as CSS cascade + computed style, applied to brain rules. Specificity + !important.

---

## 3 · Compositional semantic grammar — the productions

Each layer contributes grammar productions. A query is parsed by composing them.

### 3.1 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
```

### 3.2 City Cave (franchise) layer (productions)

```
<subject>      ::= <franchise> | <cave>     -- adds to unBasic <subject>
<franchise>    ::= "City Cave" | "the network"
<metric>       ::= <unbasic-metric> | "float volume" | "membership"
                 | "gift card sales" | "NDIS leads"
<comm-type>    ::= <unbasic-comm-type> | "territory request" | "campaign brief"
<escalation>   ::= <issue> "→" "Jess Altmann" "→" {Jeremy, Tim, BDC}
<voice-cc>     ::= <voice> + brand-words-only · no-superlatives · wellness-register
```

### 3.3 Penrith (cave) layer (productions)

```
<cave>         ::= "Penrith" | "Kevin's cave"
<person>       ::= "Kevin" | "Kevin Lee"
<sign-off>     ::= "Sincerely," NEWLINE "Kevin Lee" NEWLINE <full-sig-block>
                 -- for reply drafts the agent should match this register
<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)
<campaigns>    ::= LSM × 4 dirs ⊕ gift-card ⊕ NDIS
                 ⊕ Mothers-Day ⊕ Penrith-page-custom ⊕ national-group-buy
```

### 3.4 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")
  + City Cave   : <metric>("ROAS") on <campaign-type>("Penrith-page-custom")
  + Penrith     : <person> = Kevin Lee · use <sign-off>("Sincerely, Kevin Lee")
                · apply <voice-penrith> (numbered if multi-point, paper-trail)
                · apply <page-rule> (already Penrith page — confirm in reply)
                · the campaign exists in Kevin's portfolio at ROAS 12.34
```

The Answer is constrained by ALL productions matched, in their narrowing order. The skill that *generates* the reply only needs to satisfy the composed grammar — it doesn't need to know which layer each constraint came from.

---

## 4 · Static topology (filesystem + composition)

```mermaid
graph TB
    subgraph L1[Layer 1 · Constitutional · unBasic]
        CLAUDE[CLAUDE.md<br/>org constitution]
        RULES[.claude/rules/<br/>00-overview · 01-workflow · 02-commands · 03-feature-schema · 04-progress-log]
        HOOKS[.claude/settings.json<br/>pre/post/Stop hooks]
    end

    subgraph L2[Layer 2 · Skills · unBasic]
        S_GBRAIN[gbrain-retrieval<br/>pgvector + BM25]
        S_EMAIL[email-triage<br/>gws-based]
        S_REPORT[monthly-reporting<br/>Agency Analytics]
        S_FRAN[franchise-navigator<br/>POC · escalation · voice]
        S_META[meta-ads-manager]
        S_ANSWER[answer-cave-query<br/>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 franchise]
        GLOBAL[_global/<br/>brand · voice · service offerings]
        FRANCHISE[_franchise/<br/>HQ relationship · Jess · Jeremy · Tim · BDC]
    end

    subgraph L5[Layer 5 · Instance · Penrith cave]
        PROFILE[profile.md<br/>frontmatter + §3 relationship block]
        EMAILS[emails/<br/>synced comms thread]
        NOTES[notes.md<br/>human-written context]
        ADSEAM[ads-context.md<br/>EMPTY SEAM]
        SALES[sales-history.md<br/>OUT in slice 0]
        INV[invoicing.md<br/>OUT in slice 0]
        ADH[ad-history.md<br/>OUT in slice 0]
        CHLOG[changelog.md]
    end

    subgraph L7[Layer 7 · 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 -.most-specific.-> Q
    Q --> S_ANSWER
    S_ANSWER --> A

    style HOOKS fill:#f97316,color:#fff
    style PROFILE fill:#f97316,color:#fff
    style S_ANSWER fill:#f97316,color:#fff
    style ADSEAM stroke-dasharray: 5 5
    style SALES stroke-dasharray: 5 5
    style INV stroke-dasharray: 5 5
    style ADH stroke-dasharray: 5 5
```

**Legend:** orange = slice-0 critical path · dashed = exists as seam/stub only · solid arrows = data/rule flow · dotted = invariant enforcement.

---

## 5 · Dynamic topology — flow of a single query

```mermaid
sequenceDiagram
    autonumber
    participant K as Kevin Lee
    participant GM as Gmail (unbasic.com.au)
    participant ET as email-triage skill
    participant AR as Agent Runtime<br/>(Claude Code session)
    participant L1 as Layer 1 invariants
    participant L5 as Penrith profile + history
    participant GB as gbrain-retrieval
    participant FN as franchise-navigator
    participant SA as answer-cave-query<br/>(Slice 0 skill)
    participant H as Human (Max/Joe/Brooke)
    participant CL as Capture loop<br/>(D5)
    participant GIT as Git

    K->>GM: emails a question
    GM->>ET: gws gmail +watch → new msg event
    ET->>AR: classify · is this owner-query?
    AR->>L1: load invariants (no auto-send · cite)
    AR->>L5: load Penrith folder + Kevin profile
    L5-->>AR: relationship intel + history + active campaigns
    AR->>GB: retrieve relevant chunks (vector + BM25)
    GB-->>AR: ranked passages with citations
    AR->>FN: voice + escalation route
    FN-->>AR: register (formal · numbered · timely · sign-off)
    AR->>SA: compose draft
    SA-->>AR: candidate reply with citations
    AR->>H: present draft for approval (NEVER auto-send)
    H-->>AR: edit / approve / reject
    alt human approved with edits
        AR->>CL: propose diff to Penrith profile + changelog
        CL->>H: show diff
        H-->>CL: approve
        CL->>GIT: commit
    end
    AR->>GM: send approved reply (gws gmail +reply)
```

**The single dramatic point** of this diagram: step 14 (human approval). It is the constitutional invariant from Layer 1 (no auto-send · D5 from Exoskeleton 02), enforced by the hooks at Layer 6, before any send-side tool is allowed to fire. Removing this step is the failure mode that ends the project early.

---

## 6 · The capture loop — the Applicative→Monad boundary

```mermaid
graph LR
    Q[query] -->|parsed| ANS[Answer + voice]
    ANS -->|human edits| EDIT[edited reply]
    EDIT -->|"diff vs<br/>relationship-intel"| DIFF[proposed diff]
    DIFF -->|human approve| COMMIT[git commit]
    COMMIT -->|writes to| L5[Penrith profile + changelog]
    L5 -->|next query| Q

    style DIFF fill:#f97316,color:#fff
    style L5 fill:#141428,stroke:#f97316,color:#fff
```

This is the loop that **distinguishes brain from archive**. Without it 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 a Layer 5 observation graduate to Layer 4?):

```
if observed_at(layer_5) for ≥3 distinct caves AND consistent_signal:
    propose_promotion(layer_5 → layer_4)
    human_review(promotion)
```

Promotion is a deliberate, reviewed event. Default = stay at the layer where the pattern lives.

---

## 7 · Time — the two clocks

```mermaid
graph TB
    subgraph Heartbeat[Heartbeat · Applicative product · monthly · 3rd of month]
        MT[MyTime sales] -->|parser| SALES[sales-history.md]
        INVS[invoicing sheets] -->|parser| INV[invoicing.md]
        META[Meta + Agency Analytics] -->|parser| ADH[ad-history.md]
        REP[reports branch] -->|parser| REPORTS[reports/]
    end

    subgraph Nerve[Continuous Nerve · Monadic fold · 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

    Heartbeat -.->|derived ⟷ source<br/>distinction critical| Nerve

    style INTEL fill:#f97316,color:#fff
    style Heartbeat fill:#141428,stroke:#3b82f6,color:#fff
    style Nerve fill:#141428,stroke:#22c55e,color:#fff
```

**Slice 0 runs only the Nerve.** Heartbeat sources come on in Slices 1–3.

**Write discipline (algebraic, not stylistic):**
- Heartbeat writes to **derived** files only. Must be 100 % re-derivable from source. Never edited by humans.
- Nerve writes to **source** files (profile relationship block, notes, changelog). Edited by humans + capture loop.
- The line between source and derived files must be clear in the folder layout. Mixing them = corrupting the parser.

---

## 8 · What's IN Slice 0 vs OUT (explicit)

| | IN Slice 0 | OUT (later slice) |
|---|---|---|
| Cave folders | `_global/voice-and-tone.md` (1 page), `_franchise/profile.md`, `penrith/profile.md` (relationship block filled from Kevin's brief), `penrith/emails/` (from real history), `penrith/notes.md`, empty `penrith/ads-context.md` seam | per-cave fan-out (Slice 6+) |
| Skills | `answer-cave-query` (new), `gbrain-retrieval` (existing), `franchise-navigator` (existing) | full `email-triage` continuous nerve (Slice 1+), `monthly-reporting` cron (Slice 4) |
| Data sources | unbasic.com.au mailbox (read-only, **today's workshop unblocks this**), 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 surface 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's "done" criterion (from the plan):** one real past Kevin query answered by `answer-cave-query`; human-assembly time vs agent time recorded. *That delta is the business case.*

---

## 9 · Ultrathink — non-obvious design pressures

Ten things a less-thorough read would miss:

1. **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.
2. **Composition is left-to-right narrowing, not override.** Lower layers narrow scope; they don't contradict invariants. Hard constraints (no auto-send) are downward-closed.
3. **Higher layers cache; lower layers re-read.** Constitutional rules load once. Per-cave intel reads per query. This is the same shape as Skills' progressive disclosure, one altitude up.
4. **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.
5. **Failed parse = next slice's roadmap.** Log expected-set on every failure. Weighted by query frequency, that log is auto-prioritised ingest backlog. Without it the next slice is hand-picked (worse).
6. **The Applicative→Monad boundary is the archive→brain boundary.** The Monad (capture loop) is what makes the system *learn*. Without it: sophisticated search. With it: an intelligence that compounds.
7. **CC brain could be its own repo.** Composition by typed extension implies brain-as-library. `unbasic-brain` could be a dep that `cc-brain` imports. Other client brains compose the same way. This is the right factoring once the pattern hardens at Slice 5.
8. **Promotion rule is required.** Without "when does a Layer 5 observation graduate to Layer 4," everything ends up at Layer 5 and Layer 4 never accumulates. Pattern: ≥3 caves consistent + human-reviewed promotion.
9. **Write discipline = source vs derived separation.** Two clocks only works if Heartbeat-files are re-derivable and Nerve-files are not. Mixing = corrupting the parser. The folder layout must encode the discipline (e.g. derived files under `<cave>/derived/` or marked by frontmatter).
10. **The approval UI is the unsolved prerequisite.** Capture loop assumes a human approves a diff. Brooke won't open a PR. Until the approval surface lands (Slice 4 latest), the capture loop is half-built. Best Slice-0 stopgap: git directly, technical users only.

---

## 10 · Open questions (for the Max chat / next decision)

1. **Brain factoring.** Keep CC nested in unBasic? Or extract to its own repo when patterns harden? Affects Slice 5+ shape.
2. **Promotion mechanism.** Who decides Layer 5 → Layer 4? Same approver as capture loop? Different cadence?
3. **Approval UI.** Git direct vs thin intake. Brooke/Max/Maddi access pattern? Sequenced into which slice?
4. **Empty-seam contract for `ads-context.md`.** 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.
5. **Mailbox identity for the email nerve.** Once Joe + Max are gws-authed, whose mailbox does the nerve actually read? Shared `info@unbasic.com.au` mailbox? Or per-person + filter for cave-owner senders?

---

## 11 · 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.
