kkernel 0.9.0

khive kernel — stdio MCP server binary and admin CLI (sync, pack introspection, db ops)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# kkernel — usage patterns

`kkernel` is the single khive Rust binary. It is both the **admin/management CLI**
(sync, schema migrations, pack/backend introspection, reindex) and the **MCP server**
(`kkernel mcp`). There is no separate `khive-mcp` binary — `khive-mcp` is now a
library crate consumed by `kkernel`.

All subcommands emit JSON on stdout by default; pass `--human` where supported for a
readable table. `--log <level>` (env `KHIVE_LOG`, default `warn`) is global and goes
to stderr — stdout stays clean for JSON / MCP traffic. Stderr is diagnostic-only and
best-effort; disconnecting its consumer does not terminate the stdin/stdout MCP transport.

`kkernel -V` reports the package version; `kkernel --version` also reports the full source
revision (including `-dirty` when applicable) and UTC build time.

```
kkernel <command> [flags]

  sync      Build a working SQLite DB from .khive/kg/*.ndjson sources
  pack      Introspect registered packs (list, handler <name>)
  kg        KG validation, init, hook management
  db        Schema migration lifecycle (migrate, check)
  engine    Embedding model lifecycle (list, status, migrate, drift-check)
  vector    Vector store capabilities and orphan sweep
  reindex   Re-embed entities, notes, and the knowledge corpus (multi-engine)
  exec      Run a verb DSL expression through the pack registry
  mcp       Serve the MCP `request` surface (stdio / daemon / transports)
  backend   Inspect registered backends (list, info <name>)
```

The default database is `~/.khive/khive.db`. Override per-command with `--db`
(or `KHIVE_DB` for `mcp`/`exec`). Use `:memory:` for an ephemeral database.

---

## `kkernel mcp` — serve the MCP request surface

This is the production entrypoint. The deno/npm distribution invokes it:
`khive mcp …` → `kkernel mcp …`, and the `khive-mcp` command alias → `kkernel mcp …`.

```bash
# stdio MCP server (default transport) — what MCP clients spawn
kkernel mcp --db ~/.khive/khive.db

# pick packs explicitly (default loads all 14 production packs)
kkernel mcp --pack kg --pack gtd --pack knowledge

# warm Unix-socket daemon (owns ANN indexes; stdio clients auto-spawn + forward to it)
kkernel mcp --daemon

# ephemeral in-memory server, no embedding (fast tests)
kkernel mcp --db :memory: --no-embed
```

Key flags: `--db`, `--actor`/`--namespace`, `--no-embed`, `--pack` (repeatable),
`--config`, `--daemon`, `--transport <name>`, `--bind <addr>`.

For stdio, `KHIVE_MCP_STDIO_MAX_LINE_BYTES` sets the maximum raw JSON-RPC
line length, including its newline. It defaults to 8,454,144 bytes and accepts
1 through 67,108,864; an invalid value fails startup. An oversized line gets
a JSON-RPC parse error when it crosses the cap, then the reader discards its
remainder and serves the next complete request.

Every successful startup writes the resolved actor to stderr as
`actor: "<id>" (resolved; attributed)` or explicitly marks the unattributed
`local` fallback. This line is emitted at the forced `khive.boot` log target,
so it remains visible under the default `--log warn` setting without touching
the MCP stdout protocol.

### Transports are registerable

`--transport` selects a foreground transport by name from a registry
(`khive_mcp::transport::TransportRegistry`). `stdio` is the only built-in today;
additional transports (e.g. Streamable HTTP) register with `registry.register(...)`
before serving. An unknown name errors with the registered set. `--bind` is reserved
for network transports and is ignored by stdio.

`--daemon` is a deployment mode, not a transport: it runs the warm Unix-socket server
(`~/.khive/khived.sock`) and takes precedence over `--transport`. On first use, stdio
clients auto-spawn `kkernel mcp --daemon` and forward request frames to it; set
`KHIVE_NO_DAEMON=1` to force local dispatch (used by the smoke/contract tests).
Once demand-mode retirement ([ADR-049](../../../docs/adr/ADR-049-khived-daemon.md)
Amendment 11) ships, an automatically started daemon runs no email channel loops and no
schedules; to run either, start the daemon yourself with `kkernel mcp --daemon` or under a
supervisor.

---

## `kkernel exec` — run a verb directly through the registry

Same DSL as the MCP `request` tool, but in-process against a chosen DB and namespace —
ideal for admin verb calls without standing up an MCP client. Defaults to namespace
`local`.

```bash
kkernel exec 'stats()'
kkernel exec 'stats()' --db ~/.khive/khive.db
kkernel exec 'stats()' --config /absolute/path/to/config.toml
kkernel exec '[list(kind="entity", limit=5), stats()]'            # parallel batch
kkernel exec '[create(kind="concept", name="X"), stats()]' --strict  # nonzero exit if any op fails
kkernel exec 'create(kind="entity", entity_kind="concept", name="X") | link(source_id=$prev.id, target_id="<id>", relation="extends")'   # chain ($prev)
kkernel exec 'knowledge.index(help=true)'                         # param schema for any verb
kkernel exec 'knowledge.search(query="...", rerank=true)' --presentation verbose
```

Flags: `--db`, `--config` (env `KHIVE_CONFIG`), `--namespace`, `--actor`, `--expect-actor`,
`--presentation <agent|verbose|human>`, `--strict`.
A request in which every op failed or aborted always exits nonzero after printing the full
response. Without `--strict`, a _partially_ failed request (`status: "partial"` with at least
one success) retains its compatibility behavior and exits zero; `--strict` converts any failed
or aborted op into a nonzero process exit.

### Check grammar without execution

Use `kkernel exec --plan '<ops>'` to parse an operations string and inspect its stages without
executing any verb:

```bash
kkernel exec --plan 'create(kind="note", content="example") | get(id=$prev.id)'
```

This requires an already-running daemon using the same configuration. Start it with
`kkernel mcp --daemon` first, using matching `--db` and `--config` selections when needed.
Planning connects to that daemon directly; it does not start a daemon or construct a local
runtime. An unavailable daemon or a protocol/configuration mismatch is a terminal error.
`KHIVE_NO_DAEMON` does not select local execution for a plan.

The command prints the decoded plan object as JSON. Both `parsed=true` and `parsed=false`
exit successfully: a grammar error appears in the result's `error` field. A plan lists
normalized arguments, literal `$prev` references, stage count and whether each verb is in
the daemon's loaded catalog. It does not establish permission or resolve references.

`--plan` requires positional operations and rejects explicit presentation, output, save,
bulk-file, execution-mode and identity options, including `--presentation verbose` and
`--strict`. The ordinary command's default verbose presentation does not affect plans.
`--db` and `--config` select the matching daemon configuration; no actor or request identity
is sent. Invalid options and transport failures exit nonzero.

### Bulk JSONL execution

`kkernel exec --ops-file batch.jsonl` accepts one independent JSON operation per
non-blank line: `{"tool":"verb","args":{...}}`. It validates the complete
source before runtime construction, with a 96 MiB physical-line limit and a
512 MiB file limit, then dispatches ordered chunks bounded to 100 operations and
32 MiB. One operation larger than the chunk byte target runs alone, still under
the physical-line and total-file ceilings.

Validated chunks use the local typed JSON batch seam. They retain the same
handler, identity, presentation, strict-refusal, audit, ordered-row, and output
format pipeline as ordinary requests, without serializing the decoded values
back through the raw request parser. This separation is intentional: MCP, HTTP,
daemon, and inline `exec` strings retain their independent 1 MiB
`MAX_OPS_INPUT_LEN` safety boundary. The ops-file limits are not a global limit
increase.

The default remains bounded-parallel handler execution. For a backend with a
constrained reader/model resource, add `--serial`:

```bash
kkernel exec --ops-file batch.jsonl --serial --save-file results.jsonl
```

Serial mode still validates and freezes the complete source before runtime
construction or the first write. It then keeps the same single in-process
server, loaded models, 100-op/32 MiB logical chunk boundaries, ordered rows,
save-file publication, strict handling, progress, and reconciliation semantics.
Each complete logical chunk still receives one shared write-conflict preflight
and aggregate response budget; only its trusted handler-concurrency cap changes
from eight to one. `$prev` remains invalid because an ops-file contains
independent JSON operations, not a DSL chain. `--serial` requires `--ops-file`
and conflicts with positional inline ops and `--atomic`; the latter is a
separate whole-file transaction contract.

Serial scheduling does not renew Khive's established request-read deadline per
operation: one logical chunk retains one deadline. A trusted long-running local
model batch whose inference can exceed the 30-second default must opt into a
bounded documented override (1–3600 seconds), for example:

```bash
KHIVE_REQUEST_READ_TIMEOUT_SECS=3600 \
  kkernel exec --ops-file image-batch.jsonl --serial
```

Record this operator setting with run evidence; it is not a wire-limit bypass.

### Stable refusal reasons

Refused invocations retain their existing human-readable error and exit-code
semantics, and additionally write `kkernel-refusal: <token>` to stderr. Failed
per-operation JSON entries carry the same token in a sibling `reason` field.
The following vocabulary is closed and append-only: new classifications may be
added, but an existing token's spelling and meaning never change.

| Token                   | Meaning                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `anonymous-actor`       | The resolved actor is anonymous while attributed execution is required.              |
| `expect-actor-mismatch` | The resolved actor differs from `--expect-actor`.                                    |
| `gate-refusal`          | Content was refused by the write-time secret gate.                                   |
| `strict-op-failure`     | `--strict` observed at least one otherwise-unclassified failed or aborted operation. |
| `parse-error`           | The operation expression or JSONL operation failed to parse.                         |
| `verb-refused`          | The requested verb is unknown or is not loaded.                                      |
| `policy-refusal`        | A write was refused by the immutable stream record policy.                           |

For a multi-failure batch, stderr contains one line per classified failed or
aborted operation. A specific dispatch reason (`gate-refusal`, `verb-refused`, or
`policy-refusal`) takes precedence over the aggregate `strict-op-failure` reason
for that entry. An invocation-level actor refusal emits one line and returns
the same normal `results`/`summary` JSON shape over every parsed operation
without dispatching. Those failed rows and the `summary.failed` count describe
not-attempted operations, not per-operation execution failures. A malformed
input expression or source JSONL line has no operation list, so it preserves
the parse-before-envelope boundary and prints a dedicated invocation error
instead of inventing a tool:

```json
{
  "error": {
    "code": "invalid_params",
    "message": "<unchanged parser error>",
    "reason": "parse-error"
  },
  "invocation": { "started": false }
}
```

Carrier parsing precedes actor expectation, attributed-actor gates, and the
explicit `--db` versus multi-backend `database_override_conflict` check. If an
invocation is malformed and would also fail any of those later preflights,
`parse-error` is therefore the deterministic first classification for inline
DSL and JSONL alike.

For a completed, published `--strict --save-file` result, classification happens
before the save sink writes or hashes rows. The stdout manifest's
`failures[].reason` is consequently an exact projection of the corresponding
reason in the checksummed JSONL row.
The non-atomic ops-file path without `--save-file` retains its pre-existing
aggregate summary shape, so its compatibility `failures` objects do not gain a
`reason` field; stable classifications still appear on stderr. Use
`--save-file` when automation needs reasons correlated with durable rows.

Combined `--ops-file --save-file` has two separate commit boundaries. The
destination file is published by one atomic rename, while non-atomic database
chunks commit incrementally. After dispatch starts, success emits the ordinary
manifest unchanged. Any later error that stops the run before the manifest is
finalized, including malformed JSON or a structurally self-contradictory
response envelope, emits an aborted manifest before the non-zero exit. The
aborted manifest's `committed_chunks` are the structurally confirmed prefix;
its `dispatched_chunk`, when present, is unverified and may still have database
effects. Its `summary` covers confirmed rows, while `unconfirmed_ops` accounts
for the remainder without calling them aborted. Its `file_published=false`
means the incomplete temp JSONL was discarded and any previous destination was
preserved. A `--strict` failure or an all-failed file is a policy exit taken
after the ordinary manifest has already been published, so those runs keep the
ordinary manifest, carry none of the aborted-only fields above, and exit
non-zero without an aborted one.

Atomic ops-file preflight and prepare failures use the real per-operation
`results` shape: an unknown or unloaded verb receives `verb-refused`, while a
known verb that is merely ineligible for `--atomic` remains unclassified.
Secret-gate failures during atomic prepare receive `gate-refusal`. Immutable
stream record refusals receive `policy-refusal` during ordinary dispatch and
atomic prepare. These guards refuse before the domain write and report
`domain_disposition: "not_committed"`: inside the structured `error` for ordinary
dispatch, or beside the string `error` on the atomic result entry. This
classification does not cover ordinary sequence precondition conflicts or raw
SQL trigger failures. An atomic
rollback passed with `--strict` receives `strict-op-failure` on each
not-committed result without changing the atomic path's existing process-exit
semantics. Ordinary validation, transport, storage, and authorization-gate
failures remain unclassified unless `--strict` supplies the aggregate reason.

After `atomic.committed=true`, the write must not be replayed. If deferred
reindexing or canonical result rendering then fails, the command exits
successfully with `atomic.status="committed_degraded"`,
`atomic.retryable=false`, and one or more typed `atomic.degradations` entries.
Treat `post_commit_reindex` as an index-repair requirement and
`result_rendering` as a result re-read requirement; neither means the mutation
failed. A render-degraded result remains `ok=true` with `result=null` and its
own non-retryable degradation marker.

When `--atomic` and `--save-file` are combined, a successful stdout manifest
preserves that complete top-level `atomic` block. If JSONL write, flush, or
final publication fails after commit, stdout is instead the full atomic
envelope with `stage="save_file_publish"`, `committed=true`, and
`retryable=false`; the process then exits non-zero because the requested file
was not published. Reconcile from stdout and do not replay the durable unit.

When an explicit `--db` conflicts with a selected multi-backend config,
dispatch does not begin. The command emits
`error.code = "database_override_conflict"` with
`invocation.started = false` in a JSON envelope on stdout, retains the
actionable prose on stderr, and exits nonzero.

`--actor` is a highest-precedence identity pin: it overrides project config and
`KHIVE_ACTOR` for this invocation without changing the storage namespace, and
the configured authorization gate still checks the selected identity.
`--expect-actor` makes scripts fail before dispatch when identity resolution
produces anything else. The two flags compose:

```bash
kkernel exec 'create(kind="concept", name="X")' \
  --actor lambda:worker --expect-actor lambda:worker
kkernel exec 'stats()' --expect-actor lambda:worker  # validate config/env resolution
```

Before a successful dispatch, `exec` also writes the resolved actor line to
stderr. This is symmetrical for attributed and `local` identities and leaves
the JSON result on stdout unchanged.

---

## Reindex — `kkernel reindex`

`kkernel reindex` re-embeds **entities, notes, and the knowledge corpus** in one
pass (namespace-scoped — run once per namespace your data spans). Progress prints
to stderr; the JSON/`--human` report goes to stdout.

```bash
kkernel reindex --db ~/.khive/khive.db --namespace local   # entities + notes + knowledge
kkernel reindex --db ~/.khive/khive.db --namespace khive
kkernel reindex --db ~/.khive/khive.db --sections-only      # backfill only section embeddings
```

| Flag               | Effect                                                                              |
| ------------------ | ----------------------------------------------------------------------------------- |
| `--db <path>`      | database (env `KHIVE_DB`); with `[[backends]]`, must match one declared SQLite path |
| `--config <path>`  | khive TOML config (env `KHIVE_CONFIG`) — resolves engines like `kkernel mcp`        |
| `--knowledge-only` | only the knowledge corpus (skip entities/notes)                                     |
| `--no-knowledge`   | only entities/notes (skip knowledge)                                                |
| `--no-sections`    | within the knowledge pass, embed atoms but skip section embeddings (ADR-051)        |
| `--sections-only`  | embed only knowledge sections (skip entities/notes and atoms)                       |
| `--model <name>`   | entities/notes use this single engine instead of fanning out                        |
| `--keep-existing`  | skip records already embedded (incremental top-up) instead of replacing them        |
| `--batch-size <n>` | records per embedding batch (default 128, max 500)                                  |
| `--best-effort`    | downgrade partial failures to a warning and still exit 0 (default fails closed)     |
| `--rebuild-fts`    | rebuild + rank-1 integrity-check both global knowledge FTS indexes (see below)      |
| `--human`          | readable report instead of JSON                                                     |

There is no `--embeds-only`, `--ids`, or `--dry-run` mode. `--keep-existing` narrows
vector work to missing records, but the selected graph pass still backfills FTS.

**Config resolution.** Engines, db path, and config file are resolved with the
**same precedence as `kkernel mcp`** — config-file `[[engines]]` (via `--config`
/ `KHIVE_CONFIG` / `./khive.toml` / `./.khive/config.toml` / `~/.khive/config.toml`)
win over the `KHIVE_EMBEDDING_MODEL` env vars and over `RuntimeConfig` defaults.
This guarantees reindex writes vectors for the SAME engine set the MCP server
serves recall from. `--namespace` is the explicit per-namespace target and
always wins over any config `[actor] id`.

When the selected config declares `[[backends]]`, reindex remains a
one-database command: `--db` / `KHIVE_DB` is required and must match one of the
declared SQLite backend paths (including a secondary backend). An omitted,
`:memory:`, or undeclared path is refused before any database is opened. The
canonical path and filesystem identity (device + inode) observed at
validation time are what reindex actually opens, re-checked immediately
before open: a symlink retargeted, or the declared file replaced in place,
after validation is refused rather than silently followed.

**Fail-closed.** By default reindex returns a **non-zero exit** if any requested
engine failed, the knowledge pass errored, any knowledge atom vector insert
failed, the Vamana ANN build/snapshot persist failed, or any knowledge section
embed/write failed — a partial rebuild leaves stale recall/search state, so
automation must not see success. Pass `--best-effort` to downgrade failures to a
warning and exit 0. The report (JSON and `--human`) always reports
attempted/indexed/failed counts honestly (`errors_skipped`,
`knowledge_atoms_failed`, `knowledge_pass_errored`, `knowledge_ann_failed`,
`knowledge_sections_failed`). Note: `knowledge_ann_failed` and
`knowledge_sections_failed` are distinct failure dimensions from
`knowledge_atoms_failed` — atom vectors may have persisted successfully while the
ANN rebuild/snapshot persist or the section embed/write failed.

**Multi-engine semantics.** Entities and notes embed with **every registered
engine** (e.g. `all-minilm-l6-v2` + `paraphrase-multilingual-minilm-l12-v2`),
one vector record per engine — matching the runtime's create/update write path.
`--model` narrows to a single engine. **Knowledge is single-model**: knowledge
search retrieves via the default embedder's ANN, so the knowledge pass always
uses the default embedder (fanning out would write vectors search never reads).

The knowledge pass calls the `khive_pack_knowledge::reindex_knowledge` library
entry directly (the full-corpus `knowledge.index` handler) and rebuilds the
Vamana ANN snapshot — no verb-DSL shell required.

**FTS rebuild is opt-in.** `fts_knowledge` and `fts_sections` are global
tables, not scoped to `--namespace`, so rebuilding them is whole-database
work (and writer contention) whatever the run's scope. No run shape implies
the rebuild: it is off unless `--rebuild-fts` is passed, and passing it
rebuilds both indexes for any scope. The report carries a
`knowledge_fts_rebuild` object (index names, elapsed milliseconds,
integrity-check outcome) whenever the rebuild ran, and omits it otherwise.

```bash
kkernel reindex --db ~/.khive/khive.db --knowledge-only      # just the corpus
kkernel reindex --db ~/.khive/khive.db --no-knowledge        # just graph substrate
```

For ad-hoc / scoped knowledge indexing (specific atoms, no ANN rebuild) the
low-level verb is still available via `exec`:

```bash
kkernel exec 'knowledge.index(ids=["my-slug", "<uuid>"])' --db ~/.khive/khive.db
```

`knowledge.index` does not accept `rebuild_fts` — rebuilding the global FTS
indexes has no per-caller cost admission on the ordinary verb surface, so it
is reachable only through `kkernel reindex --rebuild-fts` above.

> Stop the MCP daemon before a large reindex to avoid SQLite write contention:
> `pkill -f 'kkernel.*--daemon'` (or `KHIVE_NO_DAEMON=1`), then reindex, then let
> the next stdio client re-spawn the daemon.

---

## `kkernel db` — schema lifecycle

```bash
kkernel db check --config ~/.khive/khive.toml --human # report configured topology
kkernel db check --backend archive --strict           # inspect one configured backend
kkernel db migrate --config ~/.khive/khive.toml       # secondaries, then main
kkernel db migrate --backend archive                  # migrate one named secondary
kkernel db migrate --dry-run                          # read-only topology check
```

The consolidated baseline is a single migration (V1, from `khive-db/sql/schema.sql`).
Both commands load an explicit `--config`/`KHIVE_CONFIG` or use normal khive
config discovery. With declared `[[backends]]`, omitting `--backend` targets the full
configured set and `--backend <name>` is an active selector. Migration
deduplicates physical aliases and prepares every distinct secondary before
canonical `main`; selecting `main` keeps those prerequisites, while selecting a
non-main secondary advances only that empty attachment-authority target. With no
declared topology, the implicit target is `main` at `--db`/`KHIVE_DB` or the
default `~/.khive/khive.db`. A concrete DB override may not conflict with a
declared main path.

The database admin path is core-only: it does not register packs, instantiate
embedding models, or apply pack-auxiliary DDL. It still resolves the configured
BlobStore and bounded hydrator when legacy V20 moodboard evidence must be
authenticated. `db check` and migrate's `--dry-run`/`--check` modes never open a
runtime or mutate the database; absent files report V0. They reuse migration's
target planner, so selecting `main` or its SQLite alias includes every secondary
prerequisite, while an independent secondary remains a single target.

Before either command opens a database, the planner rejects physical SQLite
aliases that disagree on `read_only`; a named target cannot bypass that topology
check. `--db :memory:` changes every configured name into a distinct ephemeral
backend, so only the literal `main` name carries main's prerequisite plan in that
mode. Migration output marks `prerequisite` by physical backend identity: every
alias of the selected main is a target, not a prerequisite.

A legacy V20 database with blob-bearing entities uses the same boot-gated V21
coordinator as MCP startup: it backfills role-keyed attachments, verifies any
legacy moodboard bundle/event/FANN evidence through bounded hydration, switches
GC liveness and claim fences, and only then drops `entities.content_ref`.
`kkernel db migrate` waits for that cutover and fails without exposing an
incomplete runtime when evidence or blob storage is unavailable.
A Phase-4b binary must not be allowed to start this cutover until the Phase-4a
GC compatibility build has converged across the fleet and every pre-Phase-4a
process sharing the database/blob root has been drained. Every Phase-4a
application-serving/read-write process must then be quiesced or proven unable
to touch the database for cutover; only a GC-only worker has narrow
compatibility with completed V21. Start the Phase-4b serving fleet only after
`db check --strict` reports the planned topology exact-current. See
`docs/operations.md` for the two-release runbook.
A database whose `_schema_migrations` version is **ahead** of the latest known
migration is rejected at open time — it predates the consolidation or was written by a
newer build. Recreate it from the current schema; in-place downgrade is unsupported.

---

## `kkernel sync` — build a DB from NDJSON sources

```bash
kkernel sync --repo . --db ~/.khive/working.db --namespace local
```

Reads `.khive/kg/{entities,edges}.ndjson`, builds a queryable SQLite DB in a
unique sibling file, and renames it over the target after checkpointing. Close
all SQLite clients using the target first. Sync refuses existing `-wal` or
`-shm` sidecars and serializes concurrent sync calls with a sibling lock file.
Errors before the rename leave the previous database intact. Consumed by the
deno CLI's `khive kg sync`.

---

## Introspection

```bash
kkernel pack list --human                 # all packs: verbs, note kinds, entity kinds
kkernel pack handler knowledge --human     # full handler surface for one pack
kkernel backend list --human               # registered backends
kkernel backend info main --human
kkernel engine list                        # embedding engines + model history
kkernel engine status                      # active model + migration status
kkernel vector --help                      # vector store capabilities, orphan sweep
kkernel kg --help                          # KG validation, init, pre-commit hook
```

---

## Distribution model

`kkernel` is the only published binary. The npm package `khive` ships per-platform
`@khive-ai/kernel-<platform>` subpackages that each contain `bin/kkernel`. Two command
names route to it:

- `khive <cmd>` → `kkernel <cmd>` (and `khive mcp` → `kkernel mcp`)
- `khive-mcp [args]` → `kkernel mcp [args]` (compat alias for existing MCP configs)

Binary resolution order (npm shims and `cli/lib/kernel.ts` agree): `KKERNEL_BINARY`
env override → `@khive-ai/kernel-<platform>/bin/kkernel` → monorepo
`crates/target/{release,debug}/kkernel`.

### Local development

```bash
make local          # build release kkernel, kill stale procs, codesign, install to ~/.cargo/bin
make ci             # full gate (fmt, clippy -D warnings, tests, contract + smoke)
```

After `make local`, run `/mcp` in Claude Code to reconnect to the rebuilt server.

### Tool-source catalog management

`kkernel mount repin <name> [--config <path>] [--db <path>]` refreshes an
operator-configured stdio source and atomically replaces its pinned catalog with one
audit record. Calls use ordinary `kkernel exec '<mount>.<tool>(...)'` dispatch.
See [mounted tool sources](../../../docs/packs/mounts.md) for configuration and lifecycle.