@kal-elsam/kairo-runtime 0.18.0 → 0.20.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 +41 -0
- package/package.json +1 -1
- package/src/global/cockpit/project-overlay.js +37 -11
- package/src/global/cockpit/view.js +30 -7
- package/src/global/conversation/service.js +2 -2
- package/src/global/intelligence/execution-router.js +19 -28
- package/src/global/intelligence/model-candidate-catalog.js +18 -7
- package/src/global/observability/codex-models.js +1 -1
- package/src/global/observability/codex-usage.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.20.0 — 2026-09-18 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. Promotes Cursor to a real automatic execution provider.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Cursor's accessMode flips from "manual" to "automatic"
|
|
15
|
+
(`model-candidate-catalog.js`). Its own execution adapter already
|
|
16
|
+
builds a real, auditable non-interactive launch (`cursor-agent -p
|
|
17
|
+
--output-format stream-json`) and parses its structured event stream
|
|
18
|
+
— the same shape as Codex/Claude, and an officially documented,
|
|
19
|
+
supported use of the Cursor CLI. Real task routing now judges Cursor
|
|
20
|
+
by the exact same launchable/eligibility gate as Codex/Claude, for
|
|
21
|
+
both execution and recommendations — no more special-cased
|
|
22
|
+
manual-only rejection in `execution-router.js`'s `checkCandidate`.
|
|
23
|
+
- Cursor's own opaque "auto" router model still stays manual (its
|
|
24
|
+
identity is non-deterministic, unlike a named model). OpenCode Go is
|
|
25
|
+
unaffected — it remains manual for its own, real, already-confirmed
|
|
26
|
+
Go/Zen billing-attribution ambiguity.
|
|
27
|
+
|
|
28
|
+
## 0.19.0 — 2026-09-18 (Kairo Runtime)
|
|
29
|
+
|
|
30
|
+
Patch-level polish on top of 0.18.0's PROJECT TEAM work: fixes the
|
|
31
|
+
provider column's alignment and adds a way back to the interactive
|
|
32
|
+
analyst picker once a strategy already exists.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `r` key inside the `/project` overlay (RESULT/ACTIVE/STALE states)
|
|
37
|
+
forces a genuinely fresh analysis, landing back on the real
|
|
38
|
+
interactive analyst picker — previously only reachable on a
|
|
39
|
+
project's very first `/project` run.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The PROJECT TEAM view's provider column (`CockpitView.teamRoleLabel()`)
|
|
44
|
+
now pads every row's model name to a shared width
|
|
45
|
+
(`teamModelColumnWidth()`), so the " · Provider" separator lines up
|
|
46
|
+
across rows regardless of how much individual model names vary in
|
|
47
|
+
length.
|
|
48
|
+
|
|
8
49
|
## 0.18.0 — 2026-09-18 (Kairo Runtime)
|
|
9
50
|
|
|
10
51
|
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.
|
|
3
|
+
"version": "0.20.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,
|
|
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 ?
|
|
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,
|
|
598
|
-
for (const line of teamLines("Efficient (reference)", strategy.efficientTeam,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
580
|
-
|
|
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
|
|
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
|
/**
|
|
@@ -1235,7 +1258,7 @@ export class CockpitView {
|
|
|
1235
1258
|
`Claude ${claudeText}`,
|
|
1236
1259
|
`Go ${goText}`,
|
|
1237
1260
|
`Zen ${zenText}`,
|
|
1238
|
-
`Cursor ${entry("Cursor", "
|
|
1261
|
+
`Cursor ${entry("Cursor", "READY · usage unknown")}`
|
|
1239
1262
|
];
|
|
1240
1263
|
}
|
|
1241
1264
|
|
|
@@ -496,8 +496,8 @@ export function createConversationService(deps = {}) {
|
|
|
496
496
|
// scoredAllRaw directly, so an old generation Cursor still
|
|
497
497
|
// re-exposes (e.g. Claude Sonnet 4) naturally stops competing
|
|
498
498
|
// without buildAiTeam/buildEfficientTeam's own ranking logic
|
|
499
|
-
// needing to know why. Manual-only real candidates (
|
|
500
|
-
//
|
|
499
|
+
// needing to know why. Manual-only real candidates (OpenCode Go,
|
|
500
|
+
// and Cursor's own opaque "auto" router model) stay in it — this is "what Kairo can honestly
|
|
501
501
|
// recommend", not "what Kairo can launch by itself".
|
|
502
502
|
const scoredAll = buildRecommendationPool(scoredAllRaw, completeCandidateCatalog);
|
|
503
503
|
// The Automatic Execution Pool: the real subset of scoredAll
|
|
@@ -156,40 +156,32 @@ export const LOW_QUOTA_WARN_PERCENT = 20;
|
|
|
156
156
|
* have the model via the Go subscription — without claiming Kairo can
|
|
157
157
|
* safely auto-execute through it yet (see opencode.js's checkAvailability
|
|
158
158
|
* for why: no per-event way to prove a run didn't silently bill Zen).
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* Real task routing (selectExecutionProvider/
|
|
164
|
-
* uses the default `true` — it must never pick
|
|
165
|
-
* fail at launch (run-manager.js's own
|
|
159
|
+
* Cursor no longer needs this exemption: its own execution adapter
|
|
160
|
+
* (execution-adapters/cursor.js) builds a real, auditable non-interactive
|
|
161
|
+
* launch and Cursor's own docs support headless/CI use, so real task
|
|
162
|
+
* routing now judges it exactly like Codex/Claude — same launchable gate,
|
|
163
|
+
* no special case. Real task routing (selectExecutionProvider/
|
|
164
|
+
* selectAskProvider) always uses the default `true` — it must never pick
|
|
165
|
+
* something guaranteed to fail at launch (run-manager.js's own
|
|
166
|
+
* launchable gate would reject it).
|
|
166
167
|
*/
|
|
167
168
|
export function checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, opencodeGoUsage, cursorManualQuota }, { requireLaunchable = true } = {}) {
|
|
168
169
|
// Zen carries real PAYG/billing risk (see conversation/service.js's
|
|
169
170
|
// capabilities.openCodeExecution) — never an automatic pick, regardless
|
|
170
171
|
// of what its real catalog/benchmarks might otherwise say.
|
|
171
172
|
if (adapterId === "opencode-zen") return { ok: false, reason: "OpenCode Zen is excluded from automatic routing (PAYG risk)" };
|
|
172
|
-
// Cursor is a deliberate manual-only destination for real task
|
|
173
|
-
// execution — recommendation and execution are different capabilities
|
|
174
|
-
// (see model-intelligence.js's role assignments): Kairo can genuinely
|
|
175
|
-
// recommend a real Cursor model for a role (Builder -> Cursor Composer
|
|
176
|
-
// 2.5, say), the same requireLaunchable:false exception opencode-go
|
|
177
|
-
// already gets below, but real task routing (requireLaunchable: true,
|
|
178
|
-
// the default) always refuses it — the user continues that work
|
|
179
|
-
// manually inside the Cursor IDE, never an automatic Kairo-launched run.
|
|
180
|
-
if (adapterId === "cursor" && requireLaunchable) {
|
|
181
|
-
return { ok: false, reason: "Cursor is manual-only — continue the work in the Cursor IDE, never auto-executed by Kairo" };
|
|
182
|
-
}
|
|
183
173
|
|
|
184
174
|
const adapter = findAdapter(adapterId, adapters);
|
|
185
175
|
if (!adapter) return { ok: false, reason: `${adapterId}: no adapter found` };
|
|
186
176
|
if (!adapter.available) return { ok: false, reason: adapter.reason ?? `${adapterId}: not available` };
|
|
187
177
|
// "launchable" means safe for Kairo to invoke programmatically — not the
|
|
188
|
-
// bar a manual recommendation needs.
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
178
|
+
// bar a manual recommendation needs. OpenCode Go alone keeps the
|
|
179
|
+
// exemption: real availability is enough to recommend it, even though
|
|
180
|
+
// Kairo can't safely auto-execute through it yet (unresolved Go/Zen
|
|
181
|
+
// billing-attribution gap — see opencode.js). Cursor no longer needs
|
|
182
|
+
// this exemption — its real launchability is judged the same way for
|
|
183
|
+
// both recommendation and execution now.
|
|
184
|
+
const launchableRequired = requireLaunchable || adapterId !== "opencode-go";
|
|
193
185
|
if (launchableRequired && !adapter.launchable) return { ok: false, reason: adapter.reason ?? `${adapterId}: not launchable yet` };
|
|
194
186
|
|
|
195
187
|
if (adapterId === "codex") {
|
|
@@ -209,11 +201,10 @@ export function checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, o
|
|
|
209
201
|
if (limited) return { ok: false, reason: `OpenCode Go ${limited.name} window is rate-limited` };
|
|
210
202
|
}
|
|
211
203
|
// Cursor: only ever the human's own last word (see this function's own
|
|
212
|
-
// doc) — never fabricated from a guess.
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// of quota.
|
|
204
|
+
// doc) — never fabricated from a guess. Applies to BOTH the
|
|
205
|
+
// recommendation path AND real task routing now that Cursor is a real
|
|
206
|
+
// automatic candidate — a human-reported "out of credits" must block
|
|
207
|
+
// an actual launch, not just a suggestion.
|
|
217
208
|
if (adapterId === "cursor" && cursorManualQuota?.manualExhausted) {
|
|
218
209
|
return { ok: false, reason: cursorManualQuota.reason ?? "Cursor marked out of credits (manual, via /project cursor exhausted)" };
|
|
219
210
|
}
|
|
@@ -39,7 +39,7 @@ import { matchArtificialAnalysisScore } from "./model-intelligence.js";
|
|
|
39
39
|
* @property {string} modelName - human-readable clean name, with real effort/context/privacy variant tokens stripped (see stripDisplayVariant). Never invented — always derived from the provider's own real displayName.
|
|
40
40
|
* @property {string} rawDisplayName - the provider's own displayName, completely unmodified — the real evidence modelName was derived from. Whatever stripDisplayVariant peeled off (effort/context/privacy tokens) to produce modelName is still visible here, never a separate field: /models --evidence's own "technical detail" is just this string.
|
|
41
41
|
* @property {string} adapterId - "codex" | "claude" | "cursor" | "opencode-go".
|
|
42
|
-
* @property {"automatic"|"manual"} accessMode - whether Kairo can actually launch this candidate itself right now, or whether it's a real, recommendable option the human runs manually (Cursor always; OpenCode Go until its own empirical automatic-execution proof lands — see this module's own doc).
|
|
42
|
+
* @property {"automatic"|"manual"} accessMode - whether Kairo can actually launch this candidate itself right now, or whether it's a real, recommendable option the human runs manually (Cursor's own "auto" router model always; OpenCode Go until its own empirical automatic-execution proof lands — see this module's own doc). Named Cursor models are automatic.
|
|
43
43
|
* @property {"scored"|"partial"|"unscored"} evidenceStatus - "scored": AA matched this exact model AND reports at least one of intelligenceIndex/codingIndex. "partial": AA matched it but both composite indices are null (real match, thin evidence). "unscored": no confident AA match at all. Never role-specific — see this module's own doc for why.
|
|
44
44
|
* @property {string|null} lineageKey - real, recognized model family/lineage (see LINEAGE_PARSERS) — null when the modelId doesn't match any recognized, conservative pattern. Never guessed.
|
|
45
45
|
* @property {number|null} generation - a real, comparable version number within that lineage — null whenever lineageKey is null.
|
|
@@ -285,12 +285,22 @@ function applyLifecycle(catalog) {
|
|
|
285
285
|
// automatic-execution proof this session's plan calls for
|
|
286
286
|
// (`opencode run -m opencode-go/<model>`, verifying real provider
|
|
287
287
|
// attribution and Go-only consumption) actually runs and passes — see
|
|
288
|
-
// this module's own doc. Cursor is
|
|
289
|
-
// execution-
|
|
290
|
-
//
|
|
291
|
-
|
|
288
|
+
// this module's own doc. Cursor is "automatic": its own execution
|
|
289
|
+
// adapter (execution-adapters/cursor.js) already builds a real,
|
|
290
|
+
// auditable non-interactive launch (`cursor-agent -p --output-format
|
|
291
|
+
// stream-json`) and parses its structured event stream, the exact same
|
|
292
|
+
// shape as Codex/Claude — Cursor's own docs explicitly support this
|
|
293
|
+
// (headless/CI use is an intended, documented capability, not a hack).
|
|
294
|
+
// The earlier "permanent manual" policy predated this adapter being
|
|
295
|
+
// finished and had no technical or billing reason behind it (unlike
|
|
296
|
+
// OpenCode Go's real, confirmed Go/Zen billing-attribution gap above).
|
|
297
|
+
const ACCESS_MODE_BY_ADAPTER = { codex: "automatic", claude: "automatic", cursor: "automatic", "opencode-go": "manual" };
|
|
292
298
|
|
|
293
299
|
function resolveAccessMode(adapterId, modelId) {
|
|
300
|
+
// Cursor's own "auto" router picks whichever underlying model it wants
|
|
301
|
+
// per request — an opaque, non-deterministic identity Kairo can't
|
|
302
|
+
// attribute to a real scored model, so THIS one candidate stays manual
|
|
303
|
+
// even though named Cursor models are now real automatic candidates.
|
|
294
304
|
if (adapterId === "cursor" && modelId === "auto") return "manual";
|
|
295
305
|
return ACCESS_MODE_BY_ADAPTER[adapterId] ?? "manual";
|
|
296
306
|
}
|
|
@@ -437,8 +447,9 @@ export function buildRecommendationPool(scoredAll, completeCatalog) {
|
|
|
437
447
|
* The Automatic Execution Pool: the subset of the Recommendation Pool
|
|
438
448
|
* Kairo can actually launch itself, right now — real routing's own
|
|
439
449
|
* candidate source, never QUALITY/EFFICIENT TEAM's. Requires BOTH a real
|
|
440
|
-
* accessMode of "automatic" (
|
|
441
|
-
*
|
|
450
|
+
* accessMode of "automatic" (OpenCode Go is currently manual, and
|
|
451
|
+
* Cursor's own "auto" router model stays manual — see
|
|
452
|
+
* ModelCandidateIdentity's own doc) AND real,
|
|
442
453
|
* current eligibility (adapter availability, quota, launchability — the
|
|
443
454
|
* exact same `eligibility` object checkCandidate/execution-router.js
|
|
444
455
|
* already compute, reused here rather than reimplemented). Never
|
|
@@ -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.20.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.20.0" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|