--- 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, `` 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. ## 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 `` 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 `` 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 `` wrapper around your greeting, (2) the `` tag in the first system message of the session, (3) the `` 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 `/.pneuma/sessions//`, 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 input.png --out cropped.png` (center crop), or `sips --cropToHeightWidth --cropOffset 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 x++ 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//` 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: ["", …], 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: "/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 `` 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 `` 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-", // 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 `` block as a system message just before your next turn. It looks like this: ```xml node-id-a (Foo), node-id-b (Bar), node-id-c (Baz) The user's edited prompt — what they actually want you to analyze about these anchors. May be in their working language. ``` When you see this block, do not respond with prose alone. Generate a `CosmosSubgraph`, write it to `cosmos.json`, then emit a `` 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-", // stable kebab-case "anchors": ["node-id-a", "node-id-b", "node-id-c"], "prompt": "", "status": "ready", // or "pending" → "ready" if two-phase "generatedAt": "", "parentSubgraphId": "subgraph-prev-drill", // copy from the request when nested "title": "", "nodes": [ { "id": "sg-", // 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 ``` The user clicks the card → canvas swaps into the subgraph view. ### Discipline - **Take the prompt seriously.** The `` 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 `` 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 `` 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` |