@kal-elsam/kairo-runtime 0.18.0 → 0.19.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,27 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.19.0 — 2026-09-18 (Kairo Runtime)
9
+
10
+ Patch-level polish on top of 0.18.0's PROJECT TEAM work: fixes the
11
+ provider column's alignment and adds a way back to the interactive
12
+ analyst picker once a strategy already exists.
13
+
14
+ ### Added
15
+
16
+ - `r` key inside the `/project` overlay (RESULT/ACTIVE/STALE states)
17
+ forces a genuinely fresh analysis, landing back on the real
18
+ interactive analyst picker — previously only reachable on a
19
+ project's very first `/project` run.
20
+
21
+ ### Fixed
22
+
23
+ - The PROJECT TEAM view's provider column (`CockpitView.teamRoleLabel()`)
24
+ now pads every row's model name to a shared width
25
+ (`teamModelColumnWidth()`), so the " · Provider" separator lines up
26
+ across rows regardless of how much individual model names vary in
27
+ length.
28
+
8
29
  ## 0.18.0 — 2026-09-18 (Kairo Runtime)
9
30
 
10
31
  Minor release. Provider visibility and quota-aware improvements to the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",
@@ -40,14 +40,15 @@ const overlayFrameTheme = {
40
40
  fg: (role, text) => theme.fg(role === "border" ? "info" : role, text)
41
41
  };
42
42
 
43
- function teamLines(label, team, aiTeamLabel, tone = "bold") {
43
+ function teamLines(label, team, view, tone = "bold") {
44
44
  const lines = [tone === "bold" ? theme.bold(label) : theme.fg("muted", label)];
45
45
  if (!team?.length) {
46
46
  lines.push(theme.fg("muted", " (no active roles)"));
47
47
  return lines;
48
48
  }
49
+ const modelColumnWidth = view.teamModelColumnWidth(team.map((entry) => entry.model));
49
50
  for (const entry of team) {
50
- const modelText = entry.model ? aiTeamLabel(entry.model) : theme.fg("warning", "no eligible option");
51
+ const modelText = entry.model ? view.teamRoleLabel(entry.model, modelColumnWidth) : theme.fg("warning", "no eligible option");
51
52
  lines.push(theme.fg("muted", ` ${entry.role.padEnd(10)} ${modelText}`));
52
53
  }
53
54
  return lines;
@@ -174,6 +175,23 @@ export class ProjectOverlay {
174
175
  this.requestRender();
175
176
  }
176
177
 
178
+ /**
179
+ * Forces a genuinely fresh analysis — the ONLY way back to the real
180
+ * interactive analyst-picker (SELECT_ANALYST) once a SUGGESTED/ACTIVE/
181
+ * STALE strategy already exists (the constructor's own existing-strategy
182
+ * shortcut otherwise always wins). Discards the held suggested/active
183
+ * strategy in memory only — nothing persisted is touched until a new
184
+ * choice is confirmed and a new /project approve happens; the previous
185
+ * real strategy stays exactly as persisted if the human backs out
186
+ * (Esc from SELECT_ANALYST) before confirming anything.
187
+ */
188
+ reanalyze() {
189
+ this.suggestedStrategy = null;
190
+ this.activeStrategy = null;
191
+ this.resultSelectList = null;
192
+ void this.loadPreflight();
193
+ }
194
+
177
195
  /** A short, honest label for a catalog entry's own real recommendationTags/evidenceStatus — never a fabricated "Quality"/"Efficient" claim for a model that doesn't actually carry that tag. */
178
196
  static tagLabel(model) {
179
197
  if (model.evidenceStatus === "unscored") return "Unscored";
@@ -269,8 +287,9 @@ export class ProjectOverlay {
269
287
  */
270
288
  buildResultRoleList() {
271
289
  const team = this.suggestedStrategy?.projectTeam ?? [];
290
+ const modelColumnWidth = this.view.teamModelColumnWidth(team.map((entry) => entry.model));
272
291
  const items = team.map((entry) => {
273
- const modelText = entry.model ? this.view.teamRoleLabel(entry.model) : "no eligible option";
292
+ const modelText = entry.model ? this.view.teamRoleLabel(entry.model, modelColumnWidth) : "no eligible option";
274
293
  const overrideNote = entry.assignmentSource === "override" ? theme.fg("accent", " (override)") : "";
275
294
  // Role + model are the real primary information here — explicit
276
295
  // `text` color, never left to default/muted.
@@ -458,11 +477,19 @@ export class ProjectOverlay {
458
477
  // edits the highlighted role instead. Enter must never silently
459
478
  // approve just because a role row happens to be focused.
460
479
  if (data === "a" || data === "A") return void this.approve();
480
+ // "r" forces a genuinely fresh analysis (re-picks the analyst from
481
+ // scratch) — distinct from editing one role's model (Enter) or
482
+ // approving the current suggestion (a) — see reanalyze()'s own doc.
483
+ if (data === "r" || data === "R") return void this.reanalyze();
461
484
  this.resultSelectList.handleInput(data);
462
485
  this.requestRender();
463
486
  return;
464
487
  }
465
488
 
489
+ if ((this.state === S.ACTIVE || this.state === S.STALE) && (data === "r" || data === "R")) {
490
+ return void this.reanalyze();
491
+ }
492
+
466
493
  if (this.state === S.EDIT_MODEL_SEARCH) {
467
494
  if (matchesKey(data, Key.escape) || matchesKey(data, Key.esc)) {
468
495
  this.state = S.RESULT;
@@ -533,7 +560,6 @@ export class ProjectOverlay {
533
560
  // isn't a modal boundary a human eye reliably notices.
534
561
  const box = new Box(2, 1);
535
562
  this.box = box;
536
- const aiTeamLabel = (model) => this.view.teamRoleLabel(model);
537
563
  // Text defaults to one blank row above and below every child; inside
538
564
  // a framed modal that inflated the height until pi-tui clipped the
539
565
  // bottom border. Keep spacing explicit and compact instead.
@@ -594,9 +620,9 @@ export class ProjectOverlay {
594
620
  // Quality/Efficient stay real, comparative REFERENCE — muted, and
595
621
  // rendered strictly below the real operational PROJECT TEAM list
596
622
  // above, never replacing it visually.
597
- for (const line of teamLines("Quality (reference)", strategy.qualityTeam, aiTeamLabel, "muted")) push(line);
598
- for (const line of teamLines("Efficient (reference)", strategy.efficientTeam, aiTeamLabel, "muted")) push(line);
599
- push(theme.fg("muted", "Enter edit role · a approve & activate · Esc close without approving"));
623
+ for (const line of teamLines("Quality (reference)", strategy.qualityTeam, this.view, "muted")) push(line);
624
+ for (const line of teamLines("Efficient (reference)", strategy.efficientTeam, this.view, "muted")) push(line);
625
+ push(theme.fg("muted", "Enter edit role · a approve & activate · r re-analyze from scratch · Esc close without approving"));
600
626
  break;
601
627
  }
602
628
  case S.EDIT_LOADING:
@@ -645,16 +671,16 @@ export class ProjectOverlay {
645
671
  const strategy = this.activeStrategy;
646
672
  push(theme.fg("success", "ACTIVE"));
647
673
  push(theme.fg("muted", `Approved ${strategy.approvedAt ?? "?"}`));
648
- for (const line of teamLines("PROJECT TEAM", strategy.projectTeam ?? strategy.qualityTeam, aiTeamLabel)) push(line);
649
- push(theme.fg("muted", "Esc close"));
674
+ for (const line of teamLines("PROJECT TEAM", strategy.projectTeam ?? strategy.qualityTeam, this.view)) push(line);
675
+ push(theme.fg("muted", "r re-analyze from scratch · Esc close"));
650
676
  break;
651
677
  }
652
678
  case S.STALE: {
653
679
  const strategy = this.activeStrategy;
654
680
  push(theme.fg("warning", "STALE"));
655
681
  push(theme.fg("muted", "The real project evidence has changed since this team was approved — previous assignments are kept until refreshed."));
656
- for (const line of teamLines("PROJECT TEAM (previous)", strategy.projectTeam ?? strategy.qualityTeam, aiTeamLabel)) push(line);
657
- push(theme.fg("muted", "Enter refresh · Esc close"));
682
+ for (const line of teamLines("PROJECT TEAM (previous)", strategy.projectTeam ?? strategy.qualityTeam, this.view)) push(line);
683
+ push(theme.fg("muted", "Enter refresh · r re-analyze from scratch · Esc close"));
658
684
  break;
659
685
  }
660
686
  case S.ERROR:
@@ -576,8 +576,12 @@ export class CockpitView {
576
576
  };
577
577
  }
578
578
  const lines = [];
579
- if (strategy.bootstrapAnalyst) lines.push(`${"Project Analyst".padEnd(18)} ${this.teamRoleLabel(strategy.bootstrapAnalyst)}`);
580
- if (strategy.orchestrator) lines.push(`${"Orchestrator".padEnd(18)} ${this.teamRoleLabel(strategy.orchestrator)}`);
579
+ const projectTeam = strategy.projectTeam ?? strategy.qualityTeam ?? [];
580
+ const modelColumnWidth = this.teamModelColumnWidth([
581
+ strategy.bootstrapAnalyst, strategy.orchestrator, ...projectTeam.map((entry) => entry.model)
582
+ ]);
583
+ 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)}`);
581
585
  // The real OPERATIONAL team (see buildProjectStrategy's own doc) —
582
586
  // never qualityTeam, which is comparative reference only. The overlay
583
587
  // (project-overlay.js) already shows projectTeam under this exact
@@ -585,8 +589,8 @@ export class CockpitView {
585
589
  // same label was the real, reported mismatch. qualityTeam only
586
590
  // remains as a fallback for a strategy persisted before projectTeam
587
591
  // existed (see applyProjectTeamOverride's own legacy-entry comment).
588
- for (const entry of strategy.projectTeam ?? strategy.qualityTeam ?? []) {
589
- lines.push(`${entry.role.padEnd(18)} ${entry.model ? this.teamRoleLabel(entry.model) : theme.fg("warning", "no eligible option")}`);
592
+ for (const entry of projectTeam) {
593
+ lines.push(`${entry.role.padEnd(18)} ${entry.model ? this.teamRoleLabel(entry.model, modelColumnWidth) : theme.fg("warning", "no eligible option")}`);
590
594
  }
591
595
  if (strategy.status === "suggested") lines.push(theme.fg("muted", "Suggested from real project analysis. Use /project approve to activate."));
592
596
  if (strategy.status === "stale") lines.push(theme.fg("warning", "Real evidence changed since approval — use /project refresh."));
@@ -764,10 +768,29 @@ export class CockpitView {
764
768
  * actually run against — two roles can land on visually similar model
765
769
  * names from different providers, and knowing which subscription a role
766
770
  * draws from is exactly what a real, provider-aware team review needs.
771
+ *
772
+ * `modelColumnWidth` left-pads the model name so every row's " · Provider"
773
+ * lines up in its own column instead of drifting with each model name's
774
+ * own length — see `CockpitView.teamModelColumnWidth()`, which a caller
775
+ * computes once across the real rows it's about to render and passes
776
+ * here for every row in that same list.
777
+ * @param {object} model
778
+ * @param {number} [modelColumnWidth]
767
779
  */
768
- teamRoleLabel(model) {
780
+ teamRoleLabel(model, modelColumnWidth = 0) {
769
781
  const provider = model.adapterId.charAt(0).toUpperCase() + model.adapterId.slice(1);
770
- return `${this.aiTeamLabel(model)} · ${provider}`;
782
+ return `${this.aiTeamLabel(model).padEnd(modelColumnWidth)} · ${provider}`;
783
+ }
784
+
785
+ /**
786
+ * The real model-name column width for a set of rows about to be
787
+ * rendered with `teamRoleLabel()` — the longest real, cleaned model
788
+ * name among them, never a fixed guess (model names vary wildly in
789
+ * length, e.g. "GPT-5.6-Terra" vs "Muse Spark 1.3 1M Extra High").
790
+ * @param {Array<object|null|undefined>} models
791
+ */
792
+ teamModelColumnWidth(models) {
793
+ return models.reduce((max, model) => (model ? Math.max(max, this.aiTeamLabel(model).length) : max), 0);
771
794
  }
772
795
 
773
796
  /**
@@ -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.18.0" },
92
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.19.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.18.0" },
154
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.19.0" },
155
155
  capabilities: {}
156
156
  });
157
157
  });