ctx_push
ctx_push sends the local .contexo/ pages to the team server. It is the tool with the
most behaviour behind it: one call can come back as a plain result, as a
PUSH_PAUSED handshake asking the agent to write the reasoning trail first, or as a
MERGE_REQUIRED three-way merge. Both of those are agent
directives — instruction blocks the agent is expected to act on and
then call again.
When the agent calls it
Section titled “When the agent calls it”From the tool description: “Use when the user says something like ‘sync my stripe
knowledge to contexthub’ or ‘share this with the team’.” Anything that reads as
“publish what we just figured out” lands here. Filters let the agent push a subset
rather than the whole store — feature (an alias for tag), tag, or type.
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
distill_done | boolean | no | Set true after authoring the source page that the previous PUSH_PAUSED response requested |
feature | string | no | Tag to filter by (e.g. 'stripe') |
message | string | no | Commit message |
no_distill | boolean | no | Skip the reasoning-trail handshake for this push |
source_slug | string | no | Slug of the source page just written (required when distill_done=true) |
tag | string | no | Alias of feature |
type | concept | entity | source | analysis | no |
feature and tag are the same filter; feature wins if both are set. Both match
tags case-insensitively. type filters on the page type. With no arguments, every
page in the store is pushed.
What a plain push does
Section titled “What a plain push does”- Lists the store, applies the filters. No match →
Nothing to push (no pages match filters). - Serializes each page and pairs it with its
parent_shafrom.contexo/.sync/state.json(page_shas). A page with no entry there is sent with noparent_shaat all, and the server skips its conflict check for that file — it is written whether or not the path already exists there. - POSTs the batch with the author name/email from stored credentials and
message(default:agent push (N pages)). - Records the returned sha for each accepted file back into
page_shas.
The server writes and commits one file per commit, all sharing the push message, so
pushed[] carries a different sha per page and the reported HEAD is the last one. A file
whose content is byte-identical to what is already there produces no commit at all —
pushing the whole store repeatedly is cheap and does not create empty commits.
Request and response
Section titled “Request and response”{ "jsonrpc": "2.0", "id": 11, "method": "tools/call", "params": { "name": "ctx_push", "arguments": { "feature": "stripe", "message": "stripe trials: subscription-level trial_end" } }}{ "jsonrpc": "2.0", "id": 11, "result": { "content": [{ "type": "text", "text": "Pushed 2 page(s); HEAD=a4b2c1d3" }] }}The HEAD in the text is shortened to 8 characters. Everything this tool returns — success, pause, merge, error — arrives as that single text block; the agent reads it and decides what to do next.
The distill handshake
Section titled “The distill handshake”A push that would ship knowledge pages without the reasoning behind them pauses
instead. The tool returns <PUSH_PAUSED reason=distill_required>, and it fires only
when all of these hold:
- the batch contains at least one
conceptoranalysispage, and - a capture buffer for this project was modified within the last 6 hours and has at least one recorded turn, and
no_distillis not set, and- the environment variable
CONTEXO_DISTILL_DISABLEis not1.
Entity-only or source-only batches never trigger it. Neither does a project with no capture hooks installed — nothing has been recorded, so there is nothing to distill.
The directive asks for three steps, inlines the session buffer so the agent has the raw
material, and suggests a slug of the form YYYY-MM-DD-<first-concept-slug>-reasoning:
<PUSH_PAUSED reason=distill_required>
Before pushing: capture the reasoning trail, then make sure every named systemin this batch has its own entity page. Do the three steps below in order, thenre-invoke ctx_push with distill_done=true.
STEP 1 — call ctx_write_page with: type: "source" slug: "2026-07-28-stripe-subscription-reasoning" (suggested; adjust topic to taste) tags: [stripe billing] (union of the batch's tags) reasoning_summary: one-line distillation (<= 100 chars) body: follow the TEMPLATE below
TEMPLATE (drop sections that genuinely don't apply, keep them in this order):
## Decision ## Why this approach ## Rejected alternatives ## Path of inquiry ## Dead-ends ## Open questions ## Sources [each section carries a one-line instruction in the real directive]
IMPORTANT: redact any API keys, tokens, passwords, or PII you encounter.
STEP 2 — entity coverage: if the concept/analysis pages reference namedsystems, services, libraries, products, or databases that don't already havea page under wiki/entities/, create one for each with ctx_write_page(type="entity").Keep entity pages short: purpose, where it lives in this project, gotchas, andlinks to related concepts. Skip this step if every named thing is already covered.
STEP 3 — call ctx_push again with the same filter args plus: distill_done: true source_slug: "2026-07-28-stripe-subscription-reasoning"
That distill_done call will link the source into each concept/analysis page's`sources:` frontmatter, archive the buffer, and push everything in one commit.
---
PAGES being pushed in this batch:- wiki/concepts/stripe-subscription.md (concept)
BUFFER (turn-by-turn summaries from this session, oldest first): [one JSON object per turn, oldest first; long sessions keep the first 10 and last 40 turns with a truncation marker between them]
</PUSH_PAUSED>Re-invoking with distill_done
Section titled “Re-invoking with distill_done”The second call must carry both distill_done: true and source_slug, plus the
same filters as the first. source_slug is the slug of the page just written with
type: "source" — the tool reads raw/sessions/<source_slug>.md and refuses the push
if it is not there:
{ "jsonrpc": "2.0", "id": 13, "method": "tools/call", "params": { "name": "ctx_push", "arguments": { "feature": "stripe", "distill_done": true, "source_slug": "2026-07-28-stripe-subscription-reasoning" } }}That call then, before sending anything:
- appends
source_slugto thesources:frontmatter of everyconceptandanalysispage in the batch, bumps theirupdated, and rewrites them on disk; - adds the source page itself to the batch if the filters didn’t already include it;
- archives the capture buffer (moves it to
raw/sessions/_pending/_archive/) so the same session can’t trigger the handshake twice.
So the reasoning page and the pages it explains go up together, cross-linked.
Set no_distill: true to skip the handshake for one push — that is what
ctx_migrate and ctx_generate instruct the
agent to do, since imported or code-derived pages have no session reasoning to capture.
When someone else pushed first
Section titled “When someone else pushed first”If any file in the batch has moved on the server since your last pull, that file’s write
is rejected (HTTP 409) and the tool returns <MERGE_REQUIRED> instead of a bare error.
Files in the same batch that did not conflict are still committed.
The directive carries, per conflicting file: the ancestor sha both sides diverged from, the server’s current sha, a per-section summary of what each side changed, the sections modified by both sides, and all three full versions.
<MERGE_REQUIRED>Push rejected: 1 file(s) have moved on the server since your edit.
Reconcile each file below by writing a merged version thatincorporates BOTH your changes AND the server's changes, thenre-invoke ctx_push. The state has been updated so the re-pushuses the server's current sha as the new parent.
─── FILE 1/1: wiki/concepts/stripe-subscription.md ─── ancestor (both diverged from): a4b2c1d server now: 9f31c07
YOUR changes vs ancestor: ~ frontmatter changed ~ ## Trials
SERVER changes vs ancestor: ~ ## Trials + ## Refund handling
CONFLICTING regions (both sides changed): ## Trials — synthesize both edits into one coherent version
--- ANCESTOR VERSION --- [full page bytes] --- YOUR VERSION (the one you tried to push) --- [full page bytes] --- SERVER VERSION --- [full page bytes]
STEPS for the agent: 1. For each file above: write a merged version via ctx_write_page(slug=..., type=..., body=<merged content>) The merged page must preserve BOTH sides' intent. For prose sections that conflict, synthesize — don't pick one. 2. Re-invoke ctx_push with the SAME filters. The local sync state has been updated to point at the server's current sha for each conflicting file, so the next push will not 409 unless someone pushed again in the meantime.</MERGE_REQUIRED>When no section was touched by both sides, that block is replaced by
(No section was modified by both sides — auto-mergeable in principle.) — the agent
still has to write the merged page, but it can take both sides wholesale.
Drift and conflicts walks through the whole cycle,
including the <DRIFT_NOTICE> that ctx://wiki/<slug> reads prepend so the agent
learns about divergence before it edits.
Errors
Section titled “Errors”| Text | Cause |
|---|---|
ctx_push: server not configured (run 'ctx remote set <url>', 'ctx remote set-repo <id>', 'ctx auth login') |
No credentials, server URL, or repo id |
ctx_push: distill_done=true requires source_slug (…) |
distill_done set without naming the source page |
ctx_push: distill_done set but source page "…" not found at raw/sessions/….md |
The source page was never written, or its slug differs |
Nothing to push (no pages match filters) |
Filters matched zero pages (not an error result) |
CLI equivalent
Section titled “CLI equivalent”ctx push pushes the same batch with the same filters.
About to push 1 page(s) to acme-api:
[NEW] wiki/concepts/stripe-subscription.md
Pushed 1 page(s); HEAD=a4b2c1d3Editing a page that already exists shows a per-section summary of what will change, and
in a non-interactive shell requires --yes to proceed:
About to push 1 page(s) to acme-api:
[EDIT] wiki/concepts/stripe-subscription.md (frontmatter, ~ Concepts, + Webhooks, - Open Questions)
Pushed 1 page(s); HEAD=b7e9f204About to push 1 page(s) to acme-api:
[NEW] wiki/concepts/stripe-subscription.md
(dry-run; no changes sent)| Flag | Type | Default | Description |
|---|---|---|---|
--dry-run | bool | false | show what would be pushed without sending |
--fallback-server | bool | false | (planned) route reasoning-trail distillation to the Contexo server; currently errors |
--feature | string | push only pages tagged with this feature/tag | |
--message, -m | string | commit message (default: 'ctx push (N pages)') | |
--no-preview | bool | false | skip the pre-push preview entirely (faster; loses the heads-up about what changes) |
--show-diff | bool | false | print the full per-section diff in the preview, not just a one-line summary |
--tag | string | push only pages with this tag (alias of --feature) | |
--type | string | push only pages of this type (concept|entity|source|analysis) | |
--yes, -y | bool | false | skip the pre-push confirmation prompt (required for non-interactive use when editing existing pages) |
Two differences from the MCP tool, both deliberate:
- No distill handshake. The agent-as-distiller flow only exists over MCP;
ctx pushnever pauses to ask for a reasoning page. (--fallback-serveris reserved for a server-side distiller and currently errors.) - No merge directive. On 409 the CLI prints the conflicting paths and tells you to
ctx pull, merge by hand, and push again; it does not rewrite your sync state.
See ctx push for the full command reference.
