bash
May 12, 2026 ยท View on GitHub
๐ค
lazyspec
A little TUI & CLI for project documentation.
Warning
Lazyspec is experimental. APIs and CLI interfaces will change frequently and without notice.
Features
Lazyspec manages project documentation as version-controlled markdown files with YAML frontmatter. Documents live in your repo, so agents and humans read from the same source of truth.
- Create, update, link, and validate documents. Typed relationships (
implements,supersedes,blocks,related-to) keep the chain explicit. - Catch broken links, orphaned documents, and incomplete frontmatter before they rot.
lazyspec validateexits non-zero on errors, so it slots into CI. - Embed
@refdirectives in your specs to point at source code. Lazyspec expands them inline usinggit show, with symbol-level extraction for Rust and TypeScript. - Fuzzy search, markdown preview, live file watching, and document creation without leaving the terminal.
- Every command supports
--jsonoutput for automation and agent integration. - Define your own types, templates, and directory layout in
.lazyspec.toml.
Install
Nix
nix profile install github:jkaloger/lazyspec
Or run without installing:
nix run github:jkaloger/lazyspec
Cargo
cargo install --git https://github.com/jkaloger/lazyspec
From Source
git clone https://github.com/jkaloger/lazyspec
cd lazyspec
cargo install --path .
Shell Completions
Generate and source a completion script for your shell:
# zsh
source <(lazyspec completions zsh)
# bash
source <(lazyspec completions bash)
# fish
lazyspec completions fish | source
Add the appropriate line to your shell profile (~/.zshrc, ~/.bashrc, etc.) to load completions on startup. Completions include subcommands, flags, document IDs, and relationship types.
Skills
Lazyspec includes a set of agent skills that enforce its workflow:
| Skill | Purpose |
|---|---|
plan-work | Detect existing artifacts and determine the right entry point |
write-rfc | Propose a design with intent, interface sketches, and identify stories |
create-story | Create stories with acceptance criteria linked to an RFC |
resolve-context | Gather full document chain (RFC -> Story -> Iteration) before work |
create-iteration | Plan an iteration with task breakdown and test plan |
build | Implement tasks from an iteration with subagent dispatch |
review-iteration | Two-stage review -- AC compliance first, then code quality |
create-audit | Criteria-based review (health check, security, accessibility, etc.) |
Usage
Quick Start
Initialise a new project, then launch the TUI:
lazyspec init
lazyspec
Tip
Check the examples/ directory for a complete project setup including config, templates, and agent skill definitions you can use as a starting point.
This repo dogfoods lazyspec, so you can also check out the docs/ directory or run lazyspec from this repo.
TUI
Running lazyspec with no subcommand opens the interactive dashboard. It provides fuzzy search, markdown preview, document creation, and live file watching -- documents update automatically when changed on disk.
CLI
All document management is available as subcommands. Most accept --json for machine-readable output.
| Command | Description |
|---|---|
init | Initialise lazyspec in the current project |
create <type> <title> [--author X] | Create a document (rfc, adr, story, iteration) |
list [type] [--status X] | List documents with optional filters |
show <id> [-e] | Display a document by path or shorthand ID (e.g. RFC-001) |
update <path> --status X --title X | Update document frontmatter |
delete <path> | Delete a document |
link <from> <rel> <to> | Add a typed relationship (implements, supersedes, blocks, related-to) |
unlink <from> <rel> <to> | Remove a relationship between documents |
search <query> [--doc-type X] | Full-text search across all documents |
context <id> | Show the full document chain (RFC -> Story -> Iteration) |
status | Show full project status with all documents and validation |
ignore <path> | Mark a document to skip validation |
unignore <path> | Remove validation skip from a document |
validate [--warnings] | Check document integrity and link consistency |
fix [paths] [--dry-run] | Fix documents with broken or incomplete frontmatter |
pin <id> | Pin blob hashes onto @ref directives in a document |
provenance add <id> <citation> | Append a citation to a document's provenance list |
provenance remove <id> <citation> | Remove an exact-match citation from a document's provenance list |
provenance list [id] | List citations for a document, or for all documents grouped by id |
reservations list | Show all reservation refs on the remote |
reservations prune [--dry-run] | Remove refs for documents that already exist locally |
show Flags
| Flag | Description |
|---|---|
-e, --expand-references | Expand @ref directives into fenced code blocks |
--max-ref-lines N | Max lines per expanded ref (default: 25) |
provenance Subcommands
Cite the sources of truth that informed a document. Citations are free-form strings stored as a YAML list in frontmatter.
lazyspec provenance add RFC-001 "Workshop 2026-04-12"
lazyspec provenance add RFC-001 "Privacy Act 1988"
lazyspec provenance list RFC-001
# Workshop 2026-04-12
# Privacy Act 1988
lazyspec provenance remove RFC-001 "Privacy Act 1988"
lazyspec provenance list
# RFC-001 Workshop 2026-04-12
All three subcommands accept --json. Shapes:
add/remove:{ "doc": "...", "added"|"removed": "...", "provenance": [...] }list <id>:{ "doc": "...", "provenance": [...] }list(no id):{ "documents": [{ "id": "...", "path": "...", "provenance": [...] }, ...] }
add rejects empty citations. remove is exact-match and errors when the citation is absent.
Coordination
Claude Code Hooks
Lazyspec ships hook snippets that claim, heartbeat, and release a lease on $ASSIGNED_TASK across a Claude Code session. The orchestrator (daemon, manual export, etc.) sets the env var; hooks no-op silently when it is unset, so the snippet is safe to install unconditionally.
Drop into .claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "[ -n \"$ASSIGNED_TASK\" ] && lazyspec claim \"$ASSIGNED_TASK\" --agent-id \"$CLAUDE_SESSION_ID\" --json || true"
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "[ -n \"$ASSIGNED_TASK\" ] && lazyspec heartbeat \"$ASSIGNED_TASK\" --agent-id \"$CLAUDE_SESSION_ID\" --min-interval 15m --json || true"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "[ -n \"$ASSIGNED_TASK\" ] && lazyspec release \"$ASSIGNED_TASK\" --agent-id \"$CLAUDE_SESSION_ID\" --json || true"
}
]
}
]
}
}
The standalone file lives at hooks/claude-code-settings.json.
$ASSIGNED_TASK contract. Orchestrator sets it to a doc id (e.g. ITERATION-170). If unset, the [ -n "$ASSIGNED_TASK" ] guard short-circuits and no lazyspec invocation happens.
Throttle. --min-interval 15m matches the default lease_duration / 4 (lease defaults to 60m). If you tune lease_duration in .lazyspec.toml, tune this to roughly a quarter of it.
Error tolerance. || true swallows non-zero exits from lazyspec (e.g. lease already released, network blip), so a session never fails to end because of a coordination error.
See RFC-035 for the design rationale.
@ref Syntax
@ref SyntaxDocuments can embed references to source code using @ref directives. By default, lazyspec show renders them as-is. Pass -e to expand them inline.
@ref <path> # entire file
@ref <path>#<symbol> # specific type or struct
@ref <path>#<symbol>@<sha> # symbol at a specific git commit
@ref <path>#123 # line 123
@ref <path>#123@<sha> # line 123 at a specific git commit
Expansion resolves content via git show (committed state, not working tree). Supported languages for symbol extraction are TypeScript (.ts/.tsx) and Rust (.rs).
Each expanded ref includes a caption line showing the file path, short git SHA, and symbol or line info. Expanded blocks are truncated to 25 lines by default; when truncated, a trailing comment shows how many lines were omitted. Use --max-ref-lines to adjust the limit.
Example
A document containing:
@ref src/engine/store.rs#Store
Renders as:
```rust
pub struct Store { ... }
```
Unresolvable refs render as:
> [unresolved: src/engine/store.rs#Store]
Configuration
lazyspec init creates a .lazyspec.toml in your project root with four built-in document types:
[directories]
rfcs = "docs/rfcs"
adrs = "docs/adrs"
stories = "docs/stories"
iterations = "docs/iterations"
[templates]
dir = ".lazyspec/templates"
[naming]
pattern = "{type}-{n:03}-{title}.md"
Custom Types
Instead of [directories], you can define types explicitly with [[types]]. This lets you rename the defaults, add new types, or set custom prefixes and icons used in the TUI.
[[types]]
name = "rfc"
plural = "rfcs"
dir = "docs/rfcs"
prefix = "RFC"
icon = "โ"
[[types]]
name = "spec"
plural = "specs"
dir = "docs/specs"
prefix = "SPEC"
icon = "โ"
Validation Rules
Validation rules define structural constraints between document types. Two shapes are supported:
parent-child-- the child type must link to a parent type via a given relationship.relation-existence-- documents of a given type must have at least one relationship.
[[rules]]
shape = "parent-child"
name = "stories-need-rfcs"
child = "story"
parent = "rfc"
link = "implements"
severity = "warning"
[[rules]]
shape = "relation-existence"
name = "adrs-need-relations"
type = "adr"
require = "any-relation"
severity = "error"
Numbering
Document numbers are assigned automatically during create. Three strategies are available per type:
| Strategy | Behaviour |
|---|---|
incremental | Next sequential integer from existing files (default) |
sqids | Short hash-like IDs derived from a timestamp, configured via [numbering.sqids] |
reserved | Reserves numbers on a git remote before creating files, preventing distributed collisions |
Reserved numbering uses git custom refs (refs/reservations/*) to coordinate across branches. It wraps either incremental or sqids formatting with an atomic push-based lock, so two people never get the same number.
[[types]]
name = "rfc"
prefix = "RFC"
numbering = "reserved"
[numbering.reserved]
remote = "origin" # default
format = "incremental" # or "sqids"
max_retries = 5 # push retry attempts before failing
If the remote is unreachable, create fails rather than silently falling back. Use lazyspec reservations prune to clean up refs for documents that have been created.
Templates
Place markdown templates in the templates directory (.lazyspec/templates/ by default). When creating a document, lazyspec uses the template matching the document type name (e.g. rfc.md, story.md).
Development
Nix (recommended)
The repo includes a Nix flake that provides the full toolchain. With direnv installed:
direnv allow
Or enter the dev shell manually:
nix develop
This gives you cargo, clippy, rustfmt, and rust-analyzer at pinned versions.
To run all checks (clippy, tests, formatting):
nix flake check
Without Nix
cargo build
cargo test