Skip to content

ctx ask

Design & internals — not product documentation. This doc explains why a mechanism exists and how it was reasoned out; it may describe an idea before it ships or record one that was rejected. For what the product does today, prefer spec/ and the changelog, and read any status label literally. Want to use ctx ask? The CLI guide has the seven intents and examples; this doc is the design behind them.

Date: 2026-07-20 · M-L, shipped v0.27.0 (Phase 0 + core intents). The adopted core of an external ctx ask retrieval proposal, audited against the house constitution and resequenced so the elegant, testable pieces land first and the natural-language front door lands last (or never).

The product sentence: ctx ask compiles a repository question into the smallest complete, addressable evidence view the current decision requires — by selecting a typed intent preset, not by guessing English.

The proposal’s spine is right and cheap: compile a question into an intent contract, execute it on the shipped plan executor, answer with a candidates · counterevidence · coverage receipt. That collapses the decision cost of exploration (which verbs, in what order) the way ctx plan (M-J) collapsed its turn cost. It obeys every house rule — no second executor, no evidence graph, no vector DB, no model-call classifier. So we built the spine. We did not build the packaging around it (see §5, Cut).

Not an ontology, not a product, not an NL parser output: a frozen ctx.plan/v1 template with typed slots (src/ctx/ask.py). The Ponytail move — compress INTENT, not English:

ctx ask "Why is test_build failing" --intent diagnose
▼ compile_ask (pure, total, deterministic)
ctx.plan/v1 {changes · evidence.failures · join · counter-join · context}
▼ execute_plan (SHIPPED, tier=cli)
investigate/v1 digest {conclusion candidates · counterevidence · coverage · next}

Because compilation is json.dumps(sort_keys=True) over the slots, the same slots always produce the same plan id — so every node-cache key is stable across phrasings that resolve to the same slots. Determinism is inherited from the plan tier, not re-implemented.

Seven intents ship (the observe five plus two execute-class):

intentquestionplan shapeclass
locatewhere is X defined and used?refs → warmed symbol rows → per-file census → definition bodiesobserve
impactwhat could break if X changes?callers → bounded blast radius → related tests → changesobserve
diagnosewhat explains the captured failures?changes × captured failures → root-cause join → counter-join → failing-frame contextobserve
tracehow does control/data flow through X?X’s sites → callers (in) → callees (out) → transitive reach (hop-grouped)observe
comparewhat differs between two runs?evidence.diff (the shipped run-diff as a node)observe
verifywhat proves this change is correct?changes → related tests → run the suite (birth gate)execute
reviewwhat changed, what is risky, what is under-verified?changes → symbols → tests → run → root-cause join + counterevidenceexecute

The observe five are observe-class end to enddiagnose reads evidence.failures (captured facts), it never reruns tests. verify and review are execute-class: they run tests under the birth gate, so they are CLI-only — the plan validator rejects test.run on the bounded MCP tier (execute_on_observe_tier), and the intent discloses its class up front. compare needs two run refs (--run A --against B); its slots teach when missing, like every other intent’s subject.

Two invariants are structural, not optional: counterevidence is a real join node (the investigate renderer prints the section even when empty — anti-anchoring), and the only text-emitting node is code.context (and evidence.diff/compare, terminal by construction), so bytes materialize exactly once, terminally (the DIGEST-CLOSURE law).

No natural-language parser (the deliberate omission)

Section titled “No natural-language parser (the deliberate omission)”

The proposal wanted English in, keyword-matched to an intent. We rejected NL classification as the primary path, for a measured reason: the product’s own doctrine is compact model-authored intent — an agent that serializes its intent into prose so a matcher can guess it back has paid a lossy round trip. So:

  • Intent is a flag. --intent locate|impact|diagnose. Missing → a teaching error that suggests one (your question looks like --intent diagnose (advisory — nothing was run)) and stops. It never guesses and executes.
  • Subject is a flag or the one unambiguous case. --symbol X, or the question’s sole identifier-shaped token (dotted / snake_case / CamelCase with an internal capital — “Where” and “What” are capitalized English, not subjects). Exactly one candidate → inferred and disclosed; zero or many → a teaching error naming the candidates. infer_symbol is pure and total.
  • The interpretation always shows. The disclosure (intent: / subject: / run:) rides above the digest, never behind --trace.

This makes ask’s value proposition routing (which preset, correctly slotted) rather than compression — and routing is honest for an agent, while NL-in is a genuine win for a human at a terminal. The §6 referee must measure both audiences separately.

The thin ops (Phase 0 — the reusable seams)

Section titled “The thin ops (Phase 0 — the reusable seams)”

Three observe-class plan ops, useful to ctx plan/investigate immediately, independent of ask:

  • evidence.failures — failure census from captured facts, never a rerun. run absent → the latest derived run. Freshness against the current generation is computed and declared: a census whose facts were stamped at gen:A while the worktree is now gen:B carries fresh: false and a note proposing (never running) a refresh. This is the observe invariant made legible — the same generation semantics as the rest of the system (generation_hash: porcelain bytes + untracked size/mtime; a tracked-modified file’s generation is its status line, so editing an already-dirty file correctly does not move it).
  • code.symbols — structured symbol rows (identity · kind · range · span) from skeleton-derived facts; census before detail, no outline text. An input warms facts for exactly those files (content-keyed, so unchanged files cost nothing).
  • code.context — terminal bounded materialization: sites get line ± context, symbols get their clamped range. Emits text; by the closure law nothing downstream can lift bytes back into a representation. This is the refinement boundary at the plan tier.

Optional-input code.refs/callers/callees/impact (the proposal’s §7 “add” item) already shipped in M-J — a source op accepts an input as a capped foreach feed (plan_ir.py). Nothing to build.

On a seeded regression (a raise inside a changed function, its failing pytest run captured under the birth gate), ctx ask "Why is test_build failing" --intent diagnose returns, with no test rerun:

conclusion candidates (census): 1
1. build · repo:cache.py:L2 · 1 test(s) · ValueError · planes dynamic+temporal+static
tests: test_cache.py::test_build
counterevidence:
none found (1 probe(s) executed)
coverage:
fails · evidence.failures · engine facts.sqlite · 1 rows · run:57a0f05c795c
culprits · evidence.join · engine facts.sqlite · 1 rows

One command read captured failures, joined them against the change set, and named the culprit symbol with plane attribution — the round-1 haiku scenario that once burned eight turns, in one digest.

What was cut, and why (the audit’s teeth)

Section titled “What was cut, and why (the audit’s teeth)”

A design review earns its keep by what it declines. From the proposal:

ProposedDispositionReason
NL parser as the primary pathCut → flag + one unambiguous inferencelossy round trip for agents; misroutes its own examples (“How does … reach” = trace, not explain). Keep as optional sugar later, over presets.
ctx reveal / ctx audit new verbsCutone line each, unspecified; the four-verb rebrand is a CLI migration nobody scoped
Rebrand run/ask to the whole public surfaceDeferredask ships as a compiler in front of the existing verbs; the rebrand is its own versioned decision
view: handles, ask show, role projectionsDeferredthe investigate digest already indexes addressable node blobs; projections need a measured multi-worker use, not a speculative noun
The §4 entity/relation/operation ontology (11 · 14 · 11)Cutno predicates behind taints/co_fails_with/verified_by; the shipped op names in the intent presets are sufficient and real
verify / review / trace / compare intentsShipped (v0.29.0)trace/compare observe-class; verify/review execute-class with the bounded-tier rejection enforced by the plan validator
Anticipatory inliningDeferred to shadowpromote only from measured exact follow-ups (the house Wilson-gate idiom), never on a hunch

The keep list underlines the two things better than the average version of this idea: the ship gate is measured per intent (“do not hide a weak intent behind an aggregate win”), and interactive exploration stays as the long-tail escape hatch — contracts are a fast path, not a mandatory ontology.

Shipped gates (v0.27.0): compiler determinism (same slots → same plan bytes); teaching errors that suggest but never guess-and-run; the no-rerun invariant (diagnose plans contain no test.run, proven at compile time and end to end); freshness declared on stale facts; code.context terminal by the closure law. tests/test_ask.py.

Open, deliberately: the §6 A/B/C referee — ordinary exploration (A) vs ctx q/get (B) vs ctx ask (C) — gated at retrieval turns ≤ 50% of B, task success ≥ B, decisive recall ≥ B, unresolved omissions = 0, low-complexity overhead ≤ 5%, measured per intent. Presets-before- parser gives that referee its C arm now; it runs with the next eval wave.

now ──► Phase 0 thin ops (evidence.failures · code.symbols · code.context) ✅
Phase 1 intent presets + ctx ask (locate · impact · diagnose) ✅
Phase 2 trace · compare (observe) ✅ · Phase 3 verify · review (execute) ✅
next ──► the A/B/C referee, per intent (retrieval turns ≤ 50% of ctx q/get) ─┐
NL compiler as sugar over presets (misclassification telemetry) ┤
role projections + view manifest (after a measured multi-worker need)
later ─► shadow anticipatory prefetch (Wilson-gated, like every other promotion)

Governing rule, kept from the proposal’s best instinct: exact facts and indexes precede structural, textual, and neural retrieval; retrieve relationships, not chunks; return candidates, counterevidence, coverage, and addresses; materialize bytes only at the end; and reuse the shipped executor, cache, and evidence layer rather than growing a second of anything.