livestage
v1.1.1
Published
Live-document renderer and verifier for AI agents.
Maintainers
Readme
livestage
Live-document renderer and verifier for AI agents.
Version 1.1.1 | 1239+ tests | 30 directives | MIT
Your agent spends tokens every session re-deriving things the repo already
knows. Which branch, what changed, which files have no test, whether
.env.example still matches the code. Same commands, same parsing, every
time.
A .stage file answers that once, at read time, in one Read call.
Source you write:
# Project status
@query "git log -1 --format='%h %s'" label="last_commit" /
@count "src" match="*.ts" label="file_count" /
Last commit: {{ last_commit }}. {{ file_count }} TypeScript files.What the agent actually receives:
# Project status
Last commit: a1b2c3d fix parser edge case. 47 TypeScript files.No directive syntax survives. The agent reading it needs to know nothing about LiveStage, and the numbers were true at the moment it read them.
Drift? What's that?
The reason those numbers are usually wrong is not laziness. It is that a document written once is a snapshot, and snapshots stop being true the moment the code moves.
CLAUDE.md, the file every Claude Code session in this repo
reads first, was written once by Claude Code's own /init command and
never regenerated. It claimed a directive count "as of this writing", a
number nothing kept honest, and pointed at a donor spec file that had
already stopped existing here. Both sat there, wrong, until someone
happened to grep for them. Being written by an LLM the first time bought
it nothing. A snapshot is a snapshot regardless of who typed it.
Now CLAUDE.stage generates it, and npm run claude-md:check
fails CI if the committed file no longer matches. This README is the same:
5 modules, 30 directives, 25
worked examples, all computed at render time, none typed by hand.
Reach for this when
- An agent runs the same commands at the start of every session.
- A hand-maintained file describes something the code already knows: an env example, a scripts reference, a coverage map, an API index.
- A number in your docs has an "as of" date next to it.
- You want a document that degrades honestly rather than silently.
Skip it when
- The document is genuinely static prose. A design rationale does not change because a file did.
- The data changes within a single render, so the answer is stale before the reader finishes.
- One shell command already answers the question. Wrapping it buys nothing and adds a dependency.
- You need the agent to decide something. LiveStage computes and hands back markdown. It never judges, gates or chooses.
Install
npm install --save-dev livestageThen wire it into an AI coding assistant:
npx livestage initinit is idempotent and transactional: safe to run twice, and a partial
failure rolls back everything it wrote. It registers a PostToolUse hook
so reading a .stage file with the normal file tool already returns the
rendered result, installs a SessionStart hook that injects a live brief
instead of a static CLAUDE.md section, writes both into the detected
client's settings (~/.claude/settings.json, or ~/.cursor/settings.json,
or pass --client), and seeds a policy file. See Security.
PostToolUse rather than PreToolUse on purpose: PreToolUse can only allow, deny or rewrite a tool's arguments. Only PostToolUse can substitute what a Read call actually returns.
Or standalone, no hook:
npx livestage render some-doc.stageWhat it costs an agent to not have this
An agent getting repo state with shell emits a command, reads a result, and reasons about it, three times over. With a brief it emits one Read and reads one finished answer.
| scenario | shell | one brief | |-----------------------------|-------|-----------| | repo state (3 git commands) | 91 | 25 | | coverage gap (2 listings) | 54 | 20 | | env drift (2 commands) | 61 | 19 |
Method: cl100k_base, model-emitted tokens only, matched content on both sides. Model-emitted tokens are output-priced, roughly 5x input. There is no percentage claimed here on purpose: a real saving depends on a real agent run, and a modelled number published as a benchmark is the kind of claim that deserves to be torn apart.
The second-order point matters more than the first. A regenerated command
varies. grep -r "process.env" and grep -rn 'process\.env' and rg are
three different commands, and one of them returns a different answer. A
.stage command is fixed text, authored once, and it returns the same shape
every time.
Three briefs, live from their real source
Each of these replaces several separate commands with one render. They are
read live from the runnable files under examples/agent-briefs/, not
retyped, so what you see below cannot drift from what actually runs. Run any
of them with livestage render <file> from inside that directory, where the
shared policy grant applies.
Codebase health
Old way: git rev-parse --abbrev-ref HEAD, then git log -1, then
git status --short. Three round trips, three outputs to merge mentally.
# Codebase Health Brief
The old way: `git rev-parse --abbrev-ref HEAD`, `git log -1`, `git status
--short`, three separate commands run and mentally merged into one picture
of "is this repo in good shape right now."
The new way: one render.
## Policy grant this example needs
`examples/agent-briefs/.livestage/policy.json` in this directory (shared
with `change-review.stage`): `shell.enabled` plus the exact `git ...`
command strings below in `allow_patterns`, nothing else, and no wildcard
(a prefix pattern like `"git *"` allows anything after that prefix,
including `;`/`&&`/pipe chaining; only safe with commands that never
interpolate `{{ }}`/`${}` values, exact strings are the honest default).
See that file directly for the exact JSON.
## Result
@query "git rev-parse --abbrev-ref HEAD" label="branch" visible="false" /
@query "git log -1 --format='%h %s'" label="last_commit" visible="false" /
@query "git status --short" label="dirty" visible="false" /
- Branch: {{ branch }}
- Last commit: {{ last_commit }}
@if dirty == ""
- Uncommitted files: none detected
@if-end
@if dirty != ""
- Uncommitted files:
{{ dirty }}
@if-endChange review
Old way: git diff --stat, git log -5 --oneline, git status --short.
# Change Review Brief
The old way: `git diff --stat`, `git log -5 --oneline`, `git status
--short`, three commands and three separate scrollbacks to reconstruct
"what changed here, and what's still uncommitted."
The new way: one render.
## Policy grant this example needs
Shares `examples/agent-briefs/.livestage/policy.json` with
`codebase-health.stage`: `shell.enabled` plus the exact `git ...` command
strings this file uses below in `allow_patterns`, no wildcard.
## Result
@query "git diff --stat" label="diff_stat" visible="false" /
@query "git log -5 --oneline" label="recent_commits" visible="false" /
@query "git status --short" label="status" visible="false" /
### Diff stat
{{ diff_stat }}
### Recent commits
{{ recent_commits }}
### Working tree status
{{ status }}Onboarding brief, with no shell grant at all
Old way: cat README.md, cat package.json, ls src, grep scripts
package.json. This one needs no shell permission. @read and @tree are
filesystem directives, which means a whole class of "read this project" work
never needs shell access in the first place.
# Onboarding Brief
The old way: `cat README.md`, `cat package.json`, `ls src`, `grep scripts
package.json`, four separate commands before an agent (or a new
contributor) has any real picture of what a project even is.
The new way: one render. This example needs no shell grant at all, no
`.livestage/policy.json` beyond the shared one in this directory (which
this file doesn't even use): `@read` and `@tree` are filesystem-policy
directives, not shell.
## Result
Runs against a small, self-contained fixture project
(`sample-project/`) alongside this file, so the pattern is reusable in any
project without a path escaping this example's own directory.
@read "sample-project/package.json" path="name" label="proj_name" visible="false" /
@read "sample-project/package.json" path="description" label="proj_desc" visible="false" /
@tree "sample-project/src" label="src_tree" visible="false" /
## Onboarding Brief: {{ proj_name }}
{{ proj_desc }}
### Source tree
{{ src_tree }}Security
Every directive runs under a policy file at .livestage/policy.json.
init seeds the shipped strict profile if one does not already exist.
"Strict" names the enforcement model, not a feature switch. Every surface not explicitly granted is denied, hard destructive patterns are immutable, and nothing reaches outside the project root.
- shell ships with a curated read-only allowlist, around 40 patterns:
git,cat,grep,find, the common test runners. Deliberately, since@queryis dead on arrival without it. @codeand HTTP ship genuinely empty. Anything beyond the shipped allowlist, on any surface, needs an explicit grant.
The three briefs above share one grant in examples/agent-briefs/.livestage/policy.json,
which lists the five git commands they run and nothing else.
Keep a generated .md honest at read time
Add --stamp-metadata to a livestage build call and the generator writes
a small HTML comment at the top of the output:
<!-- livestage:generated
livestage_source: docs/api.stage
livestage_updated_at: 2026-08-17T15:46:01.385Z
livestage_version: 1.0.2
livestage_content_hash: 53e17201482b...
livestage_hash_inputs: src/**/*.ts,package.json,docs/api.stage
livestage_regenerate_on_read: true
livestage_degraded: false
-->An HTML comment rather than YAML frontmatter on purpose. GitHub renders a
leading --- block badly at the top of a repo's own README, or Jekyll
interprets it. A comment renders as nothing and is equally machine-readable.
--hash-inputs names every file the render actually depends on; without it
the hash covers only the .stage source. Presence of the block, not the
filename, opts a file into the contract. A .md with no block is passed
through untouched whether the hook is installed or not.
The PostToolUse hook watches reads of any stamped .md. It hashes the
declared inputs first, and unchanged means the committed file is served
as-is with no render at all. Only a changed hash triggers work, and
livestage_regenerate_on_read, a field only you set, decides what happens:
- absent (default): committed content is served, with a notice naming what is stale and the exact regen command. Drift gets surfaced, content does not change under the reader.
true: a fresh render is served, with a notice stating plainly that what follows is a live render rather than the file's committed bytes.false: committed content, no notice, no comparison. An explicit opt-out is honored as one.
If the render fails or times out, the committed file is served unchanged with a "could not verify, may be stale" notice. A read through this hook never fails and never serves a fresh render silently. A stale file being wrong is recoverable. A stale file that looks current to the one reader who could have caught it is not.
Fallbacks
Every directive declares a static fallback. A .stage file read without the
engine, or after a timeout, is still a usable document, and it says plainly
that it is degraded rather than presenting stale output as current.
Directive reference
All 30 directives, with syntax and examples: docs/directives.md.
That page is generated the same way this one is, pulled from whichever docs
declare primitives in their frontmatter. Document a new directive with a
primitives entry and it appears there on the next npm run directives.
More examples
25 worked examples ship in this repo, each with a rendered
.md beside its .stage source, kept honest by npm run examples:check in
CI. A few are marked live because they are deliberately non-deterministic:
real git state, wall-clock timing, an environment-dependent tree. Those ship
a rendered snapshot too, just not one asserted byte-identical on every run.
examples/hello.stage (rendered, live)
The smallest one: today's date, this directory's tree.
examples/drift/ eliminates four kinds of hand-maintained file that
silently diverge from the code that governs them:
env-drift (rendered), .env.example against actual process.env usage .
scripts-reference (rendered), package.json's real scripts .
test-coverage-map (rendered), which source files have no matching test .
todo-debt (rendered), a live TODO/FIXME/HACK inventory.
examples/agent-briefs/ the three above in full:
codebase-health (rendered, live),
change-review (rendered, live),
onboarding-brief (rendered).
examples/database/ and examples/http-health/ there is no @db or
@http directive. External reach is @code under policy.
customers (rendered) runs driver code and renders a table.
check (rendered) runs fetch and renders structured status.
examples/import-graph/ (rendered)
@graph reads YAML frontmatter and has no notion of real imports.
@import-graph does, walking a source tree into a real Mermaid dependency
graph, one node per file, no shell or @code grant needed.
examples/connections/ (rendered)
A project index: path tree, dependency graph, source overlap, nothing
hand-maintained.
examples/multi-step/ files as steps, frontmatter as state, assertions
as gates, no workflow engine. index (rendered)
is the overview; its own README runs the
pipeline. The step files are not pre-rendered here because running them
changes real state on disk, which is the point.
examples/showcase/ three documents rendering under the default policy
with no extra grants: index (rendered),
api-reference (rendered),
report (rendered, live).
How this README stays current
README.stage, the source of this file, reads package.json for the name,
version and description; scripts/test-baseline.json for the reviewed test
floor, rendered as "N+" because it only ever rises; the module, directive
and example counts by counting the directories and files themselves; the
round-trip table from benchmarks/roundtrip.json; and the three worked
examples from their real source, so the shown code cannot drift from what
runs.
npm run readme regenerates README.md. npm run readme:check renders
into a throwaway and fails if the committed file differs. That check runs in
CI on every push, so a stale README fails the build instead of quietly
persisting.
The one thing that still needs a human is writing a new directive's
## Interface Overview when it lands, the same way any judgment-call
frontmatter field is authored by hand. Discovery, names, syntax, examples
and counts are automatic.
MIT License.
