Documentation goes stale because nothing keeps it tied to the code. You rename a function, and the paragraph describing it is now wrong, but nothing in your toolchain knows that. For a human reader that's a minor annoyance. For an AI agent reading your docs to make changes, a confidently wrong doc is worse than no doc at all.
Chronicle is my attempt to deal with that. It's a documentation server that generates and serves docs for a codebase, tracks when those docs drift from the source, and exposes everything over the Model Context Protocol (MCP) so people and agents read from the same place. Every operation runs as a durable Temporal workflow.
The Temporal part is the one that usually needs explaining, so I'll get to it. First, what the thing actually does.
One source, two readers
Docs have two kinds of reader, and I didn't want them reading two different copies.
Developers want Markdown they can open in a repo or a browser. Agents (Claude Code and similar) want something they can pull into context through an API. Chronicle keeps one set of documents and exposes them both ways. The Markdown files are the developer view. The same documents show up as MCP Resources, addressed by URI:
doc://path/to/module a single rendered doc page
manifest://index the index of every doc and what it covers
Both views resolve to the same underlying file, so there's no gap between what the wiki says and what the README says. There's one document.
Why MCP
MCP lets me expose two kinds of thing through one protocol: tools, which are actions an agent can call, and resources, which are readable content an agent can pull in.
That covers the whole loop an agent needs. It can call check_drift to find stale
docs, read the current source, write fresh prose back, then read the updated
doc:// resource to confirm. No custom API, no glue code on the agent side. The
tool set is small on purpose:
scan_repo list source files matching the glob
check_drift re-hash code, report stale / missing / fresh docs
propose_doc_update(src, doc) return the source plus the existing doc to rewrite
write_doc(src, doc, content) persist the doc and update the manifest
generate_doc(src, doc) have an LLM write the doc end to end
Why Temporal, for docs
This is the choice people ask about. A tool call doesn't do the work directly. It starts a durable workflow. The MCP server is a thin Temporal client, and the real logic lives in workflows that coordinate activities. Activities are the steps that touch the outside world: hashing files, reading and writing docs, calling an LLM.
The reason is that documentation work looks simple and then isn't. Generating docs across a large repo is a long, multi-step job. If it fails partway through, on a network blip or an LLM timeout, I don't want to redo the half that already worked. Temporal persists workflow state and handles retries and recovery, so a partial failure picks up where it left off. The LLM step benefits most, since it's the slowest and the most expensive to repeat.
It also keeps the roadmap simple. A nightly job that regenerates every drifted doc, or fanning out across several repos, is just more workflows. The durability is already handled, so those features don't each reinvent retry and recovery.
So the docs are ordinary content, but keeping them correct continuously, across failures, is a distributed-systems problem. That part is what Temporal is good at.
The manifest and drift detection
Drift detection is intentionally simple, which is what makes it trustworthy. The manifest
stores, for each documented source file, a content hash of the source and of the doc written
against it. check_drift re-hashes the current code and compares:
- fresh: the source hash matches what the doc was written against.
- stale: the source changed since the doc was written.
- missing: the source has no doc yet.
There's no guessing about whether something looks different. A hash matches or it doesn't. A CI gate or a cron job gets a clear, machine-readable list of which docs need work.
Autonomous prose, opt-in
The newest piece is generate_doc. Instead of handing the source back to an agent
to rewrite, Chronicle can write the doc itself. It runs as its own workflow:
GenerateDocWorkflow calls a single generate_prose activity (the
only network call), then reuses the same persist step as write_doc to write the
file and update the manifest. Whether an agent wrote the doc or the LLM did, the persist path
is identical.
In code, the workflow is small. The one network call is bounded by a retry policy; everything after it is the same tail the manual write path uses:
@workflow.defn
class GenerateDocWorkflow:
@workflow.run
async def run(self, repo_root, docs_dir, source_rel, doc_rel,
model, manifest_path, base_url=None) -> dict:
# the one network call: let the LLM write the doc (bounded retries)
content = await workflow.execute_activity(
generate_prose_activity,
args=[repo_root, docs_dir, source_rel, doc_rel, model, base_url],
start_to_close_timeout=timedelta(seconds=120),
retry_policy=RetryPolicy(maximum_attempts=3),
)
# the same persist steps the manual write path uses
await workflow.execute_activity(write_doc_activity,
args=[repo_root, docs_dir, doc_rel, content], start_to_close_timeout=_SHORT)
hashed = await workflow.execute_activity(hash_batch,
args=[repo_root, [source_rel]], start_to_close_timeout=_SHORT)
await workflow.execute_activity(update_manifest_activity,
args=[manifest_path, source_rel, doc_rel, hashed[source_rel], ""],
start_to_close_timeout=_SHORT)
return {"source": source_rel, "doc": doc_rel,
"source_hash": hashed[source_rel], "generated": True}
The part I was careful about is that this is opt-in and bring-your-own-model. The LLM
dependency is an optional install (uv sync --extra llm). With no API key set, the
rest of Chronicle works normally. Drift detection, serving, and manual writes all run, and
only generate_doc declines, with a clear non-retryable error.
It's also not locked to one provider. There's a single seam:
ProseGenerator = Callable[[ProseRequest], str]
Right now that's backed by the Anthropic SDK, with Claude Sonnet as the default model. You can point it at any Anthropic-compatible gateway with a base-URL environment variable, so a LiteLLM proxy in front of another provider already works. A native non-Anthropic backend is a one-function swap behind that type.
Testing it without spending money
Two things kept this cheap to test.
First, Temporal ships a time-skipping test server. The workflow and end-to-end tests run against an in-process server that fast-forwards timers, so I can test retry and timeout behavior without waiting in real time.
Second, I didn't want a test suite that needs an API key or runs up a bill to go green. So the end-to-end prose test runs the real Anthropic SDK over real HTTP against a local stub gateway that returns canned responses. It exercises the actual client, the transport, the workflow, and the manifest update, with no API spend. The suite runs green on CI with no secrets attached.
What's next
Most of the roadmap is about turning Chronicle from a tool you call into something that runs on its own:
- A batch
generate_drift_docsthat loops generation over every stale doc, for CI or cron. - Git-diff change detection and a PR docs check, so a pull request can fail if it leaves docs behind.
- A file-watcher trigger, process-pool hashing for large repos, and multi-repo fan-out.
It's still early, and still private while I work through that list. But the foundation I wanted is there. The docs have a verifiable relationship to the code they describe, and the people and agents reading them get the same thing.