@kal-elsam/kairo-runtime 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.29.0 — 2026-09-19 (Kairo Runtime)
9
+
10
+ Minor release. PROJECT TEAM selections explain themselves.
11
+
12
+ ### Changed
13
+
14
+ - Every PROJECT TEAM row now has a non-empty explanation — ordinary
15
+ leaders no longer render blank (`explainTeamDecision` from real
16
+ `decisionEvidence`, never invented metrics).
17
+ - Compact panel and `/project` overlay show the quality leader and real
18
+ retention% when the operational (efficient) pick differs; evidence
19
+ toggle (`e`) reuses `teamEvidenceLines`.
20
+ - Honest subtitle: operational picks are the efficient model among
21
+ eligible candidates. Orchestrator* footnote: Architect's quality pick
22
+ — Kairo has no separate orchestrator capability profile yet.
23
+ - Availability warnings for denied/unverified Claude entitlement.
24
+
8
25
  ## 0.28.0 — 2026-09-19 (Kairo Runtime)
9
26
 
10
27
  Minor release. `/models --verify-access` verifies Claude model entitlement on demand.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
@@ -1,6 +1,7 @@
1
1
  import { Box, SelectList, Text, Input, Key, matchesKey, fuzzyFilter } from "@earendil-works/pi-tui";
2
2
  import { editorTheme, theme } from "./theme.js";
3
3
  import { CARD_TONE, cardInnerWidth, renderPanel } from "./card.js";
4
+ import { explainTeamDecision } from "./view.js";
4
5
 
5
6
  // /project's interactive overlay — the real preflight -> select analyst ->
6
7
  // confirm -> analyze -> result -> approve loop, reusing the exact same
@@ -109,6 +110,7 @@ export class ProjectOverlay {
109
110
  this.editInput = null;
110
111
  this.editSelectList = null;
111
112
  this.pendingEditCandidate = null;
113
+ this.showTeamEvidence = false;
112
114
 
113
115
  const existing = view.snapshot?.projectStrategy ?? null;
114
116
  if (existing?.status === "active") {
@@ -296,16 +298,9 @@ export class ProjectOverlay {
296
298
  const overrideNote = entry.assignmentSource === "override" ? theme.fg("accent", " (override)") : "";
297
299
  // Role + model are the real primary information here — explicit
298
300
  // `text` color, never left to default/muted.
299
- // WHY this model was picked — the real, human-readable evidence
300
- // buildProjectStrategy already carries (entry.reason, sourced from
301
- // efficientTeam's own describeEfficiencyDecision), never a
302
- // fabricated justification. An override has no ranking reason of
303
- // its own (see applyProjectTeamOverride) — honestly say so instead
304
- // of silently reusing the old recommendation's reason for a
305
- // different model.
306
- const description = entry.assignmentSource === "override"
307
- ? "Manual override — not the automatic ranking's own pick."
308
- : (entry.reason ?? "");
301
+ // WHY this model was picked — explainTeamDecision (real reason,
302
+ // leader formulation, or override text), never an empty row.
303
+ const description = explainTeamDecision(entry);
309
304
  return { value: entry.role, label: theme.fg("text", `${entry.role.padEnd(10)} ${modelText}`) + overrideNote, description };
310
305
  });
311
306
  this.resultSelectList = new SelectList(items, 6, editorTheme.selectList);
@@ -484,6 +479,13 @@ export class ProjectOverlay {
484
479
  // scratch) — distinct from editing one role's model (Enter) or
485
480
  // approving the current suggestion (a) — see reanalyze()'s own doc.
486
481
  if (data === "r" || data === "R") return void this.reanalyze();
482
+ // "e" toggles the technical evidence section (kept off by default
483
+ // so the modal stays short).
484
+ if (data === "e" || data === "E") {
485
+ this.showTeamEvidence = !this.showTeamEvidence;
486
+ this.requestRender();
487
+ return;
488
+ }
487
489
  this.resultSelectList.handleInput(data);
488
490
  this.requestRender();
489
491
  return;
@@ -616,16 +618,25 @@ export class ProjectOverlay {
616
618
  case S.RESULT: {
617
619
  const strategy = this.suggestedStrategy;
618
620
  push(theme.bold("Suggested Project Team"));
621
+ push(theme.fg("muted", "Operational picks: the efficient model among eligible candidates for each role (quality leader shown when it differs)."));
619
622
  const choiceNote = strategy.bootstrapAnalystChoice ?? (strategy.bootstrapAnalystSelectionSource === "manual" ? "manual pick" : "recommended");
620
623
  push(theme.fg("muted", `Project Analyst: ${choiceNote} — ${this.view.aiTeamLabelWithProvider(strategy.bootstrapAnalyst)}`));
621
624
  push(theme.bold("PROJECT TEAM"));
622
625
  box.addChild(this.resultSelectList);
626
+ if (this.showTeamEvidence && typeof this.view.projectTeamEvidenceLines === "function") {
627
+ const eligibility = this.view.snapshot?.modelIntelligence?.eligibility ?? {};
628
+ const claudeEntitlement = this.view.snapshot?.modelIntelligence?.claudeEntitlement ?? {};
629
+ push(theme.fg("muted", "Evidence"));
630
+ for (const line of this.view.projectTeamEvidenceLines(strategy, { eligibility, claudeEntitlement })) {
631
+ push(line);
632
+ }
633
+ }
623
634
  // Quality/Efficient stay real, comparative REFERENCE — muted, and
624
635
  // rendered strictly below the real operational PROJECT TEAM list
625
636
  // above, never replacing it visually.
626
637
  for (const line of teamLines("Quality (reference)", strategy.qualityTeam, this.view, "muted")) push(line);
627
638
  for (const line of teamLines("Efficient (reference)", strategy.efficientTeam, this.view, "muted")) push(line);
628
- push(theme.fg("muted", "Enter edit role · a approve & activate · r re-analyze from scratch · Esc close without approving"));
639
+ push(theme.fg("muted", "Enter edit role · a approve & activate · e evidence · r re-analyze from scratch · Esc close without approving"));
629
640
  break;
630
641
  }
631
642
  case S.EDIT_LOADING:
@@ -3,6 +3,85 @@ import { buildTaskRows, clampSelection, isActionAvailable } from "./rows.js";
3
3
  import { CARD_TONE, cardBottom, cardInnerWidth, cardLine, cardTop, renderPanel as renderPanelWithTheme } from "./card.js";
4
4
  import { theme } from "./theme.js";
5
5
  import { LOW_QUOTA_WARN_PERCENT } from "../intelligence/execution-router.js";
6
+ import { ENTITLEMENT } from "../observability/claude-model-entitlement.js";
7
+
8
+ /** Plain-language description of what each role optimizes for — mirrors
9
+ * buildAiTeamRoleDefinitions()'s real compute functions in
10
+ * model-intelligence.js, never a per-model claim, so it never needs
11
+ * updating when the underlying models change. */
12
+ export const ROLE_CAPABILITY_BLURB = {
13
+ Explorer: "general reasoning capability",
14
+ Architect: "general reasoning capability",
15
+ Builder: "coding capability",
16
+ Debugger: "reasoning and terminal-debugging capability",
17
+ Tester: "coding and terminal-execution capability",
18
+ Reviewer: "independent reasoning and coding review"
19
+ };
20
+
21
+ const OVERRIDE_DECISION_TEXT = "Manual override — not the automatic ranking's own pick.";
22
+
23
+ /**
24
+ * Pure, human-readable why for one team assignment — from real
25
+ * `reason` / `decisionEvidence` only, never invented metrics.
26
+ * Existing `entry.reason` always wins; overrides use a single formulation.
27
+ * @param {{role?: string, reason?: string|null, assignmentSource?: string|null, decisionEvidence?: object|null}} entry
28
+ * @returns {string}
29
+ */
30
+ export function explainTeamDecision(entry) {
31
+ if (!entry) return `Selected for ${ROLE_CAPABILITY_BLURB.Explorer ?? "this role's capability requirement"}.`;
32
+ if (entry.assignmentSource === "override") return OVERRIDE_DECISION_TEXT;
33
+ if (entry.reason) return entry.reason;
34
+
35
+ const blurb = ROLE_CAPABILITY_BLURB[entry.role] ?? "this role's capability requirement";
36
+ const decisionType = entry.decisionEvidence?.decisionType ?? null;
37
+ if (decisionType === "leader") {
38
+ const floor = entry.decisionEvidence?.requiredFloor;
39
+ const risk = entry.decisionEvidence?.riskLevel;
40
+ if (floor != null && risk != null) {
41
+ const floorPct = Math.round(floor * 100);
42
+ return `Ranked first for ${blurb} among eligible candidates — nothing cheaper or faster displaced it at the ${floorPct}% capability floor (${risk}-risk role).`;
43
+ }
44
+ return `Ranked first for ${blurb} among eligible candidates.`;
45
+ }
46
+ return `Selected for ${blurb}.`;
47
+ }
48
+
49
+ /**
50
+ * Live availability for a projectTeam model ref (which has no
51
+ * `available` flag). Entitlement beats adapter quota/eligibility.
52
+ * @param {object|null|undefined} model
53
+ * @param {{eligibility?: Record<string, {ok: boolean, reason?: string}>, claudeEntitlement?: Record<string, {status: string, reason?: string|null}>}} [opts]
54
+ * @returns {{available: boolean, warning: string|null}}
55
+ */
56
+ export function resolveAssignmentAvailability(model, { eligibility = {}, claudeEntitlement = {} } = {}) {
57
+ if (!model) return { available: false, warning: null };
58
+
59
+ if (model.adapterId === "claude") {
60
+ const entitlement = claudeEntitlement[model.modelId];
61
+ if (entitlement?.status === ENTITLEMENT.DENIED) {
62
+ const reason = entitlement.reason ?? "denied";
63
+ return {
64
+ available: false,
65
+ warning: `Unavailable — your Claude plan denies this model (${reason})`
66
+ };
67
+ }
68
+ if (entitlement?.status === ENTITLEMENT.UNVERIFIED) {
69
+ return {
70
+ available: false,
71
+ warning: "Unavailable — model entitlement not verified (run /models --verify-access)"
72
+ };
73
+ }
74
+ }
75
+
76
+ const check = eligibility[model.adapterId];
77
+ if (check && check.ok === false) {
78
+ return {
79
+ available: false,
80
+ warning: `Unavailable — ${check.reason ?? "not eligible"}`
81
+ };
82
+ }
83
+ return { available: true, warning: null };
84
+ }
6
85
 
7
86
  /**
8
87
  * A real, early heads-up — never fabricated, never re-deriving its own
@@ -580,8 +659,12 @@ export class CockpitView {
580
659
  const modelColumnWidth = this.teamModelColumnWidth([
581
660
  strategy.bootstrapAnalyst, strategy.orchestrator, ...projectTeam.map((entry) => entry.model)
582
661
  ]);
662
+ lines.push(theme.fg("muted", "Operational picks: the efficient model among eligible candidates for each role (quality leader shown when it differs)."));
583
663
  if (strategy.bootstrapAnalyst) lines.push(`${"Project Analyst".padEnd(18)} ${this.teamRoleLabel(strategy.bootstrapAnalyst, modelColumnWidth)}`);
584
- if (strategy.orchestrator) lines.push(`${"Orchestrator".padEnd(18)} ${this.teamRoleLabel(strategy.orchestrator, modelColumnWidth)}`);
664
+ if (strategy.orchestrator) {
665
+ lines.push(`${"Orchestrator*".padEnd(18)} ${this.teamRoleLabel(strategy.orchestrator, modelColumnWidth)}`);
666
+ lines.push(theme.fg("muted", " * Architect's quality pick — Kairo has no separate orchestrator capability profile yet."));
667
+ }
585
668
  // The real OPERATIONAL team (see buildProjectStrategy's own doc) —
586
669
  // never qualityTeam, which is comparative reference only. The overlay
587
670
  // (project-overlay.js) already shows projectTeam under this exact
@@ -589,14 +672,42 @@ export class CockpitView {
589
672
  // same label was the real, reported mismatch. qualityTeam only
590
673
  // remains as a fallback for a strategy persisted before projectTeam
591
674
  // existed (see applyProjectTeamOverride's own legacy-entry comment).
675
+ const eligibility = this.snapshot?.modelIntelligence?.eligibility ?? {};
676
+ const claudeEntitlement = this.snapshot?.modelIntelligence?.claudeEntitlement ?? {};
592
677
  for (const entry of projectTeam) {
593
678
  lines.push(`${entry.role.padEnd(18)} ${entry.model ? this.teamRoleLabel(entry.model, modelColumnWidth) : theme.fg("warning", "no eligible option")}`);
679
+ const extra = this.projectTeamCompactExtraLine(entry, strategy, { eligibility, claudeEntitlement });
680
+ if (extra) lines.push(theme.fg("muted", ` ${extra}`));
594
681
  }
595
682
  if (strategy.status === "suggested") lines.push(theme.fg("muted", "Suggested from real project analysis. Use /project approve to activate."));
596
683
  if (strategy.status === "stale") lines.push(theme.fg("warning", "Real evidence changed since approval — use /project refresh."));
597
684
  return { title: `PROJECT TEAM · ${project} · ${strategy.status.toUpperCase()}`, lines };
598
685
  }
599
686
 
687
+ /**
688
+ * At most one muted extra line for the compact PROJECT TEAM panel —
689
+ * only when it adds something the role row itself doesn't already say
690
+ * (quality leader differs, or a real availability warning). Same pick
691
+ * + available → null.
692
+ */
693
+ projectTeamCompactExtraLine(entry, strategy, { eligibility = {}, claudeEntitlement = {} } = {}) {
694
+ const availability = resolveAssignmentAvailability(entry?.model, { eligibility, claudeEntitlement });
695
+ if (availability.warning) return availability.warning;
696
+ if (!strategy?.qualityTeam || !entry?.model) return null;
697
+ const qualityEntry = strategy.qualityTeam.find((row) => row.role === entry.role);
698
+ if (!qualityEntry?.model) return null;
699
+ const samePick = qualityEntry.model.adapterId === entry.model.adapterId
700
+ && qualityEntry.model.modelId === entry.model.modelId;
701
+ if (samePick) return null;
702
+ const retention = entry.decisionEvidence?.retention;
703
+ const retentionPct = retention != null ? Math.round(retention * 100) : null;
704
+ const leaderLabel = this.aiTeamLabel(qualityEntry.model);
705
+ if (retentionPct != null) {
706
+ return `Quality leader: ${leaderLabel} — operational pick retains ${retentionPct}%`;
707
+ }
708
+ return `Quality leader: ${leaderLabel}`;
709
+ }
710
+
600
711
  /** Contextual key hints — shown in the dashboard's fixed zone, not the scrollable conversation. */
601
712
  renderFooterLines() {
602
713
  const row = this.selectedRow();
@@ -899,27 +1010,15 @@ export class CockpitView {
899
1010
  return lines;
900
1011
  }
901
1012
 
902
- /** Plain-language description of what each role optimizes for — mirrors
903
- * buildAiTeamRoleDefinitions()'s real compute functions in
904
- * model-intelligence.js, never a per-model claim, so it never needs
905
- * updating when the underlying models change. */
906
- static ROLE_CAPABILITY_BLURB = {
907
- Explorer: "general reasoning capability",
908
- Architect: "general reasoning capability",
909
- Builder: "coding capability",
910
- Debugger: "reasoning and terminal-debugging capability",
911
- Tester: "coding and terminal-execution capability",
912
- Reviewer: "independent reasoning and coding review"
913
- };
1013
+ static ROLE_CAPABILITY_BLURB = ROLE_CAPABILITY_BLURB;
914
1014
 
915
1015
  /**
916
1016
  * The default, human-readable `/models` output: per role, the selected
917
- * model, why (the real distribution-policy reason when there is one,
918
- * else the role's plain-language capability requirement), the
919
- * EFFICIENT TEAM alternative when it actually differs, and the real
920
- * fallback used if the selection becomes unavailable. Deliberately no
921
- * raw metrics, percentages, internal ids, or source names — that detail
922
- * moves to /models --evidence (aiTeamDetailLines()) instead.
1017
+ * model, why (via explainTeamDecision — real reason or leader/blurb
1018
+ * formulation), the EFFICIENT TEAM alternative when it actually differs,
1019
+ * and the real fallback used if the selection becomes unavailable.
1020
+ * Deliberately no raw metrics, percentages, internal ids, or source
1021
+ * names that detail moves to /models --evidence (aiTeamDetailLines()).
923
1022
  */
924
1023
  modelsExplainLines() {
925
1024
  const intel = this.snapshot?.modelIntelligence;
@@ -930,7 +1029,8 @@ export class CockpitView {
930
1029
  const leaderByRole = Object.fromEntries((intel.globalGuide?.capability ?? []).map((entry) => [entry.role, entry]));
931
1030
  const freshness = intel.status === "live" ? "live" : `cached ${intel.age ?? "?"}`;
932
1031
  const lines = [theme.fg("muted", `Evidence: ${freshness}`)];
933
- aiTeam.forEach(({ role, primary, fallback, reason }, index) => {
1032
+ aiTeam.forEach((entry, index) => {
1033
+ const { role, primary, fallback } = entry;
934
1034
  // A blank string here would get silently dropped once routed through
935
1035
  // the persisted chat transcript (addTranscript trims and discards
936
1036
  // empty text) — a visible divider is the only separator that
@@ -938,8 +1038,7 @@ export class CockpitView {
938
1038
  if (index > 0) lines.push(theme.fg("muted", "·"));
939
1039
  const availabilityNote = primary.available ? "" : " (currently unavailable)";
940
1040
  lines.push(`${role.padEnd(10)} ${this.aiTeamLabel(primary)}${availabilityNote}`);
941
- const why = reason ?? `Selected for ${CockpitView.ROLE_CAPABILITY_BLURB[role] ?? "this role's capability requirement"}.`;
942
- lines.push(theme.fg("muted", ` ${why}`));
1041
+ lines.push(theme.fg("muted", ` ${explainTeamDecision(entry)}`));
943
1042
 
944
1043
  // The uncoordinated individual leader (globalGuide) — evidence for
945
1044
  // "what's honestly best with nothing else in play?", never the
@@ -984,15 +1083,18 @@ export class CockpitView {
984
1083
  * evidence the Model Intelligence Foundation registry has for that exact
985
1084
  * model. Never recalculates anything — every number here was already
986
1085
  * computed during real selection. Shared by AI TEAM and EFFICIENT TEAM
987
- * inside aiTeamDetailLines(); never called on its own.
1086
+ * inside aiTeamDetailLines(); also reused by projectTeamEvidenceLines
1087
+ * via the optional `extraLinesFor` hook (never forked).
988
1088
  * @param {Array<object>} team
1089
+ * @param {{extraLinesFor?: (entry: object) => string[]|null|undefined}} [options]
989
1090
  */
990
- teamEvidenceLines(team) {
1091
+ teamEvidenceLines(team, { extraLinesFor } = {}) {
991
1092
  const lines = [];
992
1093
  const corroborationLine = (model) => (model.corroboration ?? [])
993
1094
  .map((entry) => `${entry.metric}=${entry.value} (${entry.source})`)
994
1095
  .join(" · ");
995
- team.forEach(({ role, primary, fallback, reason, coverage, confidence, decisionEvidence }, index) => {
1096
+ team.forEach((entry, index) => {
1097
+ const { role, primary, fallback, reason, coverage, confidence, decisionEvidence } = entry;
996
1098
  // A blank string here would get silently dropped once this line is
997
1099
  // routed through the persisted chat transcript (addTranscript trims
998
1100
  // and discards empty text) — a visible divider is the only separator
@@ -1067,10 +1169,76 @@ export class CockpitView {
1067
1169
  if (fallback) lines.push(theme.fg("muted", ` fallback ${this.aiTeamLabelWithProvider(fallback)}`));
1068
1170
  else if (!primary.available) lines.push(theme.fg("warning", " no eligible fallback right now"));
1069
1171
  if (reason) lines.push(theme.fg("muted", ` ${reason}`));
1172
+ if (extraLinesFor) {
1173
+ for (const line of extraLinesFor(entry) ?? []) lines.push(line);
1174
+ }
1070
1175
  });
1071
1176
  return lines;
1072
1177
  }
1073
1178
 
1179
+ /**
1180
+ * Thin adapter: maps strategy.projectTeam entries onto teamEvidenceLines'
1181
+ * primary/fallback shape, resolving availability explicitly (projectModelRef
1182
+ * has no `available`), and injecting quality-leader + entitlement warnings
1183
+ * via extraLinesFor — never a forked evidence renderer.
1184
+ * @param {object} strategy
1185
+ * @param {{eligibility?: object, claudeEntitlement?: object}} [opts]
1186
+ */
1187
+ projectTeamEvidenceLines(strategy, { eligibility = {}, claudeEntitlement = {} } = {}) {
1188
+ const projectTeam = strategy?.projectTeam ?? [];
1189
+ const hasQualityTeam = Array.isArray(strategy?.qualityTeam);
1190
+ const qualityByRole = new Map((strategy?.qualityTeam ?? []).map((row) => [row.role, row]));
1191
+
1192
+ const team = projectTeam.map((entry) => {
1193
+ const primaryAvailability = resolveAssignmentAvailability(entry.model, { eligibility, claudeEntitlement });
1194
+ const fallbackAvailability = entry.fallback
1195
+ ? resolveAssignmentAvailability(entry.fallback, { eligibility, claudeEntitlement })
1196
+ : null;
1197
+ return {
1198
+ role: entry.role,
1199
+ primary: entry.model
1200
+ ? { ...entry.model, available: primaryAvailability.available }
1201
+ : { adapterId: "?", modelId: "?", displayName: "no eligible option", available: false },
1202
+ fallback: entry.fallback
1203
+ ? { ...entry.fallback, available: fallbackAvailability.available }
1204
+ : null,
1205
+ reason: entry.reason ?? null,
1206
+ decisionEvidence: entry.decisionEvidence ?? null,
1207
+ coverage: entry.coverage,
1208
+ confidence: entry.confidence,
1209
+ _availabilityWarning: primaryAvailability.warning,
1210
+ _qualityEntry: qualityByRole.get(entry.role) ?? null
1211
+ };
1212
+ });
1213
+
1214
+ return this.teamEvidenceLines(team, {
1215
+ extraLinesFor: (mapped) => {
1216
+ const extra = [];
1217
+ if (mapped._availabilityWarning) {
1218
+ extra.push(theme.fg("warning", ` ${mapped._availabilityWarning}`));
1219
+ }
1220
+ if (!hasQualityTeam) return extra;
1221
+ const qualityEntry = mapped._qualityEntry;
1222
+ if (!qualityEntry?.model || !mapped.primary?.modelId) return extra;
1223
+ const samePick = qualityEntry.model.adapterId === mapped.primary.adapterId
1224
+ && qualityEntry.model.modelId === mapped.primary.modelId;
1225
+ if (samePick) {
1226
+ extra.push(theme.fg("muted", " Also the quality leader for this role."));
1227
+ return extra;
1228
+ }
1229
+ const retention = mapped.decisionEvidence?.retention;
1230
+ const retentionPct = retention != null ? Math.round(retention * 100) : null;
1231
+ const leaderLabel = this.aiTeamLabel(qualityEntry.model);
1232
+ if (retentionPct != null) {
1233
+ extra.push(theme.fg("muted", ` Quality leader: ${leaderLabel} — operational pick retains ${retentionPct}%`));
1234
+ } else {
1235
+ extra.push(theme.fg("muted", ` Quality leader: ${leaderLabel}`));
1236
+ }
1237
+ return extra;
1238
+ }
1239
+ });
1240
+ }
1241
+
1074
1242
  /**
1075
1243
  * `/models --evidence`: the full breakdown behind both AI TEAM and
1076
1244
  * EFFICIENT TEAM picks — real provider, primary, availability, fallback,
@@ -89,7 +89,7 @@ export async function readCodexModels({
89
89
  child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
90
90
 
91
91
  writeRequest(child, 1, "initialize", {
92
- clientInfo: { name: "kairo", title: "Kairo", version: "0.28.0" },
92
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.29.0" },
93
93
  capabilities: {}
94
94
  });
95
95
  });
@@ -151,7 +151,7 @@ export async function readCodexUsage({
151
151
  child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
152
152
 
153
153
  writeRequest(child, 1, "initialize", {
154
- clientInfo: { name: "kairo", title: "Kairo", version: "0.28.0" },
154
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.29.0" },
155
155
  capabilities: {}
156
156
  });
157
157
  });