Skip to main content
The conversational assurance commands — context extract, design tests, and maintain reconcile — are interactive by default. This page is the contract for running them headless: from CI, from a script, or from an AI agent driving kane-cli.

The ask policy: --mode

On a terminal, extract and design open a chat. Headless is an explicit opt-in — a bare non-TTY invocation exits 2 and mutates nothing:
--mode decides both what happens when the agent has a question, and what the command writes to stdout: Rule of thumb: agent when something can read the pause and answer (an AI agent, a human on the next shift); ci when a pipeline must fail rather than guess on anything high-risk; override when you accept the recommended defaults wholesale and want one unattended pass. The same matrix drives maintain reconcile, with two reconcile-specific rules: no headless mode ever archives anything — ARCHIVE decisions wait for an interactive session — and a ci-mode run that hits a decision needing a human stores the plan and exits 2 (the work isn’t lost; walk the stored plan interactively or apply it in agent mode). context ingest follows the matrix with one extra rule (0.7.1): it lands the files and then runs the extraction under the given mode — except --mode ci, or piped stdin without any --mode, which lands only (exit 0, with a stderr guidance line naming the next command). Two extraction dials also matter headless: --trust hold holds everything new for review instead of committing it (headless-only; ci refuses the flag entirely with exit 2), and --trust auto is the default everywhere.

Exit codes

Consistent across extract, design, and the maintain commands that embed them:

The NDJSON stream (--mode agent)

With --mode agent, stdout speaks a versioned NDJSON vocabulary — envelope {"type": "<name>", "v": 1, "verb": "extract"|"design", ...}, one object per line. The vocabulary is open: new event types may appear, so tolerate unknown types. (0.7.2) The stream is strict: stdout carries only NDJSON — the first line is an event, done is the last — and stderr stays silent (crash traces excepted). No version banner, no receipts, no progress lines; everything user-relevant arrives as a typed event. On 0.7.1, prose diagnostics could ride stderr and a merged ingest printed receipt lines before the stream — a consumer that skips non-JSON prefix lines works on both releases. The done guarantee: every --mode agent invocation ends its stream with exactly one done event — including refusals and graceful interrupts. (0.7.2) The stream starts at the first line: a merged ingest’s landing failures (a bad path, an unsupported or oversized file, a refused URL, a misused --mode or --as) also arrive as error + done — codes MODE_USAGE, AS_SINGLE_SOURCE, UNSUPPORTED_URL, INGEST_FAILED — and sources already landed stay safe, with the run saying so. (On 0.7.1 these landing failures ended with a prose error line and exit 1/2 before any NDJSON began — no stream, no done.) The one exception is operator force: a second Ctrl+C can hard-kill the process (exit 130) without a done. Any other stream that ends without done should be treated as a crash. One more parsing note: the agent may also repair a draft mid-turn on its own — that surfaces only as agent_activity lines (labels like validation failed, refining the draft); treat activity labels as display text, never script against them. Two more parsing rules:
  • Receipts and prefixes. (0.7.2) Nothing precedes the stream — the landing receipts are the ingested events, and every stdout line parses as JSON. On 0.7.1, a merged ingest printed a few prose receipt lines per file before the NDJSON began, so strict per-line JSON.parse consumers had to skip non-JSON prefix lines. That skip is harmless on 0.7.2 — a version-tolerant consumer can keep it.
  • next[] carries follow-up commands (0.7.1). Pauses, gate refusals, and done can carry a next list of ready-to-run follow-ups. The common shape is objects ({cmd, why, title}); a few refusal sites emit plain strings — handle both, and treat every entry as a command to offer, not to auto-run.

Reconcile’s stream

maintain reconcile --mode agent speaks the same envelope with verb: "reconcile" and its own event set. (0.7.2) The stream opens with a minimal run_start (it carries session only — no trace), and the re-extract child rides the same stream: its extract-vocabulary events (source_start, agent_activity, plan, commit, …) interleave between the reconcile_* events, all stamped verb: "reconcile" — one command, one stream. The engine itself is unchanged: ADD/MODIFY auto-apply, ARCHIVE pauses, --apply resumes. Validation failures (bad inputs, unknown source, the fork guard) ride the stream as error + done with exit 2 — never stderr alone.

The pause → answer → resume loop

This is the heart of driving assurance from an agent. A real exchange (events abridged, payloads shortened):
The pause event carries everything needed to decide: the question, why it matters, the options, and the recommendation. Answer in plain words — no question ids, no option indexes. After the resumed run’s usual run_start, corpus, and source_start (with "resumed": true) events, the stream continues:
The agent maps your statement to its own pending questions. A statement that answers nothing pending is treated as steering (“also cover the coupon path”); if it leaves a high-risk ambiguity standing, the run pauses again with refreshed questions. Two structured alternatives to --message (0.7.1):
  • Answer by id — --answer <question-id>=<option number | free text> (repeatable, --resume --mode agent only). Each landed answer echoes as a panel_resolved event before the run continues:
  • Land a source instead of answering — --resume <sid> --with-source <path|url> lands the file or URL first and sets the pending batch aside (ask_deferred on the stream); the agent reads the new source and re-asks only what it didn’t settle. Only refs you provide can land — anything else refuses with INGEST_UNAUTHORIZED_REF.
Between the pause and the resume, everything is inspectable without contending the session:
Abandoned sessions expire after 24 hours; kane-cli context sessions clean garbage-collects them.

Headless review

Trust promotion deliberately has no auto-approve — but it does have a non-interactive path. Prepare verdicts as JSON and land them atomically:
resolution is one of approved | edited | rejected | skipped | supersede (optional reason, edit, supersede_target). One unresolvable ref fails the whole file (exit 2, nothing committed). With --json, each landed verdict echoes as one NDJSON row. Two 0.7.1 additions:
  • Structured verdict flags — for scripted single decisions without a file: --approve <refs...> lands approvals; --skip <refs...> and --defer <refs...> record nothing and leave the items queued. Mutually exclusive with --verdicts.
  • Archives require explicit consent. A headless rejection no longer destroys anything: rejected entries are held as non-destructive pending_archive facts (exit 0, loudly summarized). Destroying them takes --allow-archive plus --because "<reason>" — and under --mode ci, archives are refused under any flag (exit 2).
The rule stands: there is no auto-approve. These paths land your decisions faster; they never make them.

The sync verbs on the stream

kane-cli context sync, kane-cli context push, kane-cli context pull, kane-cli context clone and the subcommands kane-cli context sync add, kane-cli context sync list, kane-cli context sync remove, kane-cli context sync status and kane-cli context sync doctor all take --mode agent and speak the same strict envelope with verb: "sync": stdout is NDJSON only, stderr stays empty, and done is last. None of them calls the agent or spends credits. kane-cli context sync setup is the one command that needs a terminal, and under --mode agent it answers error{code: TTY_REQUIRED} naming kane-cli context sync add and kane-cli context clone as the alternatives. What these commands do is in Sharing the context graph with your team. Exit codes keep their meanings, with one addition: exit 3 is also a person has to decide something, which covers a kane-cli context push or kane-cli context pull refused because you are behind or diverged (the remedy names the command), and a rebase that stopped on open decisions (answer them with --answer, or on a terminal). Exit 2 is a precondition (a location that cannot be reached, a rebase still open, missing keys), and exit 1 a record that cannot be used. A refusal says what stopped, not that nothing happened.

Coverage on the stream (0.7.1)

cover --mode agent and cover gaps --mode agent speak the same envelope (verb: "cover" / "gaps"): the full --json payload arrives as one coverage (or gaps) event — (0.8.2) cover gaps <uc-id> emits the document closed over that use-case — and done closes the stream carrying the worklist’s ready-to-paste commands in next[]. --mode ci speaks the identical stream. Any refusal is an error event + done with exit 2.

When releases don’t match

Sessions bind to the kane-cli release that created them, and the refusals are loud with the remedy in the message: PAIR_MISMATCH at startup (exit 2 — reinstall so the installed pieces match), BINDING_MISMATCH on resume (exit 2 — the session belongs to another release: start fresh, committed work is kept, or resume on the release that created it), and a mid-run “this version of kane-cli is no longer supported — update kane-cli and retry” (a message-only runtime failure, exit 1). Hitting BINDING_MISMATCH on a paused session right after upgrading is expected, not corruption.

Machine-readable reads

These read commands have structured forms: context list --json and context sessions --json (one JSON object per line), context explain --json, context view --json (the full computed graph payload), context view --no-open --out graph.html (render without a browser), cover --json, and cover gaps --json (the nested coverage document — see Coverage).

Headless maintain

  • maintain reconcile --from <file> --source-id <id> --plan — safe preview: records the source change, stages every proposed row into a stored plan, touches nothing else. Exit 0; when the source actually changed, the plan path is the last stdout line (an unchanged source is a no-op that stores nothing).
  • maintain reconcile … --mode override (or --mode ci) — unattended application: ADD and MODIFY rows apply, archiving never happens headless, and ci fail-closes the moment human judgement is needed (the plan is stored; exit 2).
  • Re-running the same reconcile command is idempotent — it resumes a pending plan, reports an applied one, and recomputes a superseded one (details).
  • Bare headless runs without an explicit --mode refuse with exit 2 — by design.

A CI shape that works

Author and batch the resulting tests with the same CI patterns as any other test — see testrun and the CI/CD recipes.

Next steps