@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.
|
|
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 —
|
|
300
|
-
//
|
|
301
|
-
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
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((
|
|
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
|
-
|
|
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();
|
|
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((
|
|
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.
|
|
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.
|
|
154
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.29.0" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|