| name | wiki-ask |
| description | Answer a MemoryLane question and return a structured TASK object the frontend renders as a tab in the right panel. Performs the same retrieval as `wiki-query`, but reshapes the result into the JSON contract defined in `memorylane/knowledge-frontend/data.jsx` — `activatedNodeIds`, `moments[]`, `entities[]`, `insight`, `blocks[]`, `sessionMeta`, `subassemblyTags`. Use this skill when the user invokes `wiki-ask <question>` or when a backend route (`POST /api/ask`) spawns claw with the question as the prompt. Distinct from `wiki-query` (returns prose with citations for human reading); this one returns machine-readable JSON for componentized rendering.
|
Wiki Ask — Structured TASK Response
You are answering a user's natural-language question against the MemoryLane vault and returning a single JSON object the frontend turns into a right-panel tab. The shape is fixed by memorylane/knowledge-frontend/data.jsx — match it exactly. Stdout MUST be parseable as one JSON object.
Before You Start
- Resolve config — Config Resolution Protocol in
llm-wiki/SKILL.md → OBSIDIAN_VAULT_PATH.
- Read
$OBSIDIAN_VAULT_PATH/AGENTS.md — confirm the "Frontend type mapping" table. If absent, abort with {"error": "vault not configured for ask export"}.
- Parse the prompt. The user/caller passes one of:
wiki-ask <question text> — the rest of the prompt is the question
wiki-ask with no question → emit {"error": "missing question"} and exit.
- Prefer
_meta/graph.json when it exists — it gives you a fast title→id map. If missing, you can still answer, but every wikilink resolution falls back to a vault scan.
Output Schema (fixed)
Print exactly one JSON object to stdout. No surrounding text. Schema (matches data.jsx's TASKS[] entry):
{
"id": "q_<8-hex-hash>",
"question": "What happened at the park?",
"summary": "A relaxed midday walk with Maya near the pond trail.",
"subassemblyTags": ["memory:outdoor", "person:maya", "object:camera", "place:pond-trail", "session:may09"],
"activatedNodeIds": ["abc123_000142", "maya", "camera", "riverside-park", "abc123"],
"sessionMeta": {
"file": "may09-walk.mp4",
"duration": "00:42:11",
"frames": ["https://res.cloudinary.com/.../000016.jpg", "..."]
},
"entities": [
{ "kind": "person", "label": "Maya", "tone": "#a86a72", "initial": "M" },
{ "kind": "place", "label": "Riverside Park", "tone": "#5e8aa8" },
{ "kind": "object", "label": "Camera", "tone": "#b89a4a" }
],
"moments": [
{ "id": "abc123_000142", "time": "00:00:04", "subtitle": "Arrived at the park",
"tone": "#7b9a78", "with": ["Maya"], "holding": ["Camera"],
"blockId": "abc123_000142", "emotion": "positive" }
],
"insight": {
"text": "Outdoor walks with Maya consistently produce your highest sustained-positive valence.",
"confidence": 0.86,
"blockId": "outdoor-calm-pattern"
},
"blocks": [
{ "id": "abc123_000142", "type": "memory", "title": "Arrived at the park",
"summary": "Walked through the park entrance at midday.",
"nodeIds": ["abc123_000142"] }
]
}
insight is null if no synthesis page is cited. sessionMeta is null if no session can be determined. All other arrays may be empty but must be present.
If retrieval fails or the vault is empty, emit a graceful empty TASK (with summary: "No matching memories yet." and empty arrays) — NOT an error. The frontend renders that as a polite "no matches" tab.
Steps
Step 1: Run the retrieval
Execute the same retrieval as wiki-query's tiered protocol — read wiki-query/SKILL.md Steps 1–4 if you haven't. The goal is a set of cited pages with their relevance, not a synthesized prose answer.
Stop when you have the candidate set. You don't need to write prose; the frontend doesn't render it. Cap the candidate set at 30 pages to keep response time bounded.
The cited set should be a list of (file_path, relevance_score) tuples. Relevance is whatever the retrieval produced — title match, tag match, summary match, body grep hit. Higher is better.
Step 2: Load every cited page's frontmatter
For each cited file, read the YAML frontmatter. Skip and log silently any file where:
ingest_status is pending or failed (moments only)
- frontmatter parse fails
- file path is under
_raw/
You now have a list of resolved pages with their full frontmatter, classified by category (moment, entity with subtype, session, synthesis).
Step 3: Compute activatedNodeIds and subassemblyTags
activatedNodeIds = the deduplicated list of file basenames (without .md) for every resolved page. Sort alphabetically for diff stability.
subassemblyTags = a small descriptive set of <type>:<slug> tokens drawn from the resolved pages — one per "concept dimension" the answer covers. Pick at most 5, in this priority order:
- dominant
frontend_type:<first-non-system-tag> for cited moments (e.g. memory:outdoor)
person:<slug> for each cited person (max 2)
object:<slug> for each cited object (max 1)
place:<slug> for each cited place (max 1)
session:<session_id> if a dominant session exists
These are display-only chips in the frontend — not used for filtering. Keep them human-readable.
Step 4: Build sessionMeta
Pick the dominant session: the session id that the most cited moments share. Tie-breaker: most recent created date. If no moment is cited (e.g. a synthesis-only answer), pick the session with the highest relevance score. If still nothing, set sessionMeta: null.
For the dominant session, load sessions/<session_id>.md frontmatter and emit:
{
"file": "<frontmatter.video_path basename, or session_id>",
"duration": "<frontmatter.duration>",
"frames": "<frontmatter.frame_swatches[] verbatim — array of URLs>"
}
If frame_swatches is missing or empty, fall back to sampling: gather the cited moments from this session sorted by timestamp_sec, take 6–8 evenly-spaced ones, use their thumbnail_url values. If still nothing, set frames: [] (frontend has a tone-only fallback).
Step 5: Build moments[]
For each cited moment page (category moment), emit:
{
"id": "<file basename>",
"time": "<HH:MM:SS from timestamp_sec>",
"subtitle": "<frontmatter.label or title>",
"tone": "<mediaTone from emotion — see wiki-evidence Step 5 table>",
"with": ["<person labels from connected entities of kind person>"],
"holding": ["<object labels from connected entities of kind object>"],
"blockId": "<file basename — same as id>",
"emotion": "<frontmatter.emotion or 'neutral'>"
}
Sort the array by timestamp_sec ascending. Cap at 12 entries — if more, keep the most relevant 12 by retrieval score.
with and holding are derived by scanning the moment's body for [[wikilinks]] and resolving each to a node. If the resolved node's category is entity with subtype person, add to with; subtype object → holding. Drop duplicates.
Step 6: Build entities[]
Collect every cited entity page. Dedupe by id. Cap at 6 entries (frontend's "who & what" row gets crowded past that). For each:
{ "kind": "person|place|object", "label": "<title>", "tone": "<mediaTone for type>", "initial": "<first letter of label, persons only>" }
initial is included only for person (single uppercase character used by the avatar circle). Drop the field for place/object.
Order: persons first, then places, then objects. Within each kind, by relevance descending.
Step 7: Build insight
Find the cited synthesis page with the highest frontmatter.confidence. If multiple synthesis pages tie or none are cited, pick the one with the highest retrieval score. If no synthesis is cited at all, emit "insight": null.
{
"text": "<frontmatter.title — single sentence pattern statement>",
"confidence": "<frontmatter.confidence — float>",
"blockId": "<file basename>"
}
If the synthesis page has no confidence, default to 0.5.
Step 8: Build blocks[]
One entry per resolved page (all categories), in this order: synthesis first, then memories (by timestamp), then sessions, then entities. Each block is the summary form — full detail comes from wiki-evidence per id when the user clicks.
{
"id": "<file basename>",
"type": "<frontend type per AGENTS.md mapping>",
"title": "<frontmatter.title>",
"summary": "<frontmatter.summary or first paragraph of body, ≤200 chars>",
"nodeIds": ["<self id, plus 1-3 closely connected node ids>"]
}
nodeIds includes the page itself plus a small set of its strongest connections (from body wikilinks, capped at 4). The frontend uses this to highlight clusters when a block is selected.
Cap blocks[] at 20. Drop the lowest-relevance entries if exceeded.
Step 9: Generate id, summary, finalize
id — q_<8-hex> where the 8-hex is the SHA-1 of the lowercased question, first 8 chars. Deterministic — same question yields same id (allows the frontend to dedupe tabs).
summary — single sentence (≤140 chars) describing the answer. Compose from the dominant moment's summary plus the dominant entity, e.g. "A relaxed midday walk with Maya near the pond trail." If the question was about a pattern, use the insight's title.
question — the original question verbatim.
Step 10: Emit JSON
Print one compact JSON object to stdout. Use the venv python for assembly if helpful:
"$REPO_ROOT/.venv/bin/python" -c "import json,sys; print(json.dumps(payload, ensure_ascii=False))"
No preamble, no fences, no log lines on stdout. (You may write debug to stderr if needed.)
Things NOT to do
- Do not synthesize prose into the response. The frontend renders components from structured fields; an
answer_text blob would be ignored. Use summary for the one-line and insight.text for the pattern claim — that's all the prose the schema allows.
- Do not include block details beyond
summary. Detail is wiki-evidence's job; duplicating here means two paths to keep in sync. The frontend pulls evidence on click.
- Do not write the response to disk. The TASK is a per-question artifact. If the user asks the same question again, recompute (it's cheap; cap is 30 pages).
- Do not log to
log.md. Per-question writes would balloon. Stderr is fine for debugging.
- Do not invent fields. If
data.jsx doesn't read it, don't emit it. Extra keys are not a feature.
- Do not return an error on empty results. Empty arrays + a polite
summary is the right answer to "no matches."
Edge cases
- Question matches nothing in the vault. Emit a TASK with empty arrays,
summary: "No matching memories yet.", deterministic id, sessionMeta: null, insight: null. Status code 200, not an error.
- Question is a pattern question ("when do I focus best?"). Retrieval will lean on synthesis pages.
moments[] may be small; insight carries the weight; sessionMeta may be null. That's fine.
- Multiple sessions tie for dominance. Pick the most recent by
created — the frontend's filmstrip needs one canonical video to render.
- Entity collision (two pages titled "Maya"): same priority as
wiki-graph-export (entity > moment > synthesis > session). Document which one was picked in subassemblyTags if it matters.
- Frontmatter has unexpected types (e.g.
confidence as a string "0.86" instead of float). Coerce defensively. Don't error.
When to invoke
Programmatic — invoked by the backend's POST /api/ask route:
claw prompt "wiki-ask <question>" --allowedTools wiki-ask,wiki-query --json-out
Manual / debug:
claw prompt "wiki-ask What happened at the park?"
Not user-facing in the conversational sense — the user types their question into the global chat in the frontend, the frontend POSTs to the backend, the backend calls this skill, the JSON comes back, the frontend renders the right-panel tab. End to end, ~1–3 seconds for a vault under a few hundred moments.