Imported from pandazki/pneuma-skills (
modes/cosmos/skill/SKILL.md). Install upstream withnpx skills add pandazki/pneuma-skills --skill skill. Copyright stays with the author.
Cosmos
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 rewritecosmos.jsonfrom scratch.onboard— User wants a guided tour. Make suretour[]incosmos.jsonis good (refresh it if stale), then step through it withnavigate-toper 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.nameandproject.description,node.nameandnode.summaryandlanguageNotes,layer.labelandlayer.description, edgedescription, everytour[].narrative, everyperspectives[].name/insight/evidence. Keep stable identifiers and the vocabulary in English (kebab-case): allidfields,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.jsonis 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.typeis open string, but pick a coherent vocabulary per cosmos. Don't mixfile+characterin 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 everylayerIdexists inlayers[]. 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 optionaldirection. Default direction isforward. Usebidirectionalsparingly — 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", apassageref pinpointing where in that transcript the inference rests beats no ref at all. -
Set
project.sourceRootwhen 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 intocosmos.project.sourceRoot. Two reasons it matters:- 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).
- Relative
path/filevalues onCosmosSourceRefresolve againstsourceRoot, 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"}withoutsourceRootset produces "Path does not exist" when the user clicks the chip. SetsourceRoot, OR write absolute paths. Don't write bare relative paths and hope the viewer guesses.
Leave
sourceRootundefined 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 chosen editor via the editor picker — bridge upgrade WIP) |
url |
{ url } |
Default browser |
passage |
{ file, locator, quote? } |
Underlying file (auto-jump to locator deferred — locator + quote shown in 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 now also accepts an optional locator?: string and
excerpt?: { path, caption? }. See the new Visual anchoring
chapter 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:
{
"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:
{
"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):
{
"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
fileref 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 now 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.
(passage already has a required locator field — same idea,
already there. The new optional locator covers the other five
kinds.)
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(requiresbrew install poppler; Linux:apt install poppler-utils) Producesout-N.png. The-r 150gives a readable 150-dpi crop. Fallback ifpdftoppmis 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), orsips --cropToHeightWidth <h> <w> --cropOffset <x> <y> input.png --out cropped.pngfor a specific region. Verify the flag against the agent's machine if you're unsure —sips --helplists 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(requiresbrew install ffmpeg). Put-ssbefore-ifor a fast seek. - Web page region: the agent can't run a headless browser in
MVP. If the source is a live URL, fall back to a chip + locator
only, or ask the user to paste the screenshot they want as a
workspace file and reference its path in
excerpt.
Per-domain guidance
- Research / academic paper — every claim / evidence / method
node gets a PDF page or figure excerpt +
locatorlike"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
fileref + 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. Thefileref + 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 —
passageref withquotelifted verbatim (≤80 chars) +locatorlike"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,
locatornaming 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 +
locatorlike"5:32". Thevideokind already has an optionalt— use it alongsideexcerpt.pathso the chip click jumps the player to the moment too. - Conversation / transcript —
passageref withquote+locatorlike"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
importsedge 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
supportsedge can cite the section that establishes the support — different from citing the supporting claim's source, which lives on the node. - Fiction: a
vanished_nearedge 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
Workflowtool (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 atrustlevel. You stay the author — you survey, launch it, add the tour, writecosmos.json. - Path B — in-context (everything else). A short story, a paper, a
conversation, a small folder — or any input when the
Workflowtool 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
-
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), avocabulary({ nodeTypes, edgeTypes }), the workinglanguage, the absolutesourceRoot, and aprojectName.
-
Launch the workflow:
Workflow({ scriptPath: ".claude/skills/pneuma-cosmos/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). -
Take the returned graph. It returns
{ version, kind, project, nodes, edges, layers, perspectives?, stats }— every node already carries atrustverdict and realsources[], andperspectives[](if present) have already survived a judge panel for groundedness. Readstats.trust: a healthy run is mostlyverified; a pile ofunverifiablemeans 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 emptywarningsis 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) andempty(its paths held nothing) are both holes in the projection, andduplicate-onlymeans two of your partitions overlap. Each entry instats.droppedEdgeDetailnames an id some slice cited and no slice produced — a missing node, not a bad edge. Do not writecosmos.jsonon 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.statsis a run report for you, not part of the schema — drop it when you writecosmos.json, or the file fails validation. -
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). Writetour[]as{ step, nodeId, narrative }per beat — not the{ focus, narrative }shape the workflow used forperspectives[].steps[]. The workflow's perspectives sit right above your tour in the same file, so it is easy to copy theirfocusshape by reflex; see the contrast box in Perspective tours before you write. If the graph came back denser than a reader needs, prune trivial nodes here before writing. -
Write
cosmos.jsonatomically, thencapture({})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
-
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.
-
Pick a vocabulary. Open
references/node-type-vocabularies.mdand 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. -
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).
-
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 asources[]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 setcategorywhen the node clearly belongs to one (CODE / DOCS / INFRA / DATA / DOMAIN / KNOWLEDGE), andcomplexitywhen you have a sense (the viewer's layer cards aggregate it). Tags are optional and only worth including when they help search. -
Pass 2 — extract edges. Re-walk with an eye for connections. Use specific verbs (
discovers,authored,vanished_near) over generic ones (relates_to). Add adescriptionwhen the verb alone doesn't tell the story. -
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:"tour": [ { "step": 1, "nodeId": "ct-published-language", "narrative": "..." }, { "step": 2, "nodeId": "sp-omne-core-v1", "narrative": "..." } ]stepis a 1-based integer,nodeIdis a single node id (string, not an array),narrativeis this beat's paragraph. Do not writefocushere —focusbelongs toperspectives[].steps[], which is a different field with a different shape (see the contrast box in Perspective tours). -
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 orderedsteps[]array; each step has its ownfocus(1+ node ids that light up together) and its ownnarrativeparagraph (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. -
Write
cosmos.jsonatomically. Single Write call. Then immediatelycapture({})to look at the result — verify readability before you tell the user it's done. -
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
narrativemust 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 overallinsighton 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):
- 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:
{
"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:
<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:
{
"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:
<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, usepassagerefs withquote. 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'sparentSubgraphIdfield; 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 shortmessageexplaining 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 |