Skip to content

ctx evolution

ctx evolution prints every commit that touched a page together with what that commit changed — ctx history and a per-commit ctx diff folded into one request.

Use it when you’ve just landed on an unfamiliar page and the question is “how did this get to be what it is?” — a decision that reversed itself, a section that kept getting rewritten, an entity page that grew for six months. It is also the command to reach for instead of scripting a loop of ctx diff over ctx history output: one HTTP call replaces one history call plus N diff calls.

What trips people up is the default rendering. Without --show-diff each commit gets a one-line summary that names at most four changed items and then says +N more; unchanged sections are omitted from the summary entirely. It is a scan-friendly index, not the diff. Pass --show-diff when you actually need to read the content.

Terminal window
ctx evolution <slug>
ctx evolution <slug> --show-diff
ctx evolution <slug> --limit 5
ctx evolution <slug> --json
FlagTypeDefaultDescription
--blameboolfalseannotate each section with the commit that originally introduced its heading
--jsonboolfalseemit raw JSON instead of formatted text
--limitint20max commits in the evolution
--show-diffboolfalseinclude the full per-section diff under each commit
--typestringpage type (concept|entity|analysis|source); only needed when the slug is ambiguous

--limit keeps the N most recent commits and drops everything older. It does not change how any individual entry is computed: each commit is still diffed against the previous commit that touched this page, whether or not that commit made the cut. A value of 0 or less is ignored and the server falls back to its own default of 20.

--show-diff inlines the full ctx diff rendering under each commit, indented four spaces, including that commit’s own from..to header.

--blame uses the same machinery described on ctx diff — it annotates each section with the commit that first introduced its heading. Note it runs that history walk once per entry, so --limit 20 --blame performs twenty separate walks; keep --limit small when you combine them.

--json emits an array of {commit, diff} objects: commit carries sha/author/email/time/message, and diff is the same SectionDiff shape ctx diff --json produces.

FlagTypeDefaultDescription
--rootstringproject root directory (default: current directory)

The trajectory of a page at a glance:

Terminal window
ctx evolution stripe-subscription
ctx evolution stripe-subscription
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 Questions

The summary line under each commit uses the same markers as ctx diff with the ## prefix stripped: ~ modified, + added, - removed, ~> renamed, plus a bare frontmatter token when any frontmatter field changed and preamble when the text above the first heading changed.

The same thing with the actual content:

Terminal window
ctx evolution stripe-subscription --show-diff
ctx evolution stripe-subscription --show-diff
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’s header: diff: ..a4b2c1d. An empty left-hand sha means that commit is the page’s birth — there was no prior version, so every section and every frontmatter key shows as added. You only see that when the page’s whole history fits inside --limit; if the list was truncated, even the oldest entry shown has a normal parent..sha range.

Recent history only, with attribution:

Terminal window
ctx evolution stripe-subscription --limit 5 --blame
  1. The slug is resolved against your local working copy — .contexo/wiki/concepts/, .contexo/wiki/entities/, .contexo/wiki/analyses/, .contexo/raw/sessions/ — and --type narrows that search when the slug is ambiguous. The page must exist locally even though the content comes from the server.
  2. repo_id comes from .contexo/config.json; the bearer token and server URL come from .contexo/credentials.json, whose URL wins over config.json.
  3. The CLI issues GET /v1/repos/<repo_id>/evolution/<path>?limit=N, appending &blame=true only when --blame was passed.
  4. The server checks membership, runs git log -n<limit> -- <path> newest-first, and for each commit reads the page’s blob at that sha and at the previous commit that touched the same path, then runs the section differ over the pair.
  5. A commit where the path isn’t present at that sha is skipped rather than failing the whole request.
  6. Blame, when requested, is applied per entry after the diffs are built and is best-effort — failures are swallowed and the entry comes back unannotated.
  7. Nothing is written locally and no server state changes. ctx evolution is read-only.

sync: page evolution (404): {"error":"path not present in repo"} — the page exists in your working copy but has never been pushed, so the server has no history for that path. Push it with ctx push, or use ctx diff <slug> --local to see what’s there. Note the asymmetry: ctx history prints No commits found and exits 0 for the same situation, while ctx evolution treats it as an error.

slug "x" not found in .contexo/ (looked in wiki/{concepts,entities,analyses}/ and raw/sessions/) — the page isn’t in your working copy. Run ctx pull.

slug "x" is ambiguous (2 matches: [...]); pass --type to disambiguate — the same slug exists under two page types.

evolution: not authenticated (run 'ctx auth login') — no readable .contexo/credentials.json.

evolution: server URL or repo_id not configured — set them with ctx remote.

sync: page evolution (403): {"error":"not a member of this repo"} — you’re authenticated but not on this repo’s member list.

No evolution found for wiki/concepts/x.md — the server returned commits but every one of them was skipped because the path wasn’t present at that revision. Rare; usually means the page was moved rather than edited.