diff --git a/SKILL.md b/SKILL.md index 7eeb125..e1708b8 100644 --- a/SKILL.md +++ b/SKILL.md @@ -50,8 +50,9 @@ Before writing a query, classify the task. Progressive disclosure is useful, but skipping the relevant reference usually costs extra query rounds. - Read `references/schema.md` before raw `sql()` unless the needed table/column relationship is already explicit here. Do this before running the SQL, not after a missing-column error. -- Read `references/query-patterns.md` before broad "how did X evolve / what did we do / what problems happened / what was the conclusion" synthesis, and for one-shot synthesis retrieval, workflow trees, failed tool counts or failure groups, file history synthesis, summary neighbors, subagent recall, raw windows, and empty-result handling. -- Read `references/pitfalls.md` when a scoped result is empty or tiny, when a query may over-fetch, when a term is hyphenated, when project scope is ambiguous, or when helper row fields are unclear. +- Read `references/retrieval-semantics.md` before multi-step retrieval, scoped project/file/session searches, or synthesis/conclusion/history questions. It defines the query design frame. +- Read `references/query-patterns.md` when you need copyable query scripts: one-shot synthesis, learned detail passes, workflow trees, failure groups, file history, summaries, subagents, raw windows, or empty results. +- Read `references/pitfalls.md` after an error or when helper fields, FTS syntax, aliases, or row shapes are unclear. If a helper row shape is unclear, first run a tiny scoped query and return `Object.keys(row)` or a compact sample. Do not invent field names. @@ -134,26 +135,14 @@ tiny sample before relying on less common filters. Keep queries scoped, bounded, and structural. -- Preserve explicit project/session/file/time scopes. Empty or tiny scoped results are real results; do not broaden unless the user asks. -- Treat project scope as three distinct semantics: exact `sessions.project` slug, exact `sessions.project_path`, or fuzzy `LIKE` search. Use `sql()` for exact slug/path membership; helper `project` means fuzzy `LIKE`. -- Start with cheap locators: `sessions()`, `summaries()`, `search()`, or a small SQL query. -- Expand incrementally with `context()`, `trace()`, neighbor SQL, or `raw()` windows. -- For conclusion, broad history, failure investigation, or file evolution questions, prefer one bounded query script that locates, expands, dedupes, groups, and returns compact evidence rows. If a second detail pass is needed, derive its filters or facets from the first pass; do not dump whole session windows by default. -- Return compact evidence with stable IDs (`session_id`, `uuid`, `tool_call_id`, `run_id`, `agent_id`) and short snippets. -- Avoid `thread()` unless the user explicitly asks for a full transcript or all smaller probes are insufficient. -- Keep runtime JSON small, ideally under 10k-12k chars for synthesis tasks. Do not return all sessions, all summaries, all tool calls, complete workflow trees, full raw messages, or whole tool results. -- When counting or aggregating, compute counts in SQL or in the query script and return those counts. Do not hand-count from long rows in prose. -- For recent failures or "which tasks failed" questions, aggregate by session/task and return counts plus sparse examples. Do not return raw failure rows. -- For broad "how did X evolve / what did we do / what problems happened" history synthesis, use a bounded facet sweep from `references/query-patterns.md`. For concept recall, session lookup, or exact term recall, keep compact `search()` first. +- Scope First: classify the locator as scope, artifact, or semantic. Use the narrowest structural locator before FTS; empty scoped results are valid unless the user asks to broaden. +- Plan Before Probe: for conclusion, broad history, failure investigation, or file evolution, write a bounded retrieval script instead of spending turns on intermediate results. +- Structure Before Text: compute counts, joins, grouping, dedupe, and projection in SQL or JS; keep runtime JSON compact, ideally under 10k-12k chars for synthesis tasks. +- Evidence Before Conclusion: return compact evidence with stable IDs (`session_id`, `uuid`, `tool_call_id`, `run_id`, `agent_id`) and short snippets, then synthesize in the final answer. -High-frequency field contracts: - -- `summaries()` uses `source` and `content`, not `summary_type` or `text`. -- `search().context` is temporal neighbor context, not causal or parent-chain context. -- `fileHistory()` includes `Read`; filter to `Edit`/`Write` for causal change history. -- `workflowTree()` may expose raw `script` and `result_json`; omit them unless the user asks for raw workflow details. -- `fileHistory()` is ordered oldest first. For recent file changes, use SQL with `ORDER BY m.timestamp DESC`. -- FTS5 tokenizes hyphens and treats `search(text)` as raw `MATCH` syntax. For `workflow-script`, search the quoted tokenized phrase such as `"workflow script"` or use SQL `LIKE` for exact hyphen matching. +If field, context, ordering, FTS, or helper semantics affect the query, read +`references/retrieval-semantics.md` before coding. If a query errors, read +`references/pitfalls.md` before retrying. ## Minimal Patterns diff --git a/references/pitfalls.md b/references/pitfalls.md index eb897ef..88c3ba8 100644 --- a/references/pitfalls.md +++ b/references/pitfalls.md @@ -1,65 +1,40 @@ # Obelisk Pitfalls -Use this when a query may over-fetch, when a scoped query returns few or zero -rows, or when helper fields are unclear. +Use this after a query error, suspicious empty result, over-large output, or +unclear helper row shape. For query design, read `retrieval-semantics.md` first. -## Scope Is A Contract +## Missing Columns And Wrong Aliases -If the user gives a project, session, file, or time range, keep every query -inside that scope. Do not broaden because a scoped result is small. +Common wrong guesses: -There are three different project-like scopes: +- Summaries: use `source` and `content`; do not use `summary_type` or `text`. +- Tool call name: use `tool_calls.name`. `tool_name` is only an alias in `SELECT tc.name AS tool_name`; `tc.tool_name` is not a column. +- Tool call timestamps: `tool_calls` has no timestamp. Join `messages m ON m.uuid = tc.message_uuid`. +- Tool result timestamps: `tool_results` has no timestamp. Join `messages m ON m.uuid = tr.message_uuid`. +- Workflow agent message counts: `workflowTree()` returns `messageCount` for agents. -- `sessions.project`: stored Claude Code project slug. -- `sessions.project_path`: reconstructed absolute project path. -- `messages.cwd`: working directory for a specific message. - -`project` filters in helpers are SQL `LIKE` patterns over `sessions.project`. -`%quiet-zero%` can match benchmark or generated workspaces that merely contain -that string. Prefer exact `sql()` filters when the user asks for exact project -membership: +When uncertain, inspect a tiny sample instead of guessing: ```js -sql(` - SELECT id, title, project, project_path, ended_at - FROM sessions - WHERE project_path = ? - ORDER BY ended_at DESC - LIMIT 20 -`, '/Users/tomiya/Code/quiet-zero') +const rows = summaries({ limit: 1 }); +return rows.length ? Object.keys(rows[0]) : []; ``` -Use fuzzy project search only when the task is discovery or the user explicitly -asked to search broadly. If you broaden, make the broadening visible in the -returned evidence. +## FTS5 Syntax Errors -Self-noise examples to filter when the user asks for real historical sessions: - -- `SkillOpt-outputs` -- `obelisk_train` -- `obelisk-eval` - -## FTS5 Hyphens And Syntax - -`search(text)` passes text to FTS5 `MATCH`. Hyphenated terms can be parsed as -operators or separate tokens, and special characters can raise FTS syntax -errors. - -For `workflow-script`, use a quoted tokenized phrase: +`search(text)` uses raw FTS5 `MATCH`. Hyphenated terms and punctuation can be +parsed as syntax. ```js +// tokenized phrase for FTS search('"workflow script"', { limit: 10 }) ``` -Use exact phrases for phrase semantics, separate terms for token semantics, and -SQL `LIKE` for literal punctuation. Do not silently fallback from a scoped FTS -query to all sessions. - -For exact hyphen matching, use SQL `LIKE` on `messages.text` under a scope: +For literal punctuation, use SQL `LIKE` under the same scope: ```js sql(` - SELECT m.uuid, s.id AS session_id, s.title, substr(m.text,1,240) AS snippet + SELECT m.uuid, s.id AS session_id, s.title, substr(m.text,1,180) AS snippet FROM messages m JOIN sessions s ON s.id = m.session_id WHERE s.project LIKE ? @@ -69,116 +44,36 @@ sql(` `, '%quiet-zero%', '%workflow-script%') ``` -`rank` is already applied by `ORDER BY rank`; lower rank sorts earlier in this -runtime. Prefer returned order over comparing "closer to zero" manually. +## Over-Large Runtime JSON -## Context Is Not Always Causal +If runtime stdout is large, fix the query instead of reading it in chunks. -`search().context` returns temporal neighbors: nearby messages by timestamp in -the same session. It is useful for quick orientation, but it is not the parent -chain and may cross side branches, subagents, or workflow boundaries. +- Lower `LIMIT`. +- Shorten snippets to 160-240 chars. +- Group in SQL/JS and return counts plus sparse examples. +- For `fileHistory()`, filter to `Edit`/`Write` before projecting evidence. +- For `workflowTree()`, omit `script`, `result_json`, and full agent messages unless explicitly requested. +- Use `raw(uuid, { offset, limit })` only after identifying one specific message UUID. -Use: +## Empty Results -- `context(uuid)` for message, parent chain, session, subagent, and workflow. -- `trace(uuid)` for just the parent chain. -- SQL timestamp neighbors for horizontal expansion inside one session. +An empty array can be the correct answer for exact scopes or sentinels. -## Ordering Defaults Matter +When the user asks for a scoped project/file/session or exact term: -Some helpers return newest first; others do not. +1. run the scoped query; +2. return `[]` or compact counts; +3. say no matching prior result was found; +4. do not call `recent()`, all-project `summaries()`, or `thread()` as fallback unless the user asks. -- `sessions()` returns newest sessions first. -- `summaries()` returns newest summaries first. -- `workflows()` returns newest workflows first. -- `failures()` returns newest failures first, but should still be treated as an evidence helper rather than a precise count helper. -- `fileHistory()` orders by message timestamp ascending. If the user asks for recent changes, use SQL explicitly: +## Counting From Snippets + +If the user asks "how many", "counts", "top N", or "group by", compute it in +SQL or in the query script. Do not infer counts from visible snippets. ```js sql(` - SELECT tc.id, tc.name, tc.file_path, m.timestamp, s.id AS session_id, s.title - FROM tool_calls tc - JOIN messages m ON m.uuid = tc.message_uuid - JOIN sessions s ON s.id = tc.session_id - WHERE tc.file_path = ? - AND tc.name IN ('Edit', 'Write', 'NotebookEdit') - ORDER BY m.timestamp DESC - LIMIT 20 -`, '/absolute/path/to/file') -``` - -## Conversation Turns Are Expensive - -SQLite queries are cheap; repeated conversation turns are not. Every turn can -write intermediate query output into conversation context and make later turns -read it again. - -For conclusion, broad history, failure investigation, or file evolution tasks, -prefer one bounded query script that does the mechanical retrieval work inside -the script: - -1. locate candidates with `search()`, `summaries()`, or SQL; -2. expand only selected hits with `context()`, `trace()`, or neighbor SQL; -3. dedupe and group by `session_id`, facet, file, or tool; -4. return compact evidence rows plus counts/limits. - -Do not show every intermediate result to the conversation. Return the final -compact evidence view, then use the model for the conclusion. - -## Session Windows Are Not Evidence Plans - -After finding a relevant session, avoid defaulting to `LIMIT 25` or `LIMIT 40` -message windows. That is transcript browsing in miniature: it often brings back -thinking, transitions, and repeated context instead of the evidence needed for -the question. - -Preferred detail pass: - -1. extract candidate terms, files, tools, or decisions from the first pass; -2. query by learned facets inside the candidate sessions; -3. return 2-4 rows per facet, 8-12 rows total, with 160-220 char snippets. - -If the vocabulary is still unclear, use a small session window as fallback: -5-8 rows per session, filtered by timestamp, role, or discovered terms when -possible, and explain the fallback in `query_plan`. - -## Compact Vs Raw - -Default to compact evidence. Raw/full access is a conscious escalation. - -- `workflowTree()` may include `script`, `result_json`, parsed `result`, and all agents. Project only the fields needed for the answer. -- `thread(sessionId)` dumps a whole session; use it only as a last resort. -- `raw(uuid)` can recover long original JSONL lines; use small windows and cite `totalLength`/`hasMore`. -- Tool results and tool inputs can be large. Return short snippets. - -## Field Names To Avoid Guessing - -Common wrong guesses: - -- Summaries: use `source` and `content`; do not use `summary_type` or `text`. -- Tool call name: use `tool_calls.name`. `tool_name` is only a safe alias in `SELECT tc.name AS tool_name`; `tc.tool_name` is not a table column. -- Tool result timestamps: `tool_results` has no timestamp. Join `messages`. -- Tool call timestamps: `tool_calls` has no timestamp. Join `messages`. -- Workflow agent message counts: `workflowTree()` returns `messageCount` for agents. - -When uncertain: - -```js -const rows = summaries({ limit: 1 }); -return rows.length ? Object.keys(rows[0]) : []; -``` - -## Counting Must Be Structural - -If the user asks "how many", "counts", "top N", or "group by", compute it in SQL -or in the query script and return the computed data. Do not infer counts from -visible snippets in prose. - -Good: - -```js -sql(` - SELECT tc.name, COUNT(*) AS n + SELECT tc.name AS tool_name, COUNT(*) AS n FROM tool_results tr JOIN tool_calls tc ON tc.id = tr.tool_use_id WHERE tr.is_error = 1 @@ -187,21 +82,3 @@ sql(` LIMIT 10 `) ``` - -Bad: - -```js -const rows = failures({ limit: 20 }); -return rows; // then count by eye in the final answer -``` - -## Empty Results - -An empty array is often the correct answer. - -When the user asks for a scoped project/file/session or an exact sentinel: - -1. Run the scoped query. -2. Return `[]` or compact counts. -3. Say no matching prior result was found. -4. Do not call `recent()`, all-project `summaries()`, or `thread()` as fallback unless the user asks. diff --git a/references/retrieval-semantics.md b/references/retrieval-semantics.md new file mode 100644 index 0000000..702ece1 --- /dev/null +++ b/references/retrieval-semantics.md @@ -0,0 +1,99 @@ +# Obelisk Retrieval Semantics + +Read this before designing a non-trivial query. This is the query design frame; +`pitfalls.md` is only the debug checklist. + +## Four Principles + +### Scope First + +Classify the user's request before choosing tools. + +| User signal | Locator mode | Start with | Avoid first | +|-------------|--------------|------------|-------------| +| project name/path, session, cwd, file, time range | scope | `sessions()`, exact SQL on `project_path`, `sessionId`, `fileHistory()` | broad FTS | +| workflow, subagent, tool call, summary, edit | artifact | `workflows()`, `subagents()`, `summaries()`, `tool_calls`, `tool_results` | all-session search | +| concept, conclusion, design history, vague memory | semantic | `search()`, summaries, bounded facet sweep | session dumps | + +One-shot retrieval is not all-shot retrieval. A query script may perform +multiple steps, but the first locator should be the narrowest semantic fit. If a +scope locator finds the relevant project/session/file, do not also run broad FTS +unless scoped evidence is insufficient and `query_plan` says why. + +Project-like fields are distinct: + +- `sessions.project`: stored Claude Code project slug. +- `sessions.project_path`: reconstructed absolute project path. +- `messages.cwd`: working directory at message time. +- helper `project`: SQL `LIKE` over `sessions.project`, not exact membership. + +For exact project membership, use `sql()` with `s.project = ?` or +`s.project_path = ?`. Empty or tiny scoped results are valid results; do not +broaden unless the user asks or your `query_plan` explicitly marks a fallback. + +### Plan Before Probe + +For conclusion, broad history, failure investigation, or file evolution tasks, +prefer a retrieval script over interactive probing. + +Good shape: + +1. locate candidates with scope/artifact/semantic locators; +2. expand only selected hits; +3. dedupe and group in the script; +4. return compact evidence rows plus counts and limits. + +If a second detail pass is needed, derive filters or facets from the first pass: +candidate sessions, discovered vocabulary, files, tools, timestamps, or +decisions. Prefer a learned faceted detail pass over `LIMIT 25` session windows. +If vocabulary is still unclear, use a small filtered window and say so in +`query_plan`. + +### Structure Before Text + +Use the database shape before asking the model to read text. + +- Count and aggregate in SQL or JS (`GROUP BY`, `COUNT`, `MAX`, `ORDER BY`, `LIMIT`). +- Join metadata from the owner table instead of inventing fields. +- Project compact rows; do not return whole sessions, complete workflow trees, full raw messages, or entire tool results. +- Keep synthesis runtime JSON around 10k-12k chars when possible. +- For recent failures, aggregate by session/task and return sparse examples. +- For file evolution, filter `fileHistory()` to `Edit`/`Write`, group by session or phase, and return short deltas. + +Ordering and context are semantic: + +- `sessions()`, `summaries()`, `workflows()`, and `failures()` are newest first. +- `fileHistory()` is oldest first. +- `search().context` is temporal neighbors in one session, not causal context. +- `context(uuid)` and `trace(uuid)` are for parent-chain/causal expansion. + +### Evidence Before Conclusion + +Obelisk stores original structure, not precompiled claims. It has sessions, +messages, summaries, tool calls/results, files, subagents, workflows, parent +chains, and raw JSONL windows. It does not store "claim", "stance", +"contradiction", or "conclusion" entities. + +For semantic questions, build a task-local evidence view: + +```js +{ + query_plan: { mode, scope, facets, limits }, + evidence: [ + { type, id, session_id, timestamp, facet, snippet } + ], + omitted: 0 +} +``` + +Then synthesize the conclusion in the final answer. Do not pretend the evidence +view is a stored Obelisk entity. + +## Text Search Semantics + +`search(text)` passes text to SQLite FTS5 `MATCH`. + +- Hyphens tokenize: for `workflow-script`, use `"workflow script"` or SQL `LIKE` for literal punctuation. +- Special characters may produce FTS syntax errors; simplify or quote the FTS query under the same scope. +- Exact phrase, token search, and literal punctuation are different semantics. +- Results are ordered by `ORDER BY rank`; lower rank sorts earlier. Prefer returned order over "closer to zero" comparisons.