bash

May 12, 2026 ยท View on GitHub

๐Ÿค–
lazyspec

A little TUI & CLI for project documentation.

CI Rust 2021 Status: Experimental Version Last commit Nix Flake

screenshot of a terminal interface displaying codebase documentation, categorised by type

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 validate exits non-zero on errors, so it slots into CI.
  • Embed @ref directives in your specs to point at source code. Lazyspec expands them inline using git 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 --json output 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:

SkillPurpose
plan-workDetect existing artifacts and determine the right entry point
write-rfcPropose a design with intent, interface sketches, and identify stories
create-storyCreate stories with acceptance criteria linked to an RFC
resolve-contextGather full document chain (RFC -> Story -> Iteration) before work
create-iterationPlan an iteration with task breakdown and test plan
buildImplement tasks from an iteration with subagent dispatch
review-iterationTwo-stage review -- AC compliance first, then code quality
create-auditCriteria-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.

CommandDescription
initInitialise 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 XUpdate 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)
statusShow 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 listShow all reservation refs on the remote
reservations prune [--dry-run]Remove refs for documents that already exist locally

show Flags

FlagDescription
-e, --expand-referencesExpand @ref directives into fenced code blocks
--max-ref-lines NMax 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

Documents 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:

StrategyBehaviour
incrementalNext sequential integer from existing files (default)
sqidsShort hash-like IDs derived from a timestamp, configured via [numbering.sqids]
reservedReserves 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

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