JSON envelope contract
ADR-0029 Phase 4b. Per-handler migration is rolling — opt in to the envelope by passing --envelope to any command that supports it. Plain --json (where present) keeps emitting the legacy shape.
Shape
Single JSON object with four fixed top-level keys:
{
"schema_version": "1.0",
"command": "status",
"data": { /* command-specific payload */ },
"errors": []
}
schema_version— bumps on incompatible changes. Always present.command— the subcommand name (status,recall,clusters, etc.). Lets a single pipeline branch on different kannaka invocations.data— the command's actual payload. Shape is command-specific but stable within aschema_version. May benullon error.errors— array of error strings. Empty on success. On non-empty, the process also exits non-zero.
Success predicate
output="$(kannaka status --envelope)"
errors=$(jq -r '.errors | length' <<<"$output")
if [ "$errors" -eq 0 ]; then
phi=$(jq -r '.data.phi' <<<"$output")
echo "phi=$phi"
fi
NDJSON variant
Streaming commands (kannaka swarm tail, kannaka chat --json) use per-line JSON without the outer data wrap — each line is already its own envelope-like object:
{"ts": 1779550267784, "subject": "QUEEN.phase.Kannaka", "payload": {...}}
{"ts": 1779550271462, "subject": "QUEEN.phase.OxSCADA-QE", "payload": {...}}
The reason for the difference: NDJSON consumers parse one line at a time and expect each line to be a complete record. Wrapping every line in {schema_version, data: {...}, errors: []} would double the payload size without delivering new semantics.
Migration status
| command | --envelope support |
|---|---|
status | ✓ v0.6.3 |
clusters | ✓ v0.6.3 |
recall | ✓ v0.6.4 |
search | pending |
observe | pending |
neighbors | pending |
assess | pending |
stats | pending |
dream | pending |
swarm tail | NDJSON variant (already envelope-like) |
chat --json | NDJSON variant (already envelope-like) |
Pending handlers continue to emit their legacy shapes. Migrate one handler per patch release; downstream consumers (radio, observatory, TUI) adopt the new shape at their own pace.
Why opt-in instead of a flag day
Every downstream consumer of kannaka output today parses the legacy shapes. A hard cut to --envelope would break:
kannaka-radio— shells out fornow-playingcluster infokannaka-observatory—/api/hrm/statusproxies the raw outputkannaka-tui— shells out for status / observe / clusters every tick
The opt-in pattern lets each consumer migrate independently:
- Consumer adds
--envelopeto its kannaka invocation - Consumer updates its parser to read
.data.Xinstead of.X - Once every consumer is migrated, a future release can flip the default (
--no-envelopeto opt OUT of the new shape, instead of--envelopeto opt IN). That's a v0.7.0 conversation.