SpecStory internals — filename algorithm, reverse lookup, markdown structure¶
Reference for what SpecStory writes to .specstory/history/*.md and how this repo's tv agent-sessions channel links live coding-agent sessions back to those files. For install / config / providers, see SpecStory CLI.
This page documents the output format, not the runtime. Read it when you want to:
- Map a live session id (from
claude --resume/codex resume/cursor-agent --resume) back to its.specstory/history/*.md. - Grep SpecStory transcripts programmatically (agent, model, timestamps are all plain-text markers).
- Re-render a SpecStory transcript elsewhere (the structure is intentionally simple).
The SpecStory CLI is Go, BSD 3-Clause, at https://github.com/specstoryai/getspecstory. DeepWiki has a generated technical reference at https://deepwiki.com/specstoryai/getspecstory that's also useful for orientation.
Filename algorithm¶
Format: YYYY-MM-DD_HH-MM-SS[Z|±HHMM]-<slug>.md
Verified empirically against the files in this repo's .specstory/history/:
| Example | Notes |
|---|---|
2026-04-24_09-09-09Z-commit-commit-backup-e.md |
current default (UTC, with seconds) |
2026-03-30_11-20-14+0800-utility-function-bash-hash.md |
--local-time-zone mode (non-UTC offset) |
2026-02-10_10-41Z-git-commit-history-modification.md |
older format (UTC, no seconds) — prior CLI version, not retroactively renamed |
2026-04-24_06-54-14Z.md |
empty slug — no qualifying first user message |
Timestamp source: RFC3339 parse of the session's CreatedAt — the timestamp of the first JSONL / SQLite record SpecStory sees, which is typically the first user message. UTC is the default; --local-time-zone switches to a numeric offset.
Slug: first 4 words of the first real (non-synthetic) user message, after:
- Strip XML context tags injected by IDEs (
<ide_opened_file>,<environment_context>, etc.). - Unicode NFD normalization + accent stripping; lowercase.
@→at,&→and,#→hash.- Strip remaining punctuation; split on whitespace.
- Take first 4 words, join with
-.
Empty slug (no qualifying user message yet) produces a filename with just the timestamp.
Session id is NOT in the filename. It's embedded as an HTML comment on line 5 of the body — see the next section.
Source pointers (as of current dev branch):
| Responsibility | Package / function |
|---|---|
| Filename timestamp format | pkg/session/session.go → FormatFilenameTimestamp |
| Final path assembly | pkg/session/session.go → BuildSessionFilePath |
| Slug from first user message | pkg/spi/path_utils.go → GenerateFilenameFromUserMessage |
| Claude-specific slug pull | pkg/providers/claudecode/provider.go → FileSlugFromRootRecord |
| Markdown body generation | pkg/outputgen/ → GenerateMarkdown / writeMarkdownToFile |
Collision handling:
- Byte-identical content on disk → write is skipped ("up to date").
- Timestamp + slug collide with different content → second write overwrites. Undocumented; rare in practice (requires two sessions starting in the same second with the same first-4-words).
Historical files are not migrated. If the format evolves (e.g. seconds were added at some point), existing .specstory/history/*.md stay as-is.
Reverse lookup — session id → markdown file¶
Every file starts with a fixed preamble (line 5 is the key line):
<!-- Generated by SpecStory, Markdown v2.1.0 -->
# YYYY-MM-DD HH:MM:SSZ
<!-- <Provider-display-name> Session <uuid> (YYYY-MM-DD HH:MM:SSZ) -->
Provider display names observed in this repo's .specstory/history/:
| In HTML comment | Provider ID used for specstory run <id> |
|---|---|
Claude Code |
claudecode |
cursoride (Cursor IDE composer chat) |
— (written by Cursor IDE extension, not CLI) |
Codex |
codexcli |
OpenCode |
(not yet — see backlog/specstory-opencode-support.md) |
Gemini |
geminicli |
| (not seen here) Droid | droidcli |
| (not seen here) Cursor CLI | cursorcli |
The UUID in the <!-- ... Session <uuid> ... --> line is the same session id the agent uses for its resume flag (claude --resume <uuid>, codex resume <uuid>, cursor-agent --resume <uuid>, opencode --session <uuid>). That makes session id → transcript file a one-liner grep, uniform across providers:
SpecStory CLI has no built-in resolve <sid> → path subcommand. The documented commands are specstory run / specstory sync / specstory watch. The grep recipe above is the supported path.
For the tv agent-sessions channel's integration, see dot_config/television/executable_agent-sessions.py — it caches the map on startup from ~/.local/share/chezmoi/.specstory/history/ and exposes the matched path as TSV column 5 (bound to Alt+S to copy).
Markdown body structure¶
Rough skeleton written by SpecStory (pkg/outputgen/):
<!-- Generated by SpecStory, Markdown v2.1.0 -->
# 2026-04-24 09:09:09Z
<!-- Claude Code Session <uuid> (2026-04-24 09:09:09Z) -->
_**User (2026-04-24 09:09:09Z)**_
<first user prompt>
---
_**Agent (claude-opus-4-7 2026-04-24 09:09:13Z)**_
<think><details><summary>Thought Process</summary>
<thinking-block text>
</details></think>
<assistant reply prose>
<tool-use data-tool-type="shell" data-tool-name="Bash"><details>
<summary>Tool use: **Bash**</summary>
<tool invocation JSON + output>
</details></tool-use>
---
_**User**_ …
Key elements:
- Role headers are italic-bold with optional model + timestamp:
_**User (ts)**_,_**Agent (<model> <ts>)**_. ---separates turns.- Thinking blocks are collapsed
<details>wrapped in a<think>tag. - Tool use is collapsed
<details>inside a<tool-use>tag withdata-tool-typeanddata-tool-nameattributes. - No fenced code blocks around tool I/O — the
<details>payload is raw text / JSON.
glow and bat both render this cleanly. <think> and <tool-use> are non-standard HTML elements; in glow's HTML passthrough they surface as collapsible sections.
Caveats¶
- Cloud-sync failures never touch local files. SpecStory writes markdown locally first; a failing cloud sync only stops the upload.
--only-cloud-syncskips local writes entirely. If you use that flag,.specstory/history/won't grow — the reverse-lookup table will be empty.--no-cloud-syncis the opposite — local writes proceed, cloud sync is disabled.--debug-rawdumps raw session records to.specstory/debug/alongside the markdown — useful if a transcript looks wrong and you want to see exactly what SpecStory saw.- Provider IDs vs display names are not the same strings.
specstory run claudecodeis the CLI ID; the HTML comment in the resulting markdown readsClaude Code(with space). If you script against either string, pick the right one — the HTML comment is what the reverse-lookup grep matches.
References¶
- https://deepwiki.com/specstoryai/getspecstory — DeepWiki technical reference (good for architecture + commands; thin on filename specifics)
- SpecStory CLI releases — for version-pinning notes
- Install / config page — user-facing setup
backlog/tv-agent-sessions-richer-preview.md— thetv agent-sessionsimprovements built on the reverse-lookup recipe above