Arising from dec-0004 — Execution status is derived from kazi at read time, never stored in the ledger
dira integrates with kazi only through kazi's existing --json contract and hooks
Verified against kazi at `docs/adr/`, `lib/kazi/cli.ex`, and `lib/kazi/cli/schema.ex` on 2026-07-29. Three seams, in order of how much upstream work each needs.
**1. Status join — works today, undocumented.** `kazi portfolio --json` is implemented (`cli.ex:1254`, `portfolio_json/1` at `cli.ex:4249`) and emits `schema_version`, `kind: "portfolio"`, `planned`, `by_repo` (repo → bucket → entries), `fleet_remote`, `totals` (`{base, empty, rows: [{bucket, count, pct}]}`), `todo`, `blocked` (each carrying `cause` and a `blocker` label), and `rate`.
**Correction, 2026-07-30.** An earlier version of this entry claimed the bucket enum is "exactly `done | running | blocked | todo | planned`, which maps cleanly onto dec-0004's join table." That was wrong in two ways, found by re-reading the source:
1. **There are two enums.** `portfolio.ex:38` defines `@type bucket :: :in_progress | :stuck | :complete`, and `by_repo` is typed `%{String.t() => %{bucket() => [map()]}}` — the three-value enum. The five-value `five_bucket` governs only `totals.rows[]` and the top-level `todo`/`blocked` arrays. 2. **`done` and `running` are never emitted as arrays.** `portfolio_json/1` emits `buckets.todo` and `buckets.blocked` only; done and running survive as *counts* inside `totals.rows[]`. So portfolio can say how many goals converged, never which.
Two of dec-0004's six buckets are therefore not derivable from portfolio alone. The per-goal read must come from **`kazi status <ref> --json`**, which is registered in `Kazi.CLI.Schema` (`schema.ex:345`) and returns `observed_at` / `converged` / `release_ref` at roughly 0.65s per call. Blocked *attribution* (`cause`, `blocker`) exists only on portfolio. So the join is a **hybrid** — portfolio for attribution and totals, per-ref status for the running/done split — not the single call originally assumed.
The catch: `portfolio` is **not registered in `Kazi.CLI.Schema`**, which documents only `apply`, `plan`, `bus`, and `status`. So the shape is versioned but not discoverable, and nothing in kazi's test suite pins it as a public contract. dira should depend on it *and* ask upstream to register it — that is issue 1 of 2.
**2. Rationale linkage — works today, both directions, no upstream change.** `kazi plan --adr --json` exists (`cli.ex:620`, ADR-0019) and mints the proposal ref that `approve --json` flips (ADR-0049). Proposal refs look like `prop-url-shortener-3f9c1a2b` and are stable, so `realized_by: kazi:prop-…` is a durable edge upward.
The downward link needs **no new field**. Goal-files are TOML and already carry a free-form `metadata` table — documented at `lib/kazi/goal/loader.ex:24` as "table, optional, `Goal.metadata` (string-keyed map, **verbatim**)", read via `Map.get(data, "metadata", %{})` (`loader.ex:640`) and stored with no validation. So dira writes:
```toml [metadata] why = "dira:dec-0060" ```
and the loop closes today. (The loader also ignores unknown *top-level* keys outright — it only `Map.get`s the keys it knows, so a bare `why =` would not error either. Every "unknown X is a validation error" in that file concerns the *value* of a known key — mode, provider, harness id, group ref — not the presence of an extra one. `metadata` is nonetheless the intended and documented home, so use it.)
**3. Disposition capture — needs upstream.** `kazi approve <ref>` and `kazi reject <ref>` are the single front door for accept/reject decisions, and subscribing to them is the highest-leverage capture point in the whole design: every approval is already a decision event, so logging it costs the user zero new ceremony. But kazi emits no post-disposition hook today. Until it does, dira's skill wraps the commands instead — correct behaviour, worse ergonomics. That is issue 2, and it is the **only** seam that genuinely needs kazi to change.
It must be a **direct synchronous hook, not the session bus.** ADR-0067's bus is hosted by `kazi daemon` and no-ops when that daemon is down, so disposition events riding it would be silently lost — disqualifying for capture, and a daemon dependency dira cannot take (int-0002, cst-0004). See qst-0005 for the full reasoning.
**Hook installation has a proven precedent to mirror rather than invent.** `kazi install-hooks` (`cli.ex:440`, ADR-0071/ADR-0076) already registers hooks into Claude Code's settings: merge-never-clobber, idempotent, an operator's own keys survive byte-identically, `--uninstall` removes exactly what was added, and `--local` targets the repo's uncommitted `.claude/settings.local.json` instead of the user-level file. `dira install-hooks` should match that contract exactly — the failure mode it avoids (clobbering a hand-tuned settings file) is one dira would otherwise rediscover.
The boundary itself is quoted from kazi's own README — it will never "decide what to build — that's your judgment." That sentence is dira's charter: kazi is everything downstream of a declared goal; dira is everything upstream of declaration.
Alternatives on record — 3
✗Read kazi's internal state directly — its database, run files, or proposal storerefused It would couple dira to kazi's internals, so every kazi refactor silently breaks dira, and it would make dira unusable against a kazi installed from Homebrew rather than a source checkout.
The `--json` surface exists precisely to be depended on.
revisit ifnever
✗Vendor kazi as a library and call it in-processrefused Impossible across the language boundary (dec-0001), and it would make kazi a hard dependency — breaking the requirement that dira degrade gracefully when kazi is absent (dec-0004).
Impossible across the language boundary (dec-0001), and it would make kazi a hard dependency — breaking the requirement that dira degrade gracefully when kazi is absent (dec-0004).
✗Have kazi call dira instead, inverting the dependencyrefused It would make the why-layer a required part of kazi's execution path, which is wrong on both sides: kazi must stay usable with no dira installed, and dira must not be able to break a convergence run.
It would make the why-layer a required part of kazi's execution path, which is wrong on both sides: kazi must stay usable with no dira installed, and dira must not be able to break a convergence run.