refactor(skill): extract retrieval-semantics.md, compress pitfalls.md Move query design principles (scope/plan/structure/evidence) and field

semantics (context types, ordering, project scopes) out of pitfalls.md
  into a new retrieval-semantics.md. Pitfalls.md becomes a compact debug
  checklist: missing columns, FTS errors, over-large output, empty results.
  SKILL.md query routing updated to point at the three reference tiers.
This commit is contained in:
tommy0103
2026-06-07 20:31:31 +08:00
parent 0128881d04
commit dff88bbb37
3 changed files with 147 additions and 182 deletions
+10 -21
View File
@@ -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. 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/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/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/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/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 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. `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. 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. - 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.
- 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`. - Plan Before Probe: for conclusion, broad history, failure investigation, or file evolution, write a bounded retrieval script instead of spending turns on intermediate results.
- Start with cheap locators: `sessions()`, `summaries()`, `search()`, or a small SQL query. - 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.
- Expand incrementally with `context()`, `trace()`, neighbor SQL, or `raw()` windows. - 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.
- 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.
High-frequency field contracts: If field, context, ordering, FTS, or helper semantics affect the query, read
`references/retrieval-semantics.md` before coding. If a query errors, read
- `summaries()` uses `source` and `content`, not `summary_type` or `text`. `references/pitfalls.md` before retrying.
- `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.
## Minimal Patterns ## Minimal Patterns
+38 -161
View File
@@ -1,65 +1,40 @@
# Obelisk Pitfalls # Obelisk Pitfalls
Use this when a query may over-fetch, when a scoped query returns few or zero Use this after a query error, suspicious empty result, over-large output, or
rows, or when helper fields are unclear. 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 Common wrong guesses:
inside that scope. Do not broaden because a scoped result is small.
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. When uncertain, inspect a tiny sample instead of guessing:
- `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:
```js ```js
sql(` const rows = summaries({ limit: 1 });
SELECT id, title, project, project_path, ended_at return rows.length ? Object.keys(rows[0]) : [];
FROM sessions
WHERE project_path = ?
ORDER BY ended_at DESC
LIMIT 20
`, '/Users/tomiya/Code/quiet-zero')
``` ```
Use fuzzy project search only when the task is discovery or the user explicitly ## FTS5 Syntax Errors
asked to search broadly. If you broaden, make the broadening visible in the
returned evidence.
Self-noise examples to filter when the user asks for real historical sessions: `search(text)` uses raw FTS5 `MATCH`. Hyphenated terms and punctuation can be
parsed as syntax.
- `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:
```js ```js
// tokenized phrase for FTS
search('"workflow script"', { limit: 10 }) search('"workflow script"', { limit: 10 })
``` ```
Use exact phrases for phrase semantics, separate terms for token semantics, and For literal punctuation, use SQL `LIKE` under the same scope:
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:
```js ```js
sql(` 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 FROM messages m
JOIN sessions s ON s.id = m.session_id JOIN sessions s ON s.id = m.session_id
WHERE s.project LIKE ? WHERE s.project LIKE ?
@@ -69,116 +44,36 @@ sql(`
`, '%quiet-zero%', '%workflow-script%') `, '%quiet-zero%', '%workflow-script%')
``` ```
`rank` is already applied by `ORDER BY rank`; lower rank sorts earlier in this ## Over-Large Runtime JSON
runtime. Prefer returned order over comparing "closer to zero" manually.
## 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 - Lower `LIMIT`.
the same session. It is useful for quick orientation, but it is not the parent - Shorten snippets to 160-240 chars.
chain and may cross side branches, subagents, or workflow boundaries. - 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. An empty array can be the correct answer for exact scopes or sentinels.
- `trace(uuid)` for just the parent chain.
- SQL timestamp neighbors for horizontal expansion inside one session.
## 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. ## Counting From Snippets
- `summaries()` returns newest summaries first.
- `workflows()` returns newest workflows first. If the user asks "how many", "counts", "top N", or "group by", compute it in
- `failures()` returns newest failures first, but should still be treated as an evidence helper rather than a precise count helper. SQL or in the query script. Do not infer counts from visible snippets.
- `fileHistory()` orders by message timestamp ascending. If the user asks for recent changes, use SQL explicitly:
```js ```js
sql(` sql(`
SELECT tc.id, tc.name, tc.file_path, m.timestamp, s.id AS session_id, s.title SELECT tc.name AS tool_name, COUNT(*) AS n
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
FROM tool_results tr FROM tool_results tr
JOIN tool_calls tc ON tc.id = tr.tool_use_id JOIN tool_calls tc ON tc.id = tr.tool_use_id
WHERE tr.is_error = 1 WHERE tr.is_error = 1
@@ -187,21 +82,3 @@ sql(`
LIMIT 10 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.
+99
View File
@@ -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.