---
name: pneuma-cosmos
description: Build and explore a 3D knowledge graph from code, research, stories, or business material in the Pneuma Cosmos workspace. Use for creating, extending, or explaining cosmos.json and grounded graph projections.
---

# Cosmos

In script examples, `<SKILL_DIR>` means the actual directory containing this
loaded `SKILL.md`. Substitute its full path and keep shell paths quoted. The
runtime installs it under `.claude/skills` for Claude Code, `.agents/skills` for
Codex, or `.kimi-code/skills` for Kimi; use the path given in your instructions.

<!-- pneuma:start -->

## Scene

You and the user are doing a **structured projection** together. The
user brings content — a codebase, a short story, a research paper, a
business workflow, a long-form thread — and you turn it into a *cosmos*:
a graph of typed nodes and labeled edges that lays the work's inner
shape bare. You read, you pick a vocabulary that fits the content's
domain, you write a single `cosmos.json`. The user explores it as a
player in a live viewer — they pan, zoom, click, ask you to dive
deeper, or redirect your attention to a slice they care about.

"Structured" is what *the projection* is — not a precondition on the
input. Prose can be projected. Conversations can be projected. A photo
album with captions can be projected.

## Viewer contract

The viewer subscribes to `cosmos.json`. Every time you write it,
the viewer re-lays out and re-renders. The user sees the change live.

### What the user can select

A single node — by clicking it. When they do, the chat they send next
will be prefixed with a `<viewer-context>` block carrying the node's
`address` (machine-routable) plus its label and summary. You can copy
the address verbatim back into actions that take one.

### ViewerAddress vocabulary

| Key | Kind | Meaning |
|---|---|---|
| `nodeId` | coarse | A specific concrete node in the cosmos (e.g. `{ nodeId: "c-eliot" }`). |
| `layerId` | coarse | A layer slice (e.g. `{ layerId: "clues" }`). Use this for layer-level operations like `focus-layer`. |
| `perspectiveId` | coarse | A **perspective tour** — a variant walk through the cosmos framed by one design lens (e.g. `{ perspectiveId: "perspective-closed-loop" }`). `navigate-to` an address with this key starts that tour. |
| `subgraphId` | coarse | A user-driven **drill subgraph** (e.g. `{ subgraphId: "subgraph-cybernetic-loop-deep" }`). `navigate-to` enters the subgraph view; emit one in a `<viewer-locator>` after writing the subgraph so the user can click into it. See the *Drill-down* chapter. |

Nodes, perspective tours, and drill subgraphs are the atomic addressable units. There are no fine keys in v0.1.

### Actions you can invoke

- **`navigate-to({ address: { nodeId } })`** — Move the viewer to that
  node and select it. Use after writing or refining a region of the
  cosmos so the user immediately sees what changed.
- **`focus-layer({ address: { layerId } })`** — Dim everything outside
  that layer. Use when the user asks a layer-specific question
  ("show me only the clues", "what's in the service layer?") or when
  you're guiding them through one slice at a time.
- **`fit-view()`** — Zoom out to fit the whole cosmos. Use when you
  want the user to see the big picture before diving in.
- **`switch-persona({ persona })`** — `overview` (labels only),
  `learn` (labels + summaries — the default), `deep-dive` (everything,
  including tags). Switch up for "give me detail", switch down for
  "step back and orient me".
- **`capture({ address? })`** — Framework-built-in. Take a screenshot
  of the cosmos (or a specific node) and read the resulting PNG
  yourself. Use this to **visually self-verify** that the layout
  reads cleanly — too many edges in one node? a cluster looks
  disconnected? the user shouldn't be the one to catch that.

### Commands the user can trigger from the viewer

- **`regenerate`** — User says "the input changed, redo the cosmos".
  Re-read inputs and rewrite `cosmos.json` from scratch.
- **`onboard`** — User wants a guided tour. Make sure `tour[]` in
  `cosmos.json` is good (refresh it if stale), then step through it
  with `navigate-to` per step, narrating each.

## Core rules

- **Match the user's working language for prose, English for the type
  system.** Pneuma surfaces the user's locale in three places — listed
  in order of authority: (1) the `<system-info user-locale="…">`
  wrapper around your greeting, (2) the `<pneuma:env user_locale="…"
  />` tag in the first system message of the session, (3) the
  `<!-- pneuma:preferences -->` block in your instructions (free-form
  prose, e.g. "主要工作语言为中文"). When the env signal is set, follow
  it; when only the preferences block speaks, follow that. Apply the
  resolved language to every field a human reads as prose:
  `project.name` and `project.description`, `node.name` and
  `node.summary` and `languageNotes`, `layer.label` and
  `layer.description`, edge `description`, every `tour[].narrative`,
  every `perspectives[].name` / `insight` / `evidence`. Keep stable
  identifiers and the vocabulary in English (kebab-case): all `id`
  fields, `node.type` (`file`, `character`, `claim`, …), `edge.type`
  (`imports`, `discovers`, `supports`, …), `perspectives[].lens`
  (`orthogonality`, `cybernetic-loop`, `tension`, …). These function
  as a type system, not as display text. **Don't be misled by the bootstrap
  seed** — it's English because pneuma-skills itself is an English
  codebase. When you project the user's own content, follow *their*
  language. For names that live in the source (filenames, character
  names, function names, paper titles), keep them verbatim regardless
  of language — those are quoting the source, not labeling it.

- **One cosmos per workspace.** `cosmos.json` is the single source of
  truth at the workspace root. Don't shard. If the user wants two
  different projections of the same content, that's two workspaces.

- **`node.type` is open string, but pick a coherent vocabulary per
  cosmos.** Don't mix `file` + `character` in the same cosmos — that
  signals two different domain projections fighting for the same
  graph. If the user's content has multiple natures (e.g., code + its
  docs), pick the dominant lens and surface the rest as edges /
  attributes, not parallel node-types.

- **Every node has a `layerId`, and every `layerId` exists in
  `layers[]`.** Layers drive color + grouping in the viewer. Three
  to six is the comfortable range; going to seven or eight is fine
  if each layer represents a genuinely distinct concern you can name
  in one sentence. Below three feels under-modeled.

- **Node ids are stable kebab-case prefixed by short layer hint.**
  Examples: `c-eliot` (character), `cl-x-mark` (clue), `fn-auth-login`
  (function). Stability matters: when you re-project, keep old ids
  for objects the user has already explored.

- **Edges have a `type` (verb) and optional `direction`.** Default
  direction is `forward`. Use `bidirectional` sparingly — it usually
  means you haven't decided which side owns the relationship.

- **Summaries are one sentence, two at most.** The viewer renders
  them in cards; long summaries break the layout and the gestalt.

- **Tours are 5–8 steps.** If you can't tell the story in eight, the
  cosmos is doing too much or the layers are wrong.

- **Every node should cite its source(s).** Cosmos is domain-agnostic
  — the same protocol covers code, prose, research, conversations,
  domain models. The way the user *verifies* a node is by jumping
  to what produced it. Use `node.sources[]` (see *Source references*
  below) to attach one or more refs per node: a code node cites the
  file(s) it abstracts; a character node cites the chapter passages
  the character appears in; a claim node cites the paper section and
  the dataset URL. Even when the source is "the conversation we just
  had", a `passage` ref pinpointing where in that transcript the
  inference rests beats no ref at all.

- **Set `project.sourceRoot` when the cosmos has an on-disk root —
  and make sure relative source paths resolve against it.**
  For codebase / notes-folder / manuscript-directory cosmoses, write
  the absolute path of the source root into `cosmos.project.sourceRoot`.
  Two reasons it matters:

  1. The viewer's INFO tab renders an "Open project root in editor"
     affordance — one click opens the whole project; subsequent file
     source-chip clicks activate the file inside the open editor
     window (Cursor / VS Code behaviour).
  2. **Relative `path` / `file` values on `CosmosSourceRef` resolve
     against `sourceRoot`, NOT against the session directory.** The
     session lives at `<projectRoot>/.pneuma/sessions/<id>/`, which
     contains no source material — so a ref like `{kind: "file",
     path: "src/main.ts"}` without `sourceRoot` set produces "Path
     does not exist" when the user clicks the chip. Set `sourceRoot`,
     OR write absolute paths. Don't write bare relative paths and
     hope the viewer guesses.

  Leave `sourceRoot` undefined only when the cosmos genuinely has no
  on-disk root (web URLs, transcripts, ad-hoc scattered files) — and
  in that case use absolute paths or full URLs on every ref.

## Source references

Every node carries `sources?: CosmosSourceRef[]` — one or more
pointers back to the artifacts that produced the node. The viewer
renders each ref as a click-to-open chip in the INFO panel; the
chip's behaviour depends on `kind`.

### The six kinds

| kind | Shape | Opens with |
|---|---|---|
| `file` | `{ path, range? }` | OS default app, or the editor chosen in the editor picker |
| `url` | `{ url }` | Default browser |
| `passage` | `{ file, locator, quote? }` | Underlying file — the chip opens it without jumping; the locator and quote show in the tooltip |
| `image` | `{ path }` | OS default image viewer |
| `audio` | `{ path, t? }` | OS default audio app |
| `video` | `{ path, t? }` | OS default video app |

All kinds accept an optional `label` to override the auto-derived
chip text.

Every kind also accepts an optional `locator?: string` and
`excerpt?: { path, caption? }`. See *Visual anchoring* for what
they're for and the shell commands to populate them.

### Per-domain examples

**Codebase node** — typically one or more `file` refs, often with
ranges narrowing to the relevant span:

```json
{
  "id": "ct-mode-manifest",
  "type": "contract",
  "name": "ModeManifest",
  "summary": "...",
  "sources": [
    { "kind": "file", "path": "core/types/mode-manifest.ts", "range": [1, 120] },
    { "kind": "file", "path": "modes/cosmos/manifest.ts", "label": "example impl" }
  ]
}
```

**Fiction node** — `passage` refs pinpointing where a character
appears, with a lifted quote so the user can verify the inference:

```json
{
  "id": "c-eliot",
  "type": "character",
  "name": "Eliot Vance",
  "summary": "...",
  "sources": [
    { "kind": "passage", "file": "chapter-03.md", "locator": "¶12-14",
      "quote": "Eliot's hand trembled as he reached for the bell, and the housekeeper saw it." },
    { "kind": "passage", "file": "chapter-07.md", "locator": "¶3" }
  ]
}
```

**Research node** — mix of `file` (paper PDF or transcript) and
`url` (data source, related work):

```json
{
  "id": "claim-baseline-undertrained",
  "type": "claim",
  "name": "The baseline model was undertrained on long-context tasks",
  "summary": "...",
  "sources": [
    { "kind": "file", "path": "paper.pdf", "label": "§4.2" },
    { "kind": "url", "url": "https://arxiv.org/abs/2410.12345" },
    { "kind": "file", "path": "data/long-bench.csv" }
  ]
}
```

### Discipline

- **Cite first, narrate second.** If you can't point at *something*
  that produced a node, you're inventing. Add a source even when
  the source is your own previous reasoning ("conversation, turn
  15") — write that as a passage ref into a saved transcript.
- **Multiple refs are fine.** Real nodes usually have 1–3; over
  four feels like the node is doing too much. Split it.
- **Don't pad refs.** A `file` ref to a 5000-line file with no
  range is barely a ref — the user can't trust the link. Either
  add a range or pick a more specific source.
- **Order matters.** Put the most authoritative ref first; the
  chip strip preserves order, and the user reads left to right.
- **Excerpt > chip.** When you can crop a real visual extract from
  the source, do it — chips are functional, excerpts are persuasive.

### Legacy schema note

Older cosmos files used a single string field `source: string` plus
an optional `lineRange: [start, end]`. The viewer's parser migrates
those to `sources[0]` (as a `file` or `url` ref depending on
http(s) prefix), so older files keep working. New files should
write `sources[]` directly and skip the legacy fields.

## Visual anchoring — every node deserves a way home

Users open a cosmos to dive into source. They click a node, read
your summary, and the very next thing they want is to see the
artifact that produced it — the actual paragraph, the actual page,
the actual frame. A node without a visual anchor is a node they
can't trust. Chips get them there in one click; **excerpts get them
there in zero**.

So: when the source has a real visual you can lift from it, lift
it. Crop the PDF page, save the video frame, screenshot the UI
region. Put the path in `excerpt.path` and the viewer renders it
inline above the chip strip. The chip remains as the open-the-real-
thing affordance underneath.

### The excerpt rule — non-negotiable

Every `excerpt.path` MUST be a real extract from the source.

- ✅ Cropped PDF page, figure lifted from a paper, screenshot of a
  UI region, video frame at a moment, scan of a manuscript page,
  diagram from the docs.
- ❌ AI-generated illustrations. Decorative graphics. Stock images.
  "Conceptual visualisations" of what the node is about.

Concept imagery belongs on the canvas as a node, not as an
excerpt on someone else's node. The whole point of the excerpt is
that the user can verify the inference at a glance — that trust
collapses the moment you smuggle in generated material.

If the source is genuinely non-visual (a transcript, a CLI log, a
blob of prose), don't fabricate an excerpt to fill the slot. Use a
`passage` ref with a `quote` instead — the viewer renders it as a
quote card with the same prominence, and the user still gets the
verifiable extract.

### Locator everywhere

Every ref kind accepts `locator?: string`. Open-ended hint
about *where inside* the artifact this node points. Examples:

- `"p.23"` — PDF page
- `"5:32"` — moment in audio or video
- `"figure 3"` / `"§4.2"` — section / figure in a paper
- `"verse 14"` / `"ch.7 ¶3"` — passage in prose
- `"slide 7"` — deck
- `"bottom-left"` / `"nav-header"` — region in an image / UI
- `"turn 14"` — turn in a transcript

Whatever phrasing lets the user find the spot in five seconds.

(On `passage` the `locator` is required — same idea; on the other
five kinds it is optional.)

### Shell commands — populate excerpts without leaving the agent

These run on macOS by default; Linux alternatives noted where
they diverge. Don't ask the user to install anything you don't
need — fall back to chip-only if a tool isn't around.

- **PDF page → PNG:**
  `pdftoppm -f N -l N -png -r 150 paper.pdf out`
  (requires `brew install poppler`; Linux: `apt install poppler-utils`)
  Produces `out-N.png`. The `-r 150` gives a readable 150-dpi crop.
  Fallback if `pdftoppm` is missing on a single-page PDF:
  `sips -s format png paper.pdf --out page.png`.
- **Image crop:**
  `sips -c <h> <w> input.png --out cropped.png` (center crop), or
  `sips --cropToHeightWidth <h> <w> --cropOffset <x> <y> input.png --out cropped.png`
  for a specific region. Verify the flag against the agent's
  machine if you're unsure — `sips --help` lists exact arg names.
  Linux: `convert input.png -crop <w>x<h>+<x>+<y> cropped.png` (ImageMagick).
- **Video frame at t seconds:**
  `ffmpeg -ss N -i video.mp4 -frames:v 1 -q:v 2 out.png`
  (requires `brew install ffmpeg`). Put `-ss` before `-i` for a
  fast seek.
- **Web page region:** don't count on capturing a live URL yourself.
  Fall back to a chip + locator only, or ask the user to drop the
  screenshot they want into the workspace and reference its path in
  `excerpt`.

### Per-domain guidance

- **Research / academic paper** — every claim / evidence / method
  node gets a PDF page or figure excerpt + `locator` like `"p.N"`
  or `"figure N"`. Quotes are fine as backup, but when the source
  is visual a cropped figure trumps prose every time.
- **Codebase** — primary cite is a `file` ref + range. Excerpts
  are OPTIONAL — only when there's a real visual associated (an
  architecture diagram in the docs, a UI screenshot for a frontend
  component, an SVG asset). **Don't excerpt code as image.** The
  `file` ref + line range is already the right primitive — a
  screenshot of source code is worse on every axis (no copy, no
  scroll, dies on theme change).
- **Fiction / long-form prose** — `passage` ref with `quote` lifted
  verbatim (≤80 chars) + `locator` like `"ch.3 ¶12"`. Skip excerpt
  unless the book has illustrations and the node is about one.
- **UI / product analysis** — screenshot of the UI region, crop
  tightly, `locator` naming the area (`"nav-header"`,
  `"empty-state"`, `"settings: privacy tab"`). The viewer's image
  card sits right where the user expects to see the thing you're
  describing.
- **Video / talk** — frame at the moment in question + `locator`
  like `"5:32"`. The `video` kind already has an optional `t` —
  use it alongside `excerpt.path` so the chip click jumps the
  player to the moment too.
- **Conversation / transcript** — `passage` ref with `quote` +
  `locator` like `"turn 14"`. Excerpts only when the conversation
  includes shared images you want to lift.

### Where excerpts live on disk

Default convention: `.cosmos-assets/<node-id>/` at the workspace
root. Keeps the cosmos.json clean of giant inline binaries and
makes the asset tree shippable alongside the cosmos.

Examples: `.cosmos-assets/claim-baseline/p23.png`,
`.cosmos-assets/c-eliot/manuscript-folio.jpg`. Use your judgement
when the source itself already lives somewhere obvious — a figure
already saved next to the paper at `paper/figures/fig-3.png`
doesn't need to be copied; reference it where it sits.

### Edges with sources

`CosmosEdge.sources?: CosmosSourceRef[]` — same shape nodes use.
Most edges don't need sources; use sparingly and only when the
relationship claim itself benefits from grounding. Examples worth
the citation:

- Codebase: an `imports` edge can cite the import statement's line
  range — useful when "A imports B" is non-obvious because the
  import is conditional or aliased.
- Research: a `supports` edge can cite the section that
  establishes the support — different from citing the supporting
  claim's source, which lives on the node.
- Fiction: a `vanished_near` edge can cite the passage that
  establishes the disappearance.

If an edge is mechanical (a `contains` edge from a folder to a
file), don't bother — the source belongs on the nodes, not on
the link between them.

## Workflow

When the user opens a fresh workspace, the seed gives them a
**bootstrap cosmos** — Pneuma Skills projecting *itself* (contracts,
runtime, backends, modes, shell, reference, ~55 nodes / 6 layers).
Read `README.md` and skim `cosmos.json`; understanding the seed is
understanding what cosmos is for. The user typically asks you to
re-project their own content next — that's where you start working.

### Choosing a projection path

The first decision each projection is **which of two paths** produces it:

- **Path A — Workflow-backed (large codebases, Claude Code).** When the
  input is a codebase or a folder too large to read well in one context,
  AND you have the `Workflow` tool (Claude Code backend only), hand the
  heavy lifting to the projection workflow. It reads every partition in
  its own fresh context — so coverage isn't capped by *your* context —
  resolves cross-partition edges, and does the thing you cannot do
  reliably under context pressure: **verifies every node against its
  cited sources**, tagging each with a `trust` level. You stay the
  author — you survey, launch it, add the tour, write `cosmos.json`.
- **Path B — in-context (everything else).** A short story, a paper, a
  conversation, a small folder — or *any* input when the `Workflow` tool
  is absent (Codex / Kimi backends). You read and project it yourself,
  in the passes below.

**How to choose:** survey first — Glob the source, count files. A handful
of files or a token-light single document → Path B. Dozens of source
files / a real repo you can't hold in one context → Path A. No `Workflow`
tool → always Path B. Path A currently covers the **codebase** domain
only; fiction / research / business go through Path B until their
workflow paths land.

### Path A — Workflow-backed projection

1. **Survey (cheap, in-context).** Glob the source; read `README`,
   package manifests, entry points. From that decide:
   - `partitions[]` — one per coherent subsystem (usually a top-level
     dir): `{ id, label, paths: ["<relative dir/file>", …], hint }`.
     Each partition is read by its own fresh-context subagent, so
     partition by *concern a reader cares about*, not by file count.
   - a draft `layers[]` table (3–6, by architectural concern), a
     `vocabulary` (`{ nodeTypes, edgeTypes }`), the working `language`,
     the absolute `sourceRoot`, and a `projectName`.
2. **Launch the workflow:**

   ```
   Workflow({
     scriptPath: "<SKILL_DIR>/references/projection.workflow.js",
     args: { sourceRoot, language, projectName, partitions, vocabulary, layers, maxCompletenessRounds: 2 }
   })
   ```

   Watch it in `/workflows`: Extract (parallel per partition) → Merge →
   Verify → Complete (loop-until-dry). It spins up many subagents and
   costs real tokens — reserve it for inputs that genuinely exceed one
   context, never a ten-file folder (that's Path B).
3. **Take the returned graph.** It returns `{ version, kind, project,
   nodes, edges, layers, perspectives?, stats }` — every node already
   carries a `trust` verdict and real `sources[]`, and `perspectives[]`
   (if present) have already survived a judge panel for groundedness.
   Read `stats.trust`: a healthy run is mostly `verified`; a pile of
   `unverifiable` means the source wasn't where the extractor thought —
   investigate before you ship it.

   Then read `stats.warnings` — **a merge that loses something says so,
   and an empty `warnings` is the only run that covered what you asked
   for.** `stats.partitions[]` gives one row per slice: `failed` (its
   subagent never came back, even after the one retry) and `empty` (its
   paths held nothing) are both holes in the projection, and
   `duplicate-only` means two of your partitions overlap. Each entry in
   `stats.droppedEdgeDetail` names an id some slice cited and no slice
   produced — a missing node, not a bad edge. Do not write
   `cosmos.json` on top of these: re-survey the named paths and project
   the gap yourself (Path B passes over that one slice), or tell the
   user plainly which region is missing. A silently thin cosmos is
   worse than a small one, because it looks finished.

   `stats` is a run report for *you*, not part of the schema — drop it
   when you write `cosmos.json`, or the file fails validation.
4. **Add the tour (and prune if needed).** The workflow writes the
   verified graph + perspectives but NOT the overall `tour` — pick the
   5–8 nodes that teach how the cosmos hangs together (tour discipline
   below). Write `tour[]` as `{ step, nodeId, narrative }` per beat —
   see the contrast box in *Perspective tours* for how it differs
   from `perspectives[].steps[]`. If the graph came back denser than a
   reader needs, prune trivial nodes here before writing.
5. **Write `cosmos.json` atomically, then `capture({})`** to eyeball
   readability, `fit-view()`, and drop a `<viewer-locator>` to the most
   striking node — the same finish as Path B step 8–9.

### Path B — in-context projection

1. **Read the input.** Whatever the user has dropped into the
   workspace — a file, a folder, a chapter — use Read / Glob / Grep
   liberally. The projection's quality is gated by how much of the
   content you actually saw. If the user hasn't dropped anything
   yet, ask them what they want to project.

2. **Pick a vocabulary.** Open `references/node-type-vocabularies.md`
   and find the closest match. Note 3–6 node types and 5–10 edge
   types you'll use. **Don't lock it in too early** — re-pick after
   you've read more.

3. **Sketch the layer table.** Layers are *what kind of node*, not
   *what part of the work*. Characters / events / clues for fiction;
   API / service / data / UI for code. 3–6 layers; assign a color
   per layer (use the seed's palette as a starting point).

4. **Pass 1 — extract nodes.** Walk the content sequentially, emit
   nodes as you encounter referents. Give each a stable id, name,
   `layerId`, one-sentence summary, **and a `sources[]` array**
   citing the file paths / passages / URLs that produced it. The
   sources are not optional polish — they're how the user verifies
   your work (see *Source references* chapter for the six kinds and
   per-domain examples). Also set `category` when the node clearly
   belongs to one (CODE / DOCS / INFRA / DATA / DOMAIN / KNOWLEDGE),
   and `complexity` when you have a sense (the viewer's layer cards
   aggregate it). Tags are optional and only worth including when
   they help search.

5. **Pass 2 — extract edges.** Re-walk with an eye for connections.
   Use specific verbs (`discovers`, `authored`, `vanished_near`) over
   generic ones (`relates_to`). Add a `description` when the verb
   alone doesn't tell the story.

6. **Pass 3 — write the tour.** Pick the 5–8 nodes that, in order,
   teach the user how the cosmos hangs together. The tour is a
   reading path, not a complete tour of every node. The shape is a
   flat array of `{ step, nodeId, narrative }` — **not** the
   perspective-step shape:

   ```jsonc
   "tour": [
     { "step": 1, "nodeId": "ct-published-language", "narrative": "..." },
     { "step": 2, "nodeId": "sp-omne-core-v1",       "narrative": "..." }
   ]
   ```

   `step` is a 1-based integer, `nodeId` is a single node id (string,
   not an array), `narrative` is this beat's paragraph. Do **not**
   write `focus` here — `focus` belongs to `perspectives[].steps[]`,
   which is a different field with a different shape (see the contrast
   box in *Perspective tours*).

7. **Pass 4 — write perspective tours.** (Optional but encouraged.
   See the *Perspective tours* chapter below before doing this.) Step
   back from the facts you just wrote. What design lenses make the
   cosmos read differently — what variant walks would teach the user
   something the overall tour can't? Add 0–6 `perspectives[]` entries.
   Each perspective is a *walk*, not a tag — it has a thesis
   (`insight`) and an ordered `steps[]` array; each step has its own
   `focus` (1+ node ids that light up together) and its own
   `narrative` paragraph (what the user is reading on THIS beat, not
   the thesis again). Per-step narratives are the discipline knob: if
   you can't write a distinct paragraph per step, the perspective
   isn't sharp enough yet.

8. **Write `cosmos.json` atomically.** Single Write call. Then
   immediately `capture({})` to look at the result — verify
   readability before you tell the user it's done.

9. **`fit-view()` then drop a `<viewer-locator>` card pointing to the
   most striking node.** Make the user's first click satisfying.

### Refinement

When the user selects a node and asks for depth, **don't just write
prose** — update the cosmos. Add edges. Add adjacent nodes. Then
`navigate-to({ address: { nodeId } })` to bring them back to focus.

### Re-projection (responding to `regenerate`)

The input changed substantively. **Preserve node ids that still
exist** so the user doesn't lose their bearings. Diff-aware re-write
is preferable to nuke-and-pave when the input is incrementally
different.

### Guided tour (responding to `onboard`)

If `tour[]` is empty or stale, regenerate it. Then walk: for each
step, call `navigate-to` and send a short narrative message. Wait
for the user to nod (or react) before advancing.

## Perspective tours — variant walks through the cosmos

A cosmos has more than one way to be read.

`tour[]` is the **overall tour** — the single canonical reading path
the agent picks (5–8 steps) for "you've never seen this cosmos before,
here's how it hangs together". One cosmos, one overall tour.

`perspectives[]` are **variant tours** — alternate reading paths
through the *same* cosmos, each framed by a specific design lens:
"where does the system maintain a feedback loop?", "where do the v0.2
and v0.3 abstractions collide?", "where does entropy accumulate?".
Each perspective answers one such question by ordering the nodes
that bear on it and giving the through-line. Same cosmos, different
walks.

The viewer surfaces overall + perspectives side-by-side in the TOUR
tab. The user picks one. Picking starts a stepper that walks the
chosen nodes in order, with the framing (lens + thesis) shown
alongside.

### Two "steps", two different shapes — don't conflate them

Both the overall tour and each perspective have an ordered walk, and
both call the beats "steps". **The field shapes are different**, and
mixing them is the single most common cosmos mistake — the symptom is
a tour whose step numbers don't render and whose canvas won't navigate,
because you wrote `focus` where the viewer expects `step` + `nodeId`.

| | Field | One beat's shape | Node reference |
|---|---|---|---|
| **Overall tour** | `cosmos.tour[]` | `{ step: number, nodeId: string, narrative }` | `nodeId` — a **single string** |
| **Perspective walk** | `cosmos.perspectives[].steps[]` | `{ focus: string[], narrative }` | `focus` — an **array of ids** |

The tour numbers its beats (`step: 1, 2, …`) and lights exactly one
node each (`nodeId`). A perspective does **not** number its beats and
can light several nodes per beat (`focus: ["a", "b"]`, first is the
primary anchor). `narrative` is the only field they share. If you find
yourself writing `focus` inside `tour[]` or `nodeId` inside a
perspective step, stop — you've crossed the two.

### The discipline

A perspective isn't a slogan with a few node-id fig leaves. Three
practices keep it honest:

- **Facts before perspectives.** Don't write `perspectives[]` until
  you've finished passes 1–3 (nodes, edges, overall tour). A
  perspective earned by stepping back from concrete material reads
  differently from one composed before the material exists; the user
  can tell even if they can't articulate why.

- **Every perspective must be grounded.** Each step's `focus[]`
  names the concrete node ids the beat lands on. If you can't pick
  specific ones for every step, the perspective isn't earned.
  Delete it. The grounded list is what separates "you can actually
  take this walk" from "I'm narrating a vibe".

- **Per-step narratives, not a refrain.** Each step's `narrative`
  must be its **own** paragraph — what the user is reading on THIS
  beat, why these specific focus nodes matter here, what the next
  beat will pivot to. Repeating the perspective's overall `insight`
  on every step is the tell that you only had one thing to say.
  Cut the perspective down to fewer steps and write each one
  sharply, rather than padding the manifestsIn list.

A perspective that walks five focused beats with five distinct
narratives is worth more than one that lists eight nodes and
repeats the thesis verbatim each step.

### Lenses — vocabulary, not a checklist

Perspectives aren't a fixed catalogue. They're a **way of looking** —
and the agentic-systems literature has crystallized a handful of
mental models that name this kind of looking. Treat them as
**lenses** to set on a perspective you're already perceiving, not as
**boxes** to fill.

Some lenses worth recognizing (deeper catalog + per-domain examples
in [`references/perspective-lenses.md`](references/perspective-lenses.md)):

- **Orthogonality** — two axes that vary independently. *Watch
  for*: pairs of enums that never co-occur; modules that share a
  data path but never each other's caller.
- **Cybernetic loop** — a subsystem holding [property] against
  [perturbation]. *Watch for*: explicit retries, idempotency tags,
  feedback edges, "watchdog" / "reconcile" naming.
- **Entropy gradient** — where chaos grows in the system, where it's
  resisted. *Watch for*: directories with sprawling tests vs. tight
  cores; layers that accumulate config; "drift" / "stale" in TODOs.
- **Self-similarity (fractal)** — the same structure at multiple
  scales. *Watch for*: a Plan that mirrors a Project that mirrors a
  Task; an Agent that itself contains a swarm of sub-agents.
- **Causal chain** — X causes Y causes Z, not just correlated.
  *Watch for*: dependency chains where each step is the *because*
  of the next, not just a precondition.
- **Tension** — two forces pulling the design opposite directions,
  consciously held. *Watch for*: contradictory constraints
  documented as ADRs; classes whose comments argue with each other.
- **Convergence point** — many concerns funnel through one node.
  *Watch for*: high in-degree, "the place where everything lands";
  a single file every other layer reaches.
- **Layered translation** — raw → structured → semantic → action,
  each layer translating from previous. *Watch for*: pipelines
  where adjacent stages don't share a vocabulary.
- **Hidden hand** — an offstage agent shapes events. *Watch for*:
  nodes with no outbound edges that are referenced everywhere; a
  policy file no one calls but everyone honors.
- **Paradigm shift / Succession** — the work is mid-migration from
  one set of axioms to another. *Watch for*: v1 + v2 living side
  by side; type names with "Legacy" / "v0" suffixes; ADRs that
  reverse earlier ADRs.

When none of these lens names fit, invent. `perspectives[].lens` is
open vocabulary like node types are. The list above is *help*, not
*grammar*.

### When to write perspectives, when not to

Do reach for perspectives when:

- The user has lived with the facts cosmos for a beat and is asking
  meta questions ("what's the *shape* of this thing?", "where would
  you place a new feature?").
- You re-project an evolving system and the same patterns keep
  re-emerging — name them.
- The cosmos is large enough (≥ ~25 nodes) that the overall tour
  alone misses material the user would benefit from walking.

Don't reach for perspectives when:

- The cosmos is small (< 15 nodes). The overall tour covers it.
- You haven't read enough. Premature perspectives are the worst
  kind — slogans wearing a node-list.
- The user asks a concrete question. Answer it with concrete nodes.
  Perspectives are for "what does it all add up to" / "what's
  another way to read this", not "what does this function do".

### Schema note

The field is `cosmos.perspectives[]`. Each entry:

```jsonc
{
  "id": "perspective-<slug>",        // stable kebab-case
  "name": "...",                       // noun phrase in user language
  "lens": "cybernetic-loop",           // open vocab, English kebab-case
  "insight": "...",                    // 1-paragraph thesis (user language)
  "steps": [
    {
      "focus": ["node-id-a"],          // 1+ nodes lit on this beat
      "narrative": "..."               // THIS step's paragraph
    },
    {
      "focus": ["node-id-b", "node-id-c"],  // multi-node beat OK
      "narrative": "..."
    }
  ],
  "evidence": "...",                   // optional — why the inference is reasonable
  "tags": ["..."]                      // optional
}
```

`steps[]` is the walk. Each step's `focus` is the set of node ids
the canvas lights up together on that beat; `narrative` is the
paragraph the sidebar shows. The viewer's INFO panel pins to
`focus[0]`; the rest stay lit alongside.

**Backward compat:** older cosmos files may have `tao[]` instead of
`perspectives[]`, `type` instead of `lens`, or `manifestsIn[]`
instead of `steps[]`. The viewer's parser normalizes all three to
the new shape — files with only `manifestsIn[]` get steps
synthesized with the perspective's `insight` reused as narrative
(renders, but degraded). New files should write `steps[]` with
distinct per-step narratives.

See `references/cosmos-schema.md` for the full schema.

## Drill-down — user-driven subgraphs

The overall tour and perspective tours are *your* pre-curated walks
through the cosmos: "I think you'll want to see things in this order".
Drill-down is the inverse channel: the user picks one or more nodes,
asks "go deeper here", and you generate a focused subgraph on demand.

This pattern is the cosmos's solution to the "must I analyze
everything upfront?" problem. The main graph stays a navigable
overview; depth is paid for where the user has shown interest.

### Recognizing a drill request

When the user clicks the **Drill into N nodes →** button on the
canvas, the viewer dispatches a `<drill-request>` block as a
system message just before your next turn. It looks like this:

```xml
<drill-request>
  <anchors ids="node-id-a,node-id-b,node-id-c" />
  <parent subgraph-id="subgraph-prev-drill" />   <!-- present only if drilling inside an existing subgraph -->
  <anchor-names>node-id-a (Foo), node-id-b (Bar), node-id-c (Baz)</anchor-names>
  <prompt>
The user's edited prompt — what they actually want you to analyze
about these anchors. May be in their working language.
  </prompt>
</drill-request>
```

When you see this block, do not respond with prose alone. Generate
a `CosmosSubgraph`, write it to `cosmos.json`, then emit a
`<viewer-locator>` card so the user can click into it.

### What you write

Append a new entry to `cosmos.subgraphs[]` with the full subgraph:

```jsonc
{
  "id": "subgraph-<slug-derived-from-prompt>",     // stable kebab-case
  "anchors": ["node-id-a", "node-id-b", "node-id-c"],
  "prompt": "<verbatim user prompt from the request>",
  "status": "ready",                                // or "pending" → "ready" if two-phase
  "generatedAt": "<ISO timestamp>",
  "parentSubgraphId": "subgraph-prev-drill",       // copy from the request when nested
  "title": "<short noun phrase — 4-8 words, user language>",
  "nodes": [
    {
      "id": "sg-<id>",                              // ids unique to this subgraph; can also reference main-graph node ids to link back
      "type": "...",
      "name": "...",
      "summary": "...",
      "sources": [...]                              // cite where the analysis comes from — see Source references
    }
  ],
  "edges": [
    { "source": "node-id-a", "target": "sg-some-new", "type": "...", "description": "..." }
  ]
}
```

Then in your chat reply emit:

```xml
<viewer-locator label="Open the new drill: <title>"
                address='{"subgraphId": "subgraph-<slug>"}'/>
```

The user clicks the card → canvas swaps into the subgraph view.

### Discipline

- **Take the prompt seriously.** The `<prompt>` text is what the
  user wants from THIS drill. If they asked "how do v0.2 and v0.3
  collide here?", the subgraph should organize nodes around that
  collision — not be a generic expansion of the anchors.
- **Anchor nodes are first-class context.** Read each anchor's
  full summary (and `sources[]` if relevant) before authoring the
  subgraph. The drill expands from what the user pointed at.
- **Anchors stay; new nodes scope.** Subgraph nodes can reference
  the anchors by id (edges link back to the main graph); ids
  unique to the subgraph render as new content scoped to this
  drill only. The viewer shows anchor nodes alongside subgraph
  nodes when it renders the drill, so the user reads "what's new
  here + where it attaches".
- **Cite sources for every new node.** Drills are inquiry — the
  user will want to verify your claims. `sources: [{kind: "file",
  path: "...", range: [...]}, ...]` is non-negotiable for code; for
  prose, use `passage` refs with `quote`. See the *Source
  references* chapter.
- **One subgraph per request.** Don't try to satisfy a drill with
  five subgraphs. If the user's question is too broad to answer in
  one focused expansion, write one subgraph that *names* the
  branches and let the user re-drill into whichever they want next.
- **Recursive depth is fine.** When `<parent subgraph-id="..."/>`
  is set, the user drilled inside an existing subgraph. Copy that
  id into your new entry's `parentSubgraphId` field; the viewer
  uses it to build a breadcrumb.
- **When you can't, say so.** If you genuinely can't answer the
  drill (you don't have the source material, the anchors don't
  share a coherent relationship, etc.), write a `status: "failed"`
  entry with a short `message` explaining why. Don't fabricate.

### When to drill autonomously

Most drills come from the user clicking the button. But the agent
can also write subgraphs *unprompted* when:

- The user asks a chat question whose answer is naturally a small
  graph rather than a paragraph ("walk me through how X depends on
  Y depends on Z").
- A perspective walk reaches a point where the next beat would
  need its own mini-graph — emit a subgraph linked from the
  perspective's last step instead of cramming all the nodes into
  the main graph.

In both cases, follow the same schema; just leave the `prompt`
field as a brief restatement of what triggered it, and emit the
`<viewer-locator>` for the user to navigate.

## References

| Topic | File |
|---|---|
| Full schema (TypeScript types + JSON shape + auto-fix rules) | `references/cosmos-schema.md` |
| Vocabulary catalogs by content domain (code / fiction / research / business / mixed) | `references/node-type-vocabularies.md` |
| Step-by-step projection workflows per domain | `references/projection-workflows.md` |
| **Perspective-lens catalog — mental models for variant tours** | `references/perspective-lenses.md` |

<!-- pneuma:end -->
