refactor(docs): restructure SKILL.md into progressive-disclosure layers
Split the monolithic skill prompt into three tiers: - Core API (search/context/sql) stays in the first prompt - Structured helpers listed as one-liners with filter signatures - Detailed patterns and pitfalls extracted to references/ Add references/query-patterns.md (copyable CodeAct recipes) and references/pitfalls.md (scope, FTS, ordering, compactness traps). Clarify project scope semantics (slug vs path vs cwd) throughout. Add ORDER BY timestamp DESC to failures() for newest-first default.
This commit is contained in:
@@ -12,186 +12,188 @@ allowed-tools:
|
||||
|
||||
# obelisk
|
||||
|
||||
Searches and queries your Claude Code session history stored in `~/.claude/`.
|
||||
A SQLite index with FTS5 full-text search covers all sessions, subagent conversations, and workflow agent runs.
|
||||
You write JS query snippets that run in a sandboxed VM against the indexed data, then parse the JSON output.
|
||||
Search and query Claude Code session history stored in `~/.claude/`.
|
||||
Obelisk indexes sessions, messages, tool calls, tool results, summaries,
|
||||
subagents, workflows, workflow agents, parent chains, and raw JSONL lines into
|
||||
SQLite + FTS5.
|
||||
|
||||
Obelisk is a CodeAct memory layer: write a small JS query, run it locally, read
|
||||
the JSON, then answer. Do not turn history into a flat document or browse entire
|
||||
sessions by default.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The base directory for this skill is provided as `$SKILL_DIR` at invocation time (shown as "Base directory for this skill: ...").
|
||||
The skill directory is provided as `$SKILL_DIR` at invocation time.
|
||||
|
||||
**Fast keyword search** (no script needed):
|
||||
Fast keyword search:
|
||||
|
||||
```bash
|
||||
node $SKILL_DIR/scripts/runtime.mjs --search "keyword"
|
||||
```
|
||||
|
||||
**Custom query** (write a JS snippet, run it):
|
||||
Custom query:
|
||||
|
||||
1. Write a query to a temp file (e.g. `/tmp/q.mjs`)
|
||||
2. Run: `node $SKILL_DIR/scripts/runtime.mjs --query /tmp/q.mjs`
|
||||
3. Parse the JSON stdout and answer the user
|
||||
1. Write a bounded JS query to a temp file, for example `/tmp/q.mjs`.
|
||||
2. Run:
|
||||
|
||||
The query file body is executed inside `(async () => { ... })()` with the API below available as globals. The last expression is returned as JSON. Use `return` to emit results.
|
||||
```bash
|
||||
node $SKILL_DIR/scripts/runtime.mjs --query /tmp/q.mjs
|
||||
```
|
||||
|
||||
## API
|
||||
3. Parse JSON stdout and answer with concise evidence.
|
||||
|
||||
### search(text, opts?)
|
||||
The query file runs inside `(async () => { ... })()`. Use `return` to emit JSON.
|
||||
|
||||
Full-text search across all messages (user, assistant, subagent, workflow agent).
|
||||
## Reference Triggers
|
||||
|
||||
Returns: `[{ message: {uuid, text, role, timestamp, model, cwd}, session: {id, title, project, started_at}, rank, context: [...surrounding messages] }]`
|
||||
Use progressive disclosure, but do not guess.
|
||||
|
||||
opts: `{ limit, sessionId, project, after, before, cwd }`
|
||||
- Read `references/schema.md` before raw `sql()` unless the needed table/column relationship is already explicit here.
|
||||
- Read `references/query-patterns.md` for workflow trees, failed tool counts or failure groups, broad development-history synthesis, 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.
|
||||
|
||||
`rank` is the FTS5 relevance score (negative; closer to 0 = more relevant). Use it to judge result quality and stop early when results become irrelevant.
|
||||
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.
|
||||
|
||||
### sessions(opts?)
|
||||
## Core API
|
||||
|
||||
Query sessions with filters. Returns session rows ordered by `ended_at` descending.
|
||||
### `search(text, opts?)`
|
||||
|
||||
opts: `{ project, after, before, limit, branch, sessionId, sessions }`
|
||||
Full-text search across main messages, subagent messages, and workflow-agent
|
||||
messages.
|
||||
|
||||
Returns:
|
||||
|
||||
```js
|
||||
sessions({ project: '%quiet-zero%' })
|
||||
sessions({ after: '2026-06-01', branch: 'main', limit: 5 })
|
||||
[{ message: { uuid, text, role, timestamp, model, cwd },
|
||||
session: { id, title, project, started_at },
|
||||
rank,
|
||||
context }]
|
||||
```
|
||||
|
||||
### context(uuid)
|
||||
`context` here means temporal neighbors: nearby messages in the same session by
|
||||
timestamp. It is not the parent chain. Use `context(uuid)` or `trace(uuid)` for
|
||||
causal/parent-chain context.
|
||||
|
||||
Full story around a message: the message itself, parent chain, session info, subagent/workflow metadata.
|
||||
Opts: `{ limit, sessionId, project, after, before, cwd }`.
|
||||
|
||||
Returns: `{ message, parentChain, session, subagent, workflow }`
|
||||
`project` is a SQL `LIKE` filter over `sessions.project`, not an exact project
|
||||
identity. Results are already ordered by FTS5 rank; lower rank sorts earlier.
|
||||
Prefer returned order over manually interpreting numeric rank unless you are
|
||||
deliberately using FTS5 semantics.
|
||||
|
||||
### recent(n?)
|
||||
### `context(uuid)`
|
||||
|
||||
Shorthand for `sessions({ limit: n })`. Latest n sessions (default 10).
|
||||
|
||||
### sql(query, ...params)
|
||||
|
||||
Raw SQL. Use `?` placeholders. Returns array of row objects.
|
||||
|
||||
**Before writing your first SQL query, read `references/schema.md` for the full table schema, column names, and relationships.** Don't guess column names — the schema is your source of truth.
|
||||
|
||||
**Schema-safe SQL pattern:** when aggregating event tables, join to the table that actually owns the metadata instead of inventing columns. For example, `tool_calls` does **not** own timestamps; join `messages m ON m.uuid = tc.message_uuid` for `m.timestamp`, and join `sessions s ON s.id = tc.session_id` for project/session filters. Prefer SQL-side `GROUP BY`/`COUNT`/`MAX` with `LIMIT`, and return compact evidence rows with stable IDs rather than raw event records.
|
||||
|
||||
Tables: `sessions`, `messages`, `tool_calls`, `tool_results`, `subagents`, `workflows`, `workflow_agents`, `messages_fts`
|
||||
|
||||
### Other APIs
|
||||
|
||||
All list-returning functions accept a common filter opts object: `{ project, after, before, limit, sessionId, sessions }`. For backward compatibility, passing a string is treated as `sessionId`, a number as `limit`.
|
||||
|
||||
- `trace(uuid)` -- full parent chain from root to message
|
||||
- `thread(sessionId)` -- all messages in a session, ordered by time
|
||||
- `subagents(opts?)` -- subagent metadata + message counts. opts: `{ sessionId, project, limit }`
|
||||
- `workflows(opts?)` -- workflow runs with duration, tokens, status. opts: `{ sessionId, project, after, before, limit }`
|
||||
- `workflowTree(runId)` -- workflow metadata + parsed result + agents with phase/label/tokens/duration (no messages; use `sql()` with `agent_id` to drill into a specific agent)
|
||||
- `fileHistory(filePath, opts?)` -- every Edit/Write/Read on a file. opts: `{ after, before, limit }`
|
||||
- `failures(opts?)` -- tool calls that returned errors, with surrounding context. opts: `{ sessionId, project, after, before, limit }`
|
||||
- `summaries(opts?)` -- session summaries (away recaps, compaction summaries). opts: `{ sessionId, project, after, before, limit, sessions }`. Returns: `[{ id, session_id, timestamp, source, content, session_title, project }]`. Use `source` for values like `away_summary`; use `content` for the summary text.
|
||||
- `raw(uuid, opts?)` -- windowed access to the original JSONL line (bypasses index truncation)
|
||||
|
||||
### Retrieval strategy
|
||||
|
||||
**For helper field/schema confirmation questions:** use the relevant helper under the user's explicit scope with a small `limit`, then return only `Object.keys(row)` or a projection of the fields being verified plus short snippets. Do not invent alias fields when helper docs/rows use different names (for summaries, use `source`, `content`, `session_id`, `project`; not `text` or `summary_type`). Include stable evidence IDs such as `id`, `session_id`, or `uuid` in the final answer.
|
||||
|
||||
**Respect explicit scopes and empty results.** If the user asks for a specific project/session/file/time range, keep every query inside that scope. When a scoped helper call such as `summaries({ project, limit })` returns `[]`, report no results; do not broaden to all projects or all summaries unless the user asks for fallback.
|
||||
|
||||
**Never pull an entire session.** Navigate incrementally:
|
||||
|
||||
1. `sessions({ project: '...' })` or `recent()` — find relevant sessions
|
||||
2. `summaries({ project: '...' })` — read session summaries to judge relevance (cheapest)
|
||||
3. `search()` — find specific messages matching a query
|
||||
4. When you find a relevant message and want more context, expand from that point:
|
||||
- **Horizontally**: use `sql()` to fetch neighboring messages by timestamp
|
||||
```js
|
||||
sql('SELECT uuid,role,text FROM messages WHERE session_id=? AND timestamp>? ORDER BY timestamp LIMIT 5', sid, msg.timestamp)
|
||||
```
|
||||
- **Vertically**: use `trace(uuid)` to walk up the parent chain, or `context(uuid)` to see subagent/workflow relationships
|
||||
4. `raw(uuid, opts?)` — recover truncated content from a specific message
|
||||
5. `thread(sessionId)` — full session dump, **last resort only**
|
||||
|
||||
**File history queries:** `fileHistory()` can include many `Read` rows and large tool inputs. For questions about how a file changed, filter to `Edit`/`Write` before returning, cap the filtered list to the requested evidence count, and return compact evidence records only.
|
||||
|
||||
### raw(uuid, opts?)
|
||||
|
||||
Some indexed fields (tool call inputs, tool results) are truncated to 10k chars. `raw()` reads the original JSONL line to recover the full content.
|
||||
|
||||
Returns: `{ text, totalLength, offset, limit, hasMore }`
|
||||
|
||||
opts: `{ offset: 0, limit: 10000 }` — character window into the raw JSONL line.
|
||||
Returns the full story around one indexed message:
|
||||
|
||||
```js
|
||||
// First window
|
||||
const r = raw(messageUuid)
|
||||
// r.text = first 10k chars of the original JSONL line
|
||||
// r.totalLength = full line length
|
||||
// r.hasMore = true if more content remains
|
||||
|
||||
// Scroll forward
|
||||
const r2 = raw(messageUuid, { offset: 10000, limit: 10000 })
|
||||
{ message, parentChain, session, subagent, workflow }
|
||||
```
|
||||
|
||||
## Examples
|
||||
Use this after `search()` finds a promising message. It is the usual way to
|
||||
expand vertically from one evidence point without dumping the whole session.
|
||||
|
||||
### "上次怎么修 auth 的"
|
||||
### `sql(query, ...params)`
|
||||
|
||||
Raw SQL with `?` placeholders. Returns array rows.
|
||||
|
||||
Before writing non-trivial SQL, read `references/schema.md`. Common safe joins:
|
||||
|
||||
- `tool_calls` does not have timestamps. Join `messages m ON m.uuid = tc.message_uuid`.
|
||||
- `tool_results` does not have timestamps. Join `messages m ON m.uuid = tr.message_uuid`.
|
||||
- For project/session filters, join `sessions s ON s.id = <table>.session_id`.
|
||||
- Prefer SQL-side `GROUP BY`, `COUNT`, `MAX`, `ORDER BY`, and `LIMIT` over hand-counting in the final answer.
|
||||
|
||||
Tables: `sessions`, `messages`, `tool_calls`, `tool_results`, `summaries`,
|
||||
`subagents`, `workflows`, `workflow_agents`, `messages_fts`.
|
||||
|
||||
## Structured Helpers
|
||||
|
||||
These helpers are convenience accessors over the same SQLite structure. They do
|
||||
not replace `sql()`; use `sql()` when you need an exact aggregation or a join
|
||||
the helper does not expose.
|
||||
|
||||
All list helpers accept a bounded `limit`. Many also accept:
|
||||
`{ project, after, before, sessionId, sessions, branch }`. Check the schema or a
|
||||
tiny sample before relying on less common filters.
|
||||
|
||||
- `sessions(opts?)` -- session rows, newest first. `project` is a SQL `LIKE` pattern.
|
||||
- `recent(n?)` -- shorthand for recent sessions.
|
||||
- `summaries(opts?)` -- summary rows, newest first: `{ id, session_id, timestamp, source, content, session_title, project }`.
|
||||
- `subagents(opts?)` -- subagent metadata plus `messageCount`.
|
||||
- `workflows(opts?)` -- workflow runs, newest first.
|
||||
- `workflowTree(runId)` -- workflow row plus parsed `result` and `agents`; may include bulky `script` and `result_json`, so project compact fields.
|
||||
- `fileHistory(filePath, opts?)` -- Read/Edit/Write tool calls for a file, oldest first; includes many `Read` rows.
|
||||
- `failures(opts?)` -- failed tool results with tool/session context, newest first.
|
||||
- `trace(uuid)` -- parent chain from root to message.
|
||||
- `thread(sessionId)` -- full session messages; last resort only.
|
||||
- `raw(uuid, opts?)` -- windowed access to the original JSONL line.
|
||||
|
||||
## Retrieval Contract
|
||||
|
||||
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.
|
||||
- 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. 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:
|
||||
|
||||
- `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.
|
||||
|
||||
## Minimal Patterns
|
||||
|
||||
Search, then expand one promising hit:
|
||||
|
||||
```js
|
||||
const hits = search('auth fix')
|
||||
return hits.slice(0, 5).map(h => ({
|
||||
session: h.session.title,
|
||||
date: h.session.started_at,
|
||||
message: h.message.text?.slice(0, 200)
|
||||
}))
|
||||
const hits = search('auth fix', { limit: 5 });
|
||||
if (!hits.length) return [];
|
||||
return hits.slice(0, 3).map(h => ({
|
||||
session_id: h.session.id,
|
||||
session_title: h.session.title,
|
||||
uuid: h.message.uuid,
|
||||
snippet: h.message.text?.slice(0, 240),
|
||||
}));
|
||||
```
|
||||
|
||||
### "最近在做什么"
|
||||
Check helper fields before assuming names:
|
||||
|
||||
```js
|
||||
return sessions({ limit: 10 }).map(s => ({ title: s.title, project: s.project, date: s.started_at }))
|
||||
const rows = summaries({ project: '%quiet-zero%', limit: 1 });
|
||||
return rows.length ? Object.keys(rows[0]) : [];
|
||||
```
|
||||
|
||||
### "哪些文件被反复修改"
|
||||
Fetch message neighbors without a full thread:
|
||||
|
||||
```js
|
||||
return sql(`
|
||||
SELECT file_path, COUNT(*) as n FROM tool_calls
|
||||
WHERE name IN ('Edit','Write') AND file_path IS NOT NULL
|
||||
GROUP BY file_path HAVING n > 3 ORDER BY n DESC LIMIT 20
|
||||
`)
|
||||
const hit = search('runtime query', { limit: 1 })[0];
|
||||
return sql(
|
||||
`SELECT uuid, role, timestamp, substr(text,1,240) AS snippet
|
||||
FROM messages
|
||||
WHERE session_id=? AND timestamp>=?
|
||||
ORDER BY timestamp LIMIT 6`,
|
||||
hit.session.id,
|
||||
hit.message.timestamp
|
||||
);
|
||||
```
|
||||
|
||||
### "这个项目的 workflow 跑过几次"
|
||||
|
||||
```js
|
||||
return workflows({ project: '%quiet-zero%' }).map(w => ({
|
||||
run: w.run_id, agents: w.agent_count, time: w.timestamp
|
||||
}))
|
||||
```
|
||||
|
||||
### "上次跑 experiment 用了多少 token"
|
||||
|
||||
```js
|
||||
const hits = search('experiment')
|
||||
if (!hits.length) return 'No experiment sessions found'
|
||||
const sid = hits[0].session.id
|
||||
return sql('SELECT SUM(input_tokens) as input, SUM(output_tokens) as output FROM messages WHERE session_id = ?', sid)
|
||||
```
|
||||
|
||||
### "追踪一下那个决策是怎么做的"
|
||||
|
||||
```js
|
||||
const hits = search('the decision query here')
|
||||
if (!hits.length) return 'Nothing found'
|
||||
return context(hits[0].message.uuid)
|
||||
```
|
||||
See `references/query-patterns.md` for longer recipes.
|
||||
|
||||
## Notes
|
||||
|
||||
- First run builds the index (~5s for ~100 sessions). Subsequent runs are incremental.
|
||||
- DB location: `~/.claude/obelisk.sqlite`
|
||||
- Subagent and workflow agent conversations are fully indexed and searchable.
|
||||
- Query scripts run in a sandboxed VM context -- no file system or network access from inside scripts.
|
||||
- Text is truncated to 10k chars per message during indexing.
|
||||
- FTS5 search supports standard SQLite FTS syntax: `"exact phrase"`, `term1 AND term2`, `term1 OR term2`, `term1 NOT term2`.
|
||||
- FTS5 tokenizes on hyphens. To search for `SkillOpt-outputs`, use `"skillopt outputs"` (replace hyphen with space, wrap in quotes for phrase match). For exact match on hyphenated strings, use `sql()` with LIKE instead.
|
||||
- First run builds the index. Later runs update incrementally.
|
||||
- DB location: `~/.claude/obelisk.sqlite`.
|
||||
- Query scripts run in a sandboxed VM with no filesystem or network access from inside the script.
|
||||
- Indexed text and stored tool inputs/results are truncated to 10k chars. Use `raw(uuid, { offset, limit })` for specific JSONL windows.
|
||||
|
||||
Reference in New Issue
Block a user