How a page evolved
A page in a shared knowledge base is worth more than its current text. When did we learn this? Who added the webhook section? What did the last edit actually change? Contexo answers those with three commands that all read the same server-side git history, and differ only in what they hand back.
| You want to know | Reach for |
|---|---|
| When this page changed, and who touched it | ctx history |
| What one specific edit changed | ctx diff |
| Who first wrote a given section | ctx diff --blame |
| The whole trajectory, every commit + what it did | ctx evolution |
| What my uncommitted local copy would change | ctx diff --local |
All of them resolve the slug against your local working copy — .contexo/wiki/{concepts,entities,analyses}/<slug>.md
and .contexo/raw/sessions/<slug>.md — and then ask the server for the commits. A page a teammate
created but you have never pulled will fail to resolve before any request is made. Pull first.
Start with the timeline
Section titled “Start with the timeline”b7e9f204 2026-07-28 dev ctx push (1 pages)
a4b2c1d3 2026-07-28 dev ctx push (1 pages)Columns: 8-character sha, date, author padded to 12 characters, commit subject. ctx push without
-m writes ctx push (N pages); an agent pushing through ctx_push writes agent push (N pages),
which is a quick way to tell who — or what — was driving.
History is per path, produced by git log -- <path> with no rename following. Rename a page’s
slug and the new path starts a fresh timeline while the old path keeps every commit before the
rename. --limit defaults to 50.
Those shas are the input to everything below.
Then diff one edit
Section titled “Then diff one edit”With no --from/--to, ctx diff compares the page’s most recent change: HEAD-for-this-path
against its parent.
diff: stripe-subscription a4b2c1d..b7e9f20
Frontmatter
~ updated
- 2026-07-20T10:04:00Z
+ 2026-07-24T16:31:00Z
+ tags: proration
Sections
~ ## Concepts (2 lines changed)
+ ## Webhooks (2 lines added)
= ## Agent Reasoning
- ## Open Questions (1 lines removed)This is not a line diff with markdown in it — the differ splits both sides into frontmatter,
preamble and ## sections, and reports each part separately. Reading the output:
~ updated— a scalar frontmatter field changed.-is the old value,+the new.+ tags: proration— list-valued frontmatter fields use set semantics. This means one element was added totags, not that the field appeared. A removed element shows as- tags: ….~ ## Concepts (2 lines changed)— the section exists on both sides with a different body. The count is the number of+/-lines in that section’s own line diff; the full text is in--json.+ ## Webhooks (2 lines added)— a new section.= ## Agent Reasoning— present on both sides, byte-identical. Unchanged sections are listed rather than hidden, so you can see what an edit deliberately left alone.- ## Open Questions (1 lines removed)— gone in the newer version.
A fifth status you’ll meet occasionally is
~> ## New heading (renamed from "## Old heading", 3 lines changed) — the line count is dropped
when the body is byte-identical and only the heading moved. An unmatched removed/added pair is
treated as a rename when the two bodies overlap by at least 70% (Jaccard over word tokens); below
that threshold it stays a genuine delete plus a genuine add.
If either side has unparseable frontmatter the differ gives up on structure and sets
parse_fallback: the whole file collapses into a single _preamble entry holding a line diff of
both versions. The text output says so in one line —
(frontmatter unparseable on at least one side; whole-file line diff below) followed by
~ preamble (N lines changed) — and the diff itself is only in --json.
--blame: who introduced this section
Section titled “--blame: who introduced this section”diff: stripe-subscription a4b2c1d..b7e9f20
Frontmatter
~ updated
- 2026-07-20T10:04:00Z
+ 2026-07-24T16:31:00Z
+ tags: proration
Sections
~ ## Concepts (2 lines changed) — introduced by dev at a4b2c1d
+ ## Webhooks (2 lines added) — introduced by dev at b7e9f20
= ## Agent Reasoning — introduced by dev at a4b2c1d
- ## Open Questions (1 lines removed)Blame here is heading-level, not line-level. The server walks up to the 200 most recent commits
for the path, oldest first, parsing the ## headings present at each revision and recording the
first commit in which each heading appeared. Every section in the diff then gets annotated with that
commit.
Consequences worth internalising before you trust the answer:
- It attributes the heading, not the prose. If somebody rewrote every word under
## Conceptslast week, the annotation still names whoever created the heading. - Removed sections print no annotation — look at the
- ## Open Questionsline above. That is the text renderer, not the data: the server still resolves anintroduced_byfor a removed heading, and--jsonshows it. - Renamed sections prefer the old heading’s blame, falling back to the new one if the old heading was never seen.
- The walk covers only the 200 most recent commits for the path. A heading introduced before that window is attributed to the oldest commit inside it — an answer that is too recent rather than absent.
- Blame is best-effort: if it fails, it is silently dropped and you get the plain diff back. It also costs one file read per commit, so it is noticeably slower on long histories.
ctx evolution: the whole arc in one round-trip
Section titled “ctx evolution: the whole arc in one round-trip”ctx history plus one ctx diff per commit is N+1 requests. ctx evolution is one:
Evolution of wiki/concepts/stripe-subscription.md (2 commits, newest first):
b7e9f204 2026-07-28 dev ctx push (1 pages)
frontmatter, ~ Concepts, + Webhooks, - Open Questions
a4b2c1d3 2026-07-28 dev ctx push (1 pages)
frontmatter, + Concepts, + Agent Reasoning, + Open QuestionsEach commit line is followed by a one-line summary of what that commit changed — the same summary
format the push preview uses, capped at four parts with a +N more overflow.
--show-diff inlines each commit’s full section diff instead:
Evolution of wiki/concepts/stripe-subscription.md (2 commits, newest first):
b7e9f204 2026-07-28 dev ctx push (1 pages)
diff: a4b2c1d..b7e9f20
Frontmatter
~ updated
- 2026-07-20T10:04:00Z
+ 2026-07-24T16:31:00Z
+ tags: proration
Sections
~ ## Concepts (2 lines changed)
+ ## Webhooks (2 lines added)
= ## Agent Reasoning
- ## Open Questions (1 lines removed)
a4b2c1d3 2026-07-28 dev ctx push (1 pages)
diff: ..a4b2c1d
Frontmatter
+ agent: claude
+ author: dev
+ created: 2026-07-20T10:04:00Z
+ parent_sha:
+ reasoning_summary: How trials, proration and webhooks fit together in acme-api.
+ related: [webhook-retries]
+ schema: ctx.page.v1
+ slug: stripe-subscription
+ tags: [stripe billing]
+ type: concept
+ updated: 2026-07-20T10:04:00Z
Sections
+ ## Concepts (2 lines added)
+ ## Agent Reasoning (3 lines added)
+ ## Open Questions (1 lines added)Note the oldest entry: its range prints as ..a4b2c1d and everything is an addition, including
each frontmatter field individually. That’s not a quirk of the display — the first commit for a path
has no prior version, so it is diffed against an empty document. Any evolution whose window reaches
back to the page’s first commit therefore ends with one all-+ entry.
--limit defaults to 20 here (not 50). --blame works too, but it annotates each entry
independently: N commits means N history walks, so combine it with a small --limit.
--local: what would my push change?
Section titled “--local: what would my push change?”Everything above compares two server-side revisions. --local compares your working copy against the
server’s current version of the same page — the same computation the pre-push preview runs, without
pushing:
diff: stripe-subscription a4b2c1d..local
Frontmatter
~ updated
- 2026-07-20 10:04:00 +0000 UTC
+ 2026-07-24 16:31:00 +0000 UTC
+ tags: proration
Sections
~ ## Concepts (2 lines changed)
+ ## Webhooks (2 lines added)
= ## Agent Reasoning
- ## Open Questions (1 lines removed)It is mutually exclusive with --from, --to and --blame (there is no sha to blame against), and
if the page doesn’t exist on the server yet, every section reports as added.
One cosmetic tell: compare the updated values here with the ones in the server-side diff further
up. --local renders 2026-07-20 10:04:00 +0000 UTC, the server-side diff renders
2026-07-20T10:04:00Z. The local diff is computed in-process and keeps a real timestamp value; the
server-side diff crosses JSON on the way back, which turns it into an RFC 3339 string. Same change,
different renderer.
When the agent asks instead of you
Section titled “When the agent asks instead of you”The MCP tools are the same three endpoints with the same defaults:
ctx_history, ctx_diff, ctx_evolution.
ctx_history returns a formatted text timeline; ctx_diff and ctx_evolution return the raw
structured JSON so the model can introspect statuses and headings rather than parse a rendering.
ctx_evolution is the one to prefer when an agent is picking up a topic cold — it is explicitly
described to the model as the way to get a page’s whole trajectory without N round-trips. Point an
agent at ctx_diff when it is about to edit one specific page and needs to know what the last person
changed.
Common snags
Section titled “Common snags”no parent commit for this path; pass ?from explicitly — you asked for the default
parent..head on a page that has exactly one commit. Nothing precedes it; the evolution view handles
this case for you.
No commits found for wiki/concepts/x.md — the page exists locally but was never pushed. Not an
error; the command prints the line and exits 0.
A page that “lost its history” — check for a slug rename. History is per path.
Everything fails offline. All three commands read the server’s copy of the repo. There is no local fallback, and no local git repository to fall back to.
See also
Section titled “See also”ctx history,ctx diff,ctx evolution— full flag referencectx log— repo-wide timeline, all pages- Drift and conflicts — what to do when the page moved under you
