Skip to content

feat(tools): Trajectory — query the session transcript as structured data - #1867

Open
0bsolescence wants to merge 3 commits into
danielmiessler:mainfrom
0bsolescence:feat/trajectory-query
Open

feat(tools): Trajectory — query the session transcript as structured data#1867
0bsolescence wants to merge 3 commits into
danielmiessler:mainfrom
0bsolescence:feat/trajectory-query

Conversation

@0bsolescence

Copy link
Copy Markdown

What

LIFEOS/TOOLS/Trajectory.ts: a read-only CLI that treats Claude Code session transcripts (JSONL) as queryable data — filter by session, time window, tool, or text; emit structured summaries instead of grepping raw JSONL. Useful for post-session review, hook debugging, and building on-disk observability without a server.

Standalone: one new tool file plus its test, no changes to existing files.

Testing

bun test test/Trajectory.test.ts — 63 pass, 0 fail, 153 assertions, run against current main.

Developed with AI assistance (Claude); reviewed and tested by me.

Daniel added 3 commits August 15, 2026 11:43
Claude Code already writes the full trajectory of every session to disk as
JSONL under ~/.claude/projects, but nothing in LifeOS reads it as a record.
Answering "when did we last touch this file", "which tools keep failing" or
"where did that decision get made" meant hand-rolled jq against a schema
nobody had written down. The evidence was there; the query surface was not.

Trajectory.ts adds four read-only commands over that record:

- sessions: uuid, first/last timestamp, cwd(s), git branch, message and tool
  counts, plus Claude Code's own ai-title line, which turns out to be the
  single most useful column for finding a session again.
- grep: a regex over user text, assistant text, thinking blocks, tool inputs
  and tool results, filterable by --role, --session and --since. Tool hits are
  attributed back to the tool name by resolving tool_use_id, so a match inside
  a result names the call that produced it.
- tools: call frequency with a failure count. Failures live on the user-side
  tool_result as is_error, which only identifies the call by id, so the id map
  is built first and errors are attributed through it.
- file: every read/write/edit of a path, with session and timestamp.

Design notes, both places where being wrong would have put fiction in an
evidence tool:

- Bash is deliberately excluded from 'file'. A shell command that touches a
  file is not reliably distinguishable from one that merely mentions a path,
  and guessing would manufacture history. 'grep' covers shell archaeology.
- --since reads a bare 2026-08-13 as LOCAL midnight. new Date() on a bare date
  yields UTC midnight, which on this UTC-7 host silently pulled in the previous
  evening's sessions — caught against real transcripts, where a session ending
  19:33 local appeared under --since for the following day. It also accepts
  48h / 7d, matching WorkSweep's existing --since.

Every unknown line type is skipped rather than fatal: the format carries at
least fifteen type values across versions, most of them bookkeeping, and a
malformed or truncated line is a skip, never a crash. Empty results exit 0;
only usage and read errors exit 2. --json emits [] rather than prose so it
stays machine-readable when nothing matched.

Reads nothing but *.jsonl one level under the transcript root, makes no network
calls, and writes nothing.

63 tests, all against fixtures synthesised in a temp dir from the observed line
shapes — no real transcript is read by the suite, so it carries no personal data
and runs on a machine that has never held a session.
Cross-vendor audit (codex review) flagged that statSync follows symlinks, so a
link beneath the configured root could point the walk at an arbitrary tree and
defeat the confinement this tool advertises. Switch to lstatSync for both the
project dir and the .jsonl entry; a symlink resolving outside <root>/<project>
is now rejected. Two regression tests cover the file and directory cases.
…dates

Two audit-flagged quality nits:
- excerpt() re-ran the regex on flattened text, which missed when the pattern
  matched whitespace, defaulting to index 0 and showing a window without the
  match. Window on the original text around the true match, flatten for display.
- parseSince() accepted out-of-range days (2026-02-31 → March 3) because the
  Date constructor normalizes rather than returning NaN. Round-trip the y/m/d
  components and reject on drift.
Both covered by regression tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant