Building Chronicle: durable documentation on MCP and Temporal

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.

Chronicle architecture: an AI agent calls the Chronicle MCP server (tools and resources), which starts durable Temporal workflows that run activities over a manifest and docs store; docs are served back to the agent as resources.
How a request flows: agent to MCP server to durable workflows to activities to the manifest and docs store, then back.

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:

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.

The generate_doc tool starts GenerateDocWorkflow, which runs the generate_prose activity (the one network call to an LLM, with bounded retries) and then the shared persist tail: write the doc, re-hash the source, and update the manifest.
generate_doc reuses the same persist steps as a manual write. Only the prose step is new.

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:

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.