| name | wiki-evidence |
| description | Resolve a vault page (moment, entity, session, or synthesis) into the JSON shape the MemoryLane frontend's evidence sheet renders. Given a `block_id` (file basename without `.md`) and optional `node_type` hint, parses the page's YAML frontmatter, tokenizes the markdown body into typed blocks (h1, h2, p, quote, links), resolves `[[wikilinks]]` to connected node ids, and emits a single JSON object to stdout. Use this skill when the user invokes `wiki-evidence <block_id> [<node_type>]`, or when a backend route (`/api/evidence/{block_id}`) spawns claw with that prompt. NOT a user-facing conversational skill — the output is machine-readable JSON for the frontend.
|
Wiki Evidence — Per-Block Detail JSON
You are producing the JSON the MemoryLane frontend reads to render its evidence sheet (memorylane/knowledge-frontend/evidence.jsx). The shape is fixed — match it exactly. Do not invent fields, do not omit required fields, do not pretty-print or wrap in markdown fences. Stdout MUST be parseable as a single JSON object.
Before You Start
-
Resolve config — follow the Config Resolution Protocol in llm-wiki/SKILL.md to set OBSIDIAN_VAULT_PATH. For MemoryLane this resolves to memorylane/knowledge_base/.
-
Read $OBSIDIAN_VAULT_PATH/AGENTS.md — confirm the "Frontend type mapping" table exists; you'll need it to derive typeLabel. If absent, abort with JSON {"error": "vault not configured for evidence export"}.
-
Parse the prompt arguments. The user/caller passes one of:
wiki-evidence <block_id> — block_id only; you must scan all categories
wiki-evidence <block_id> <node_type> — node_type is one of memory|person|place|object|session|synthesis; resolve directly to the matching folder
If no block_id is given, abort with JSON {"error": "missing block_id"}.
Output Schema (fixed)
Print exactly one JSON object to stdout. No surrounding text, no markdown, no log lines. Schema:
{
"typeLabel": "memory · moment",
"title": "Arrived at the park",
"mediaTone": "#7b9a78",
"frontmatter": { "category": "moment", "tags": ["outdoor"], "created": "...", "...": "..." },
"body": [
{ "kind": "h1", "text": "Arrived at the park" },
{ "kind": "p", "text": "Walked through the park entrance..." },
{ "kind": "h2", "text": "Transcript" },
{ "kind": "quote", "text": "Hey, this is the place I was telling you about." },
{ "kind": "h2", "text": "Related moments" },
{ "kind": "links", "items": ["[[abc123_000089]]", "[[Maya]]"] }
],
"connected": ["abc123_000089", "maya"]
}
frontmatter passes through every YAML key/value verbatim (the frontend renders them as a kv table). body[] order matches source order. connected[] is the deduplicated list of resolved wikilink target ids from the entire body, sorted alphabetically for diff stability.
If the page can't be found or fails to parse, emit {"error": "<reason>"} and exit. The backend treats any non-error JSON as success.
Steps
Step 1: Resolve block_id → file path
Map node_type to a folder; if no hint was given, try each in priority order until a match is found:
| node_type | Folder |
|---|
memory | moments/ |
person | entities/people/ |
place | entities/places/ |
object | entities/objects/ |
session | sessions/ |
synthesis | synthesis/ |
Search order with no hint: moments/, entities/people/, entities/places/, entities/objects/, sessions/, synthesis/. The first existing <folder>/<block_id>.md wins.
If nothing matches: emit {"error": "block not found", "block_id": "<id>"} and exit. Do not search _raw/failed/ — failed pages are not addressable evidence.
Step 2: Parse frontmatter
Read the file. Frontmatter is everything between the first --- line and the next --- line. Parse it as YAML.
Pass-through rules:
- Every top-level key/value goes into
frontmatter verbatim. Do not rename, do not drop.
- Arrays stay arrays. Booleans stay booleans. Numbers stay numbers. Null stays null.
- If YAML parsing fails:
{"error": "frontmatter parse failed", "block_id": "<id>"}.
- If
ingest_status is pending or failed: still emit, but include "status_warning": "<value>" at the top level. The frontend can decide whether to render.
Step 3: Parse body into typed blocks
The body is everything after the closing ---. Split into blocks separated by blank lines, then classify each block. One source block → one output block unless wikilink-grouping merges adjacent link lines (see below).
Classification rules, applied in this order:
h1 — block is a single line starting with # (one hash, one space). Strip the prefix.
h2 — single line starting with ## . Strip the prefix.
quote — block whose first non-whitespace character is > (Markdown blockquote): strip the leading > and any single space, join multi-line quotes with single spaces. Also classify as quote if the entire block is a single line wrapped in straight or curly double quotes ("..." or "...") — typical for transcript paragraphs per docs/ingest_contract.md §3 body convention.
links — block consisting only of lines that start with [[. Each line may carry a trailing — DESCRIPTION (em-dash + label, e.g. [[abc123_000089]] — TEMPORAL_NEXT). Strip the description; keep the bare [[target]] (preserve any |alias) as the item. Adjacent link-only lines collapse into a single links block whose items is the list. Order = source order.
p — anything else. Join multi-line paragraphs with single spaces.
Emit each classified block as { "kind": "...", "text": "..." } (or { "kind": "links", "items": [...] }).
Empty body: emit body: []. Do not fabricate placeholder content.
Step 4: Compute connected[]
Scan the entire body (every block, not just links) with the alias-aware regex \[\[([^\]|]+)(?:\|[^\]]+)?\]\]. For each match:
- Take capture group 1, trim, strip any path prefix and trailing
.md.
- Resolve to a node id. The cheapest path: if
_meta/graph.json exists, load it once and build a title→id map from its nodes[] (each node has id and label — label is the page title). Resolve case-insensitively against this map; fall back to slug match (the target string IS a node id).
- If
_meta/graph.json does NOT exist, resolve by scanning the vault directly: read title: from the frontmatter of every *.md under moments/, entities/{people,places,objects}/, sessions/, synthesis/. This is slower but always correct. Recommend the user run wiki-graph-export first.
- Drop unresolved targets silently (orphan links —
wiki-lint is the right place to complain).
Deduplicate, sort alphabetically, emit as connected: [...].
Step 5: Derive typeLabel, mediaTone, title
-
title — value of frontmatter.title. Fall back to block_id if missing.
-
typeLabel — <frontend_type> · <subtype>, where:
frontend_type is from the AGENTS.md mapping (memory / person / place / object / session / synthesis)
subtype is:
memory → moment (always)
person → frontmatter.relationship if present (e.g. "close friend"), else "entity"
place → first non-system tag in frontmatter.tags matching place semantics (e.g. outdoor, indoor), else "entity". System tags (entity/place, visibility/*) don't count.
object → artifact (always)
session → source (always)
synthesis → pattern (always)
-
mediaTone — a hex color the frontend uses for thumbnail gradients. Deterministic function of type and (for moments) emotion:
| Type / Emotion | Hex |
|---|
| memory · positive | #7b9a78 |
| memory · calm | #7b9a78 |
| memory · curious | #8aa888 |
| memory · neutral / null | #7b8a90 |
| memory · mixed | #a89a6a |
| memory · negative | #a86a6a |
| person | #a86a72 |
| place | #5e8aa8 |
| object | #b89a4a |
| session | #7a8190 |
| synthesis | #6ea077 |
These match the styling in knowledge-frontend/data.jsx and should not drift. If you find yourself wanting to invent a new tone, instead extend this table — the goal is consistency across runs.
Step 6: Emit JSON
Print the assembled object to stdout. Use compact JSON (no indentation) — the backend can pretty-print if it wants. Do not write to disk; do not log.
"$REPO_ROOT/.venv/bin/python" -c "import json,sys; print(json.dumps(payload, ensure_ascii=False))"
(Or assemble the JSON in claw's working memory and printf it — both are fine, the venv path is just available if needed.)
Things NOT to do
- Do not pretty-print or wrap in code fences. The caller is parsing stdout as a JSON object — any preamble breaks it.
- Do not log to
log.md. This skill is called per-request (potentially many times per second). Logs would balloon.
- Do not write evidence to disk. Evidence is a derived view; staleness is a real risk if cached.
- Do not include
CATEGORY_COLORS or any other styling object. Only mediaTone (a single hex) leaks color into the response — and that's because it's per-page, derived from emotion.
- Do not resolve wikilinks by guessing. If
_meta/graph.json is missing, scan the vault; if a target doesn't resolve, drop it. Don't fabricate a target id.
- Do not search
_raw/failed/. Failed pages are not user-visible evidence.
Edge cases
- Body has no recognizable structure (e.g. just one long paragraph): emit it as a single
{"kind": "p", "text": "..."}. Don't try to be clever.
- Wikilink at the end of a non-link block (e.g.
Walked with [[Maya]] in the park.): the regex still picks it up for connected[], but the block stays a p. Don't split mid-paragraph.
- Frontmatter missing (file has no
--- ... ---): emit frontmatter: {} and parse the whole file as body. Don't error.
- Block id collides across categories (e.g. an entity slug
may09 and a session id may09): the priority order in Step 1 wins. With node_type hint, the hint always wins.
title collision in connected resolution (e.g. two pages both titled "Maya"): resolve to the first match in priority order entity → moment → synthesis → session, matching wiki-graph-export's rule. Document this in the _meta/graph.json build so behavior is consistent.
When to invoke
This skill is called programmatically by the backend's /api/evidence/{block_id} route, NOT by the user in conversation. The route spawns claw with a one-shot prompt:
claw prompt "wiki-evidence <block_id> <node_type>" \
--allowedTools wiki-evidence \
--json-out
It can also be invoked manually for debugging:
claw prompt "wiki-evidence abc123_000142 memory"
It is NOT invoked by the user typing natural language — there's no fuzzy match path here. The block_id comes from _meta/graph.json (which the frontend already loaded) or from wiki-ask's task output.