@kal-elsam/kairo-runtime 0.13.1 → 0.15.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 +107 -0
- package/README.md +12 -10
- package/bin/kairo-runtime.js +0 -0
- package/bin/kairo.js +0 -0
- package/package.json +5 -1
- package/scripts/cockpit-smoke.mjs +14 -10
- package/src/cli.js +182 -11
- package/src/global/check-resolutions.js +31 -0
- package/src/global/cli-help.js +23 -4
- package/src/global/component-ecosystem-checks.js +2 -0
- package/src/global/component-integration-cli.js +29 -10
- package/src/global/components-resolve-cli.js +246 -0
- package/src/global/connection-actions.js +147 -0
- package/src/global/connections.js +269 -0
- package/src/global/fleet-configure-plan.js +123 -0
- package/src/global/fleet-configure.js +303 -0
- package/src/global/fleet-models.js +188 -0
- package/src/global/fleet-set.js +219 -0
- package/src/global/fleet-shared.js +38 -0
- package/src/global/ink/cockpit-controller.js +2 -4
- package/src/global/ink/cockpit-enter.js +3 -1
- package/src/global/ink/cockpit-focus.js +7 -1
- package/src/global/ink/cockpit-models.js +33 -21
- package/src/global/ink/cockpit-palette.js +8 -3
- package/src/global/ink/cockpit-views.js +9 -2
- package/src/global/ink/orchestrator-app.js +42 -4
- package/src/global/ink/ux/live-overview.js +53 -26
- package/src/global/ink/ux/overview-actions.js +80 -0
- package/src/global/ink/ux/overview-needs.js +1 -1
- package/src/global/integrations/engram-evidence.js +7 -2
- package/src/global/integrations/sdd-apply.js +17 -7
- package/src/global/integrations/sdd-evidence.js +22 -3
- package/src/global/integrations/sdd-plan.js +21 -3
- package/src/global/integrations/sdd-resolutions.js +73 -0
- package/src/global/integrations/sdd-state.js +69 -0
- package/src/global/integrations/sdd-verify.js +9 -4
- package/src/global/mcp/kairo-mcp.js +56 -5
- package/src/global/mcp/resolve-mcp-workspace.js +51 -0
- package/src/global/mcp/work-snapshot-rule.js +89 -0
- package/src/global/mcp/work-snapshot-tool.js +49 -0
- package/src/global/mcp-install.js +239 -0
- package/src/global/next/next-cli.js +35 -0
- package/src/global/next/next-report.js +145 -0
- package/src/global/next/project-key.js +36 -0
- package/src/global/next/publish-work-snapshot.js +116 -0
- package/src/global/next/work-enroll.js +91 -0
- package/src/global/next/work-snapshot.js +216 -0
- package/src/global/observability/fleet-activity.js +197 -0
- package/src/global/observability/fleet-models-catalog.js +137 -0
- package/src/global/observability/fleet-platforms.js +166 -0
- package/src/global/observability/fleet-probe.js +229 -0
- package/src/global/paths.js +2 -1
- package/src/global/self-update.js +216 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified fleet model profile (~/.harness/fleet-models.json).
|
|
3
|
+
* Multi-agent platforms (Claude + OpenCode) share phase keys.
|
|
4
|
+
* Codex keeps a single default model. Cursor Auto stays IDE-managed.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, mkdir } from "node:fs/promises";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { resolveHomeDir } from "./paths.js";
|
|
9
|
+
import { writeAtomicJson } from "./runtime/write-atomic-json.js";
|
|
10
|
+
import {
|
|
11
|
+
SDD_PHASES,
|
|
12
|
+
loadGentleClaudeAssignments
|
|
13
|
+
} from "./fleet-shared.js";
|
|
14
|
+
import { parseFrontmatterModel, parseCodexDefaultModel } from "./observability/fleet-platforms.js";
|
|
15
|
+
|
|
16
|
+
export const FLEET_MODELS_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
export function fleetModelsPath(homeDir = resolveHomeDir()) {
|
|
19
|
+
return join(homeDir, ".harness", "fleet-models.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function emptyFleetProfile() {
|
|
23
|
+
const phases = {};
|
|
24
|
+
for (const id of SDD_PHASES) {
|
|
25
|
+
phases[id] = { claude: null, opencode: null };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
version: FLEET_MODELS_VERSION,
|
|
29
|
+
claudeDefault: null,
|
|
30
|
+
codexDefault: null,
|
|
31
|
+
cursorAgentModel: "inherit",
|
|
32
|
+
phases,
|
|
33
|
+
note: "Multi-agent phases for Claude + OpenCode. Codex uses codexDefault only. Cursor agents use inherit (Auto is IDE-managed)."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readJsonSafe(path, read) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(await read(path, "utf8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Seed profile from Gentle + current on-disk OpenCode/Claude/Codex.
|
|
47
|
+
* Preserves tuned OpenCode models when present.
|
|
48
|
+
*/
|
|
49
|
+
export async function seedFleetProfile({
|
|
50
|
+
homeDir = resolveHomeDir(),
|
|
51
|
+
read = readFile
|
|
52
|
+
} = {}) {
|
|
53
|
+
const profile = emptyFleetProfile();
|
|
54
|
+
const gentle = await loadGentleClaudeAssignments(homeDir, read);
|
|
55
|
+
if (gentle) {
|
|
56
|
+
profile.claudeDefault = gentle.default ?? "sonnet";
|
|
57
|
+
for (const id of SDD_PHASES) {
|
|
58
|
+
if (gentle[id]) profile.phases[id].claude = gentle[id];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const settings = await readJsonSafe(join(homeDir, ".claude", "settings.json"), read);
|
|
63
|
+
if (settings?.model && !profile.claudeDefault) profile.claudeDefault = settings.model;
|
|
64
|
+
|
|
65
|
+
for (const id of SDD_PHASES) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = await read(join(homeDir, ".claude", "agents", `${id}.md`), "utf8");
|
|
68
|
+
const model = parseFrontmatterModel(raw).model;
|
|
69
|
+
if (model && model !== "inherit") profile.phases[id].claude = model;
|
|
70
|
+
} catch { /* missing */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const oc = await readJsonSafe(join(homeDir, ".config", "opencode", "opencode.json"), read);
|
|
74
|
+
const agents = oc?.agent ?? oc?.agents ?? {};
|
|
75
|
+
for (const id of SDD_PHASES) {
|
|
76
|
+
const model = agents[id]?.model;
|
|
77
|
+
// Only declare OpenCode models that exist on disk — never invent from Claude tiers.
|
|
78
|
+
if (typeof model === "string") profile.phases[id].opencode = model;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const toml = await read(join(homeDir, ".codex", "config.toml"), "utf8");
|
|
83
|
+
profile.codexDefault = parseCodexDefaultModel(toml);
|
|
84
|
+
} catch { /* missing */ }
|
|
85
|
+
|
|
86
|
+
return profile;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function loadFleetProfile({
|
|
90
|
+
homeDir = resolveHomeDir(),
|
|
91
|
+
read = readFile,
|
|
92
|
+
seedIfMissing = true
|
|
93
|
+
} = {}) {
|
|
94
|
+
const path = fleetModelsPath(homeDir);
|
|
95
|
+
const existing = await readJsonSafe(path, read);
|
|
96
|
+
if (existing?.phases) {
|
|
97
|
+
return { profile: existing, path, seeded: false };
|
|
98
|
+
}
|
|
99
|
+
if (!seedIfMissing) {
|
|
100
|
+
return { profile: emptyFleetProfile(), path, seeded: false };
|
|
101
|
+
}
|
|
102
|
+
const profile = await seedFleetProfile({ homeDir, read });
|
|
103
|
+
return { profile, path, seeded: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function saveFleetProfile(profile, {
|
|
107
|
+
homeDir = resolveHomeDir(),
|
|
108
|
+
writeAtomicJsonFn = writeAtomicJson,
|
|
109
|
+
mkdirFn = mkdir
|
|
110
|
+
} = {}) {
|
|
111
|
+
const path = fleetModelsPath(homeDir);
|
|
112
|
+
await mkdirFn(dirname(path), { recursive: true });
|
|
113
|
+
const next = {
|
|
114
|
+
...profile,
|
|
115
|
+
version: FLEET_MODELS_VERSION,
|
|
116
|
+
updatedAt: new Date().toISOString()
|
|
117
|
+
};
|
|
118
|
+
await writeAtomicJsonFn(path, next);
|
|
119
|
+
return { path, profile: next };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Expand profile → assignment maps used by apply. */
|
|
123
|
+
export function profileToPlatformAssignments(profile) {
|
|
124
|
+
const claude = {};
|
|
125
|
+
const opencode = {};
|
|
126
|
+
if (profile.claudeDefault) claude.default = profile.claudeDefault;
|
|
127
|
+
for (const id of SDD_PHASES) {
|
|
128
|
+
const row = profile.phases?.[id] ?? {};
|
|
129
|
+
if (row.claude) claude[id] = row.claude;
|
|
130
|
+
if (row.opencode) opencode[id] = row.opencode;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
claude,
|
|
134
|
+
opencode,
|
|
135
|
+
codex: profile.codexDefault ? { codex_default: profile.codexDefault } : {},
|
|
136
|
+
cursorAgentModel: profile.cursorAgentModel ?? "inherit"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function formatFleetProfileText(profile, { path = null } = {}) {
|
|
141
|
+
const lines = ["Fleet profile (multi-agent)", ""];
|
|
142
|
+
if (path) lines.push(`Path · ${path}`);
|
|
143
|
+
lines.push(`Claude default · ${profile.claudeDefault ?? "—"}`);
|
|
144
|
+
lines.push(`Codex default · ${profile.codexDefault ?? "—"} (single-model tool)`);
|
|
145
|
+
lines.push(`Cursor agents · ${profile.cursorAgentModel ?? "inherit"} (Auto IDE-managed)`);
|
|
146
|
+
lines.push("");
|
|
147
|
+
for (const id of SDD_PHASES) {
|
|
148
|
+
const row = profile.phases?.[id] ?? {};
|
|
149
|
+
lines.push(`${id} · claude ${row.claude ?? "—"} · opencode ${row.opencode ?? "—"}`);
|
|
150
|
+
}
|
|
151
|
+
if (profile.note) {
|
|
152
|
+
lines.push("");
|
|
153
|
+
lines.push(profile.note);
|
|
154
|
+
}
|
|
155
|
+
return lines.join("\n").trimEnd();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function runFleetModels({
|
|
159
|
+
json = false,
|
|
160
|
+
profile = false,
|
|
161
|
+
homeDir = resolveHomeDir()
|
|
162
|
+
} = {}) {
|
|
163
|
+
const { printJson } = await import("./json-output.js");
|
|
164
|
+
const { commandHeader } = await import("./brand/index.js");
|
|
165
|
+
const { buildFleetModelsCatalog, formatFleetModelsText } = await import(
|
|
166
|
+
"./observability/fleet-models-catalog.js"
|
|
167
|
+
);
|
|
168
|
+
const catalog = await buildFleetModelsCatalog({ homeDir });
|
|
169
|
+
if (!profile) {
|
|
170
|
+
if (json) printJson(catalog);
|
|
171
|
+
else {
|
|
172
|
+
console.log(commandHeader("Fleet models"));
|
|
173
|
+
console.log(formatFleetModelsText(catalog));
|
|
174
|
+
}
|
|
175
|
+
return catalog;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const loaded = await loadFleetProfile({ homeDir, seedIfMissing: true });
|
|
179
|
+
const payload = { ok: true, profile: loaded.profile, path: loaded.path, catalog };
|
|
180
|
+
if (json) printJson(payload);
|
|
181
|
+
else {
|
|
182
|
+
console.log(commandHeader("Fleet models"));
|
|
183
|
+
console.log(formatFleetProfileText(loaded.profile, { path: loaded.path }));
|
|
184
|
+
console.log("");
|
|
185
|
+
console.log(formatFleetModelsText(catalog));
|
|
186
|
+
}
|
|
187
|
+
return payload;
|
|
188
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent-gated fleet model writes (OpenCode / Claude / Codex).
|
|
3
|
+
* Plan by default; --yes applies with backup. Never touches Cursor Auto / state.vscdb.
|
|
4
|
+
*/
|
|
5
|
+
import { copyFile, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { resolveHomeDir } from "./paths.js";
|
|
8
|
+
import { writeAtomicJson } from "./runtime/write-atomic-json.js";
|
|
9
|
+
import { printJson } from "./json-output.js";
|
|
10
|
+
import { commandHeader } from "./brand/index.js";
|
|
11
|
+
import { formatCliCommand } from "./brand/cli.js";
|
|
12
|
+
import {
|
|
13
|
+
parseFrontmatterModel,
|
|
14
|
+
replaceFrontmatterModel,
|
|
15
|
+
replaceCodexDefaultModel
|
|
16
|
+
} from "./observability/fleet-platforms.js";
|
|
17
|
+
|
|
18
|
+
const PLATFORMS = new Set(["opencode", "claude", "codex"]);
|
|
19
|
+
|
|
20
|
+
function backupPath(path, stamp = Date.now()) {
|
|
21
|
+
return `${path}.kairo-backup.${stamp}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requirePlatform(platform) {
|
|
25
|
+
const p = String(platform ?? "").toLowerCase();
|
|
26
|
+
if (!PLATFORMS.has(p)) {
|
|
27
|
+
throw new Error(`Unsupported platform "${platform}". Use opencode, claude, or codex.`);
|
|
28
|
+
}
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function requireAgent(agent) {
|
|
33
|
+
const id = String(agent ?? "").trim();
|
|
34
|
+
if (!id) throw new Error("Missing --agent <id>.");
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireModel(model) {
|
|
39
|
+
const m = String(model ?? "").trim();
|
|
40
|
+
if (!m) throw new Error("Missing --model <id>.");
|
|
41
|
+
return m;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildOpenCodeFleetSetPlan({ config, agent, model, configPath }) {
|
|
45
|
+
const agents = config?.agent ?? config?.agents;
|
|
46
|
+
if (!agents || typeof agents !== "object" || !(agent in agents)) {
|
|
47
|
+
throw new Error(`OpenCode agent "${agent}" not found in ${configPath}.`);
|
|
48
|
+
}
|
|
49
|
+
const prev = agents[agent];
|
|
50
|
+
const previousModel = typeof prev?.model === "string" ? prev.model : (config?.model ?? null);
|
|
51
|
+
const nextAgents = {
|
|
52
|
+
...agents,
|
|
53
|
+
[agent]: { ...prev, model }
|
|
54
|
+
};
|
|
55
|
+
const next = { ...config, agent: nextAgents };
|
|
56
|
+
if (config.agents && !config.agent) {
|
|
57
|
+
delete next.agent;
|
|
58
|
+
next.agents = nextAgents;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
platform: "opencode",
|
|
62
|
+
agent,
|
|
63
|
+
model,
|
|
64
|
+
previousModel,
|
|
65
|
+
path: configPath,
|
|
66
|
+
wouldWrite: previousModel !== model,
|
|
67
|
+
next,
|
|
68
|
+
note: `Set OpenCode agent.${agent}.model → ${model}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function runFleetSet({
|
|
73
|
+
platform,
|
|
74
|
+
agent,
|
|
75
|
+
model,
|
|
76
|
+
yes = false,
|
|
77
|
+
json = false,
|
|
78
|
+
dryRun = false,
|
|
79
|
+
homeDir = resolveHomeDir(),
|
|
80
|
+
read = readFile,
|
|
81
|
+
writeText = writeFile,
|
|
82
|
+
copyFileFn = copyFile,
|
|
83
|
+
writeAtomicJsonFn = writeAtomicJson,
|
|
84
|
+
now = () => Date.now()
|
|
85
|
+
} = {}) {
|
|
86
|
+
const p = requirePlatform(platform);
|
|
87
|
+
const nextModel = requireModel(model);
|
|
88
|
+
const agentId = p === "codex"
|
|
89
|
+
? (String(agent ?? "default").trim() || "default")
|
|
90
|
+
: requireAgent(agent);
|
|
91
|
+
const apply = yes === true && dryRun !== true;
|
|
92
|
+
|
|
93
|
+
let plan;
|
|
94
|
+
let path;
|
|
95
|
+
let applyFn;
|
|
96
|
+
|
|
97
|
+
if (p === "opencode") {
|
|
98
|
+
path = join(homeDir, ".config", "opencode", "opencode.json");
|
|
99
|
+
const raw = await read(path, "utf8");
|
|
100
|
+
const config = JSON.parse(raw);
|
|
101
|
+
plan = buildOpenCodeFleetSetPlan({
|
|
102
|
+
config, agent: agentId, model: nextModel, configPath: path
|
|
103
|
+
});
|
|
104
|
+
applyFn = async () => {
|
|
105
|
+
await writeAtomicJsonFn(path, plan.next);
|
|
106
|
+
};
|
|
107
|
+
} else if (p === "claude") {
|
|
108
|
+
if (agentId === "default") {
|
|
109
|
+
path = join(homeDir, ".claude", "settings.json");
|
|
110
|
+
const existing = JSON.parse(await read(path, "utf8"));
|
|
111
|
+
const previousModel = typeof existing.model === "string" ? existing.model : null;
|
|
112
|
+
plan = {
|
|
113
|
+
platform: "claude",
|
|
114
|
+
agent: agentId,
|
|
115
|
+
model: nextModel,
|
|
116
|
+
previousModel,
|
|
117
|
+
path,
|
|
118
|
+
wouldWrite: previousModel !== nextModel,
|
|
119
|
+
next: { ...existing, model: nextModel },
|
|
120
|
+
note: `Set Claude settings.model → ${nextModel}`
|
|
121
|
+
};
|
|
122
|
+
applyFn = async () => {
|
|
123
|
+
await writeAtomicJsonFn(path, plan.next);
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
path = join(homeDir, ".claude", "agents", `${agentId}.md`);
|
|
127
|
+
const raw = await read(path, "utf8");
|
|
128
|
+
const previousModel = parseFrontmatterModel(raw).model;
|
|
129
|
+
const nextText = replaceFrontmatterModel(raw, nextModel);
|
|
130
|
+
plan = {
|
|
131
|
+
platform: "claude",
|
|
132
|
+
agent: agentId,
|
|
133
|
+
model: nextModel,
|
|
134
|
+
previousModel,
|
|
135
|
+
path,
|
|
136
|
+
wouldWrite: previousModel !== nextModel,
|
|
137
|
+
nextText,
|
|
138
|
+
note: `Set Claude agent ${agentId} frontmatter model → ${nextModel}`
|
|
139
|
+
};
|
|
140
|
+
applyFn = async () => {
|
|
141
|
+
await writeText(path, plan.nextText, "utf8");
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
path = join(homeDir, ".codex", "config.toml");
|
|
146
|
+
const raw = await read(path, "utf8");
|
|
147
|
+
const previousMatch = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
|
|
148
|
+
const previousModel = previousMatch ? previousMatch[1] : null;
|
|
149
|
+
const nextText = replaceCodexDefaultModel(raw, nextModel);
|
|
150
|
+
plan = {
|
|
151
|
+
platform: "codex",
|
|
152
|
+
agent: "default",
|
|
153
|
+
model: nextModel,
|
|
154
|
+
previousModel,
|
|
155
|
+
path,
|
|
156
|
+
wouldWrite: previousModel !== nextModel,
|
|
157
|
+
nextText,
|
|
158
|
+
note: `Set Codex config.toml model → ${nextModel}`
|
|
159
|
+
};
|
|
160
|
+
applyFn = async () => {
|
|
161
|
+
await writeText(path, plan.nextText, "utf8");
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
plan.backupPath = backupPath(path, now());
|
|
166
|
+
plan.applyWith = formatCliCommand(
|
|
167
|
+
`fleet set --platform ${p} --agent ${plan.agent} --model ${nextModel} --yes`
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (!apply) {
|
|
171
|
+
const payload = {
|
|
172
|
+
ok: true,
|
|
173
|
+
applied: false,
|
|
174
|
+
plan: {
|
|
175
|
+
platform: plan.platform,
|
|
176
|
+
agent: plan.agent,
|
|
177
|
+
model: plan.model,
|
|
178
|
+
previousModel: plan.previousModel,
|
|
179
|
+
path: plan.path,
|
|
180
|
+
wouldWrite: plan.wouldWrite,
|
|
181
|
+
note: plan.note,
|
|
182
|
+
applyWith: plan.applyWith
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if (json) printJson(payload);
|
|
186
|
+
else {
|
|
187
|
+
console.log(commandHeader("Fleet set"));
|
|
188
|
+
console.log(plan.note);
|
|
189
|
+
console.log(`Path · ${plan.path}`);
|
|
190
|
+
console.log(`Was · ${plan.previousModel ?? "—"}`);
|
|
191
|
+
console.log(`Now · ${plan.model}`);
|
|
192
|
+
console.log(`Apply · ${plan.applyWith}`);
|
|
193
|
+
}
|
|
194
|
+
return payload;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await copyFileFn(path, plan.backupPath);
|
|
198
|
+
await applyFn();
|
|
199
|
+
|
|
200
|
+
const receipt = {
|
|
201
|
+
ok: true,
|
|
202
|
+
applied: true,
|
|
203
|
+
platform: plan.platform,
|
|
204
|
+
agent: plan.agent,
|
|
205
|
+
model: plan.model,
|
|
206
|
+
previousModel: plan.previousModel,
|
|
207
|
+
path: plan.path,
|
|
208
|
+
backupPath: plan.backupPath,
|
|
209
|
+
note: "Model assignment updated. Refresh Kairo Fleet to see declared changes."
|
|
210
|
+
};
|
|
211
|
+
if (json) printJson(receipt);
|
|
212
|
+
else {
|
|
213
|
+
console.log(commandHeader("Fleet set"));
|
|
214
|
+
console.log(`Wrote · ${receipt.path}`);
|
|
215
|
+
console.log(`Backup · ${receipt.backupPath}`);
|
|
216
|
+
console.log(receipt.note);
|
|
217
|
+
}
|
|
218
|
+
return receipt;
|
|
219
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fleet constants + Gentle assignment loader (no circular imports).
|
|
3
|
+
*/
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
export const SDD_PHASES = Object.freeze([
|
|
8
|
+
"sdd-apply", "sdd-archive", "sdd-design", "sdd-explore", "sdd-init",
|
|
9
|
+
"sdd-onboard", "sdd-propose", "sdd-spec", "sdd-tasks", "sdd-verify"
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/** Map Gentle/Claude tier names → OpenCode provider/model ids. */
|
|
13
|
+
export const CLAUDE_TO_OPENCODE = Object.freeze({
|
|
14
|
+
opus: "opencode-go/deepseek-v4-pro",
|
|
15
|
+
sonnet: "opencode-go/qwen3.5-plus",
|
|
16
|
+
haiku: "opencode-go/deepseek-v4-flash"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export async function loadGentleClaudeAssignments(homeDir, read = readFile) {
|
|
20
|
+
try {
|
|
21
|
+
const state = JSON.parse(await read(join(homeDir, ".gentle-ai", "state.json"), "utf8"));
|
|
22
|
+
const map = state?.claude_model_assignments;
|
|
23
|
+
if (!map || typeof map !== "object") return null;
|
|
24
|
+
return { ...map };
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mapClaudeAssignmentsToOpenCode(assignments = {}) {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [agent, tier] of Object.entries(assignments)) {
|
|
33
|
+
if (agent === "default" || agent === "codex_default") continue;
|
|
34
|
+
const key = String(tier ?? "").toLowerCase();
|
|
35
|
+
out[agent] = CLAUDE_TO_OPENCODE[key] ?? CLAUDE_TO_OPENCODE.sonnet;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
export function createCockpitUiState({
|
|
30
30
|
layoutMode = LAYOUT_MODES.COMPACT,
|
|
31
31
|
view = ORCHESTRATOR_VIEWS.HOME,
|
|
32
|
-
region =
|
|
32
|
+
region = null,
|
|
33
33
|
navIndex = 0,
|
|
34
34
|
listIndex = 0,
|
|
35
35
|
helpOpen = false,
|
|
@@ -122,9 +122,7 @@ export function reduceCockpitUi(state, action) {
|
|
|
122
122
|
if (state.region === COCKPIT_REGIONS.SYSTEM) return state;
|
|
123
123
|
|
|
124
124
|
const navigatesNav = state.region === COCKPIT_REGIONS.NAV
|
|
125
|
-
|| isNavFocusedView(state.view)
|
|
126
|
-
|| (state.view === ORCHESTRATOR_VIEWS.HOME
|
|
127
|
-
&& state.layoutMode === LAYOUT_MODES.MINIMAL);
|
|
125
|
+
|| isNavFocusedView(state.view);
|
|
128
126
|
|
|
129
127
|
if (navigatesNav) {
|
|
130
128
|
const delta = action.direction === "up" ? -1 : 1;
|
|
@@ -11,7 +11,9 @@ export function resolveEnterNavIntent({
|
|
|
11
11
|
} = {}) {
|
|
12
12
|
if (!navItem) return { kind: "noop" };
|
|
13
13
|
|
|
14
|
-
const isOverview = navItem.id === "
|
|
14
|
+
const isOverview = navItem.id === "home"
|
|
15
|
+
|| navItem.id === "overview"
|
|
16
|
+
|| navItem.view === ORCHESTRATOR_VIEWS.HOME;
|
|
15
17
|
if (isOverview) {
|
|
16
18
|
if (currentView === ORCHESTRATOR_VIEWS.HOME && ctaDestination) {
|
|
17
19
|
if (ctaDestination === "setup") return { kind: "activate-setup" };
|
|
@@ -3,7 +3,6 @@ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
|
3
3
|
import { LAYOUT_MODES } from "./layout.js";
|
|
4
4
|
|
|
5
5
|
const NAV_FOCUSED_VIEWS = new Set([
|
|
6
|
-
ORCHESTRATOR_VIEWS.HOME,
|
|
7
6
|
ORCHESTRATOR_VIEWS.IDES,
|
|
8
7
|
ORCHESTRATOR_VIEWS.MODULES,
|
|
9
8
|
ORCHESTRATOR_VIEWS.CHANGES,
|
|
@@ -14,6 +13,7 @@ const NAV_FOCUSED_VIEWS = new Set([
|
|
|
14
13
|
]);
|
|
15
14
|
|
|
16
15
|
const CONTENT_INTERACTIVE_VIEWS = new Set([
|
|
16
|
+
ORCHESTRATOR_VIEWS.HOME,
|
|
17
17
|
ORCHESTRATOR_VIEWS.RUNS,
|
|
18
18
|
ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
|
|
19
19
|
ORCHESTRATOR_VIEWS.RECENT_RUNS,
|
|
@@ -102,6 +102,12 @@ export function routeCockpitKey(state, keyAction) {
|
|
|
102
102
|
if (state.region === COCKPIT_REGIONS.NAV || isNavFocusedView(state.view)) {
|
|
103
103
|
return { type: "enter-nav" };
|
|
104
104
|
}
|
|
105
|
+
if (
|
|
106
|
+
state.region === COCKPIT_REGIONS.CONTENT
|
|
107
|
+
&& state.view === ORCHESTRATOR_VIEWS.HOME
|
|
108
|
+
) {
|
|
109
|
+
return { type: "enter-home-button" };
|
|
110
|
+
}
|
|
105
111
|
return null;
|
|
106
112
|
default:
|
|
107
113
|
return null;
|
|
@@ -17,23 +17,33 @@ export const COCKPIT_REGIONS = {
|
|
|
17
17
|
|
|
18
18
|
export const COCKPIT_NAV = [
|
|
19
19
|
{
|
|
20
|
-
id: "
|
|
21
|
-
label: "
|
|
20
|
+
id: "home",
|
|
21
|
+
label: "Home",
|
|
22
22
|
view: ORCHESTRATOR_VIEWS.HOME,
|
|
23
|
-
description: "
|
|
23
|
+
description: "What needs you, and what to do about it."
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
id: "settings",
|
|
27
|
+
label: "Settings",
|
|
28
|
+
view: ORCHESTRATOR_VIEWS.PROFILE,
|
|
29
|
+
description: "Choose agents, connect Obsidian, add integrations."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "history",
|
|
33
|
+
label: "History",
|
|
34
|
+
view: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
35
|
+
description: "What Kairo changed, and how to undo it."
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Destinations reachable via the action palette, not the top nav. */
|
|
40
|
+
export const COCKPIT_SECONDARY = [
|
|
25
41
|
{
|
|
26
42
|
id: "governance",
|
|
27
43
|
label: "Governance",
|
|
28
44
|
view: ORCHESTRATOR_VIEWS.CHANGES,
|
|
29
45
|
description: "Repair drift and apply governed changes."
|
|
30
46
|
},
|
|
31
|
-
{
|
|
32
|
-
id: "activity",
|
|
33
|
-
label: "Activity",
|
|
34
|
-
view: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
35
|
-
description: "Operations, backups, and recovery."
|
|
36
|
-
},
|
|
37
47
|
{
|
|
38
48
|
id: "orchestration",
|
|
39
49
|
label: "Orchestration",
|
|
@@ -45,12 +55,6 @@ export const COCKPIT_NAV = [
|
|
|
45
55
|
label: "Usage",
|
|
46
56
|
view: ORCHESTRATOR_VIEWS.USAGE,
|
|
47
57
|
description: "Token and context pressure when auditable."
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: "settings",
|
|
51
|
-
label: "Settings",
|
|
52
|
-
view: ORCHESTRATOR_VIEWS.PROFILE,
|
|
53
|
-
description: "Profile, policy, and guided configuration."
|
|
54
58
|
}
|
|
55
59
|
];
|
|
56
60
|
|
|
@@ -64,8 +68,8 @@ export function regionsForLayout(layoutMode) {
|
|
|
64
68
|
|
|
65
69
|
export function navIndexForView(view, items = COCKPIT_NAV) {
|
|
66
70
|
if (isRunsBranchView(view)) {
|
|
67
|
-
const
|
|
68
|
-
return
|
|
71
|
+
const historyIndex = items.findIndex((item) => item.id === "history");
|
|
72
|
+
return historyIndex >= 0 ? historyIndex : 0;
|
|
69
73
|
}
|
|
70
74
|
const index = items.findIndex((item) => item.view === view);
|
|
71
75
|
return index >= 0 ? index : 0;
|
|
@@ -105,6 +109,7 @@ export function resolveNavStatusSummary(item, {
|
|
|
105
109
|
const backups = snapshot?.backups?.count ?? 0;
|
|
106
110
|
|
|
107
111
|
switch (item.id) {
|
|
112
|
+
case "home":
|
|
108
113
|
case "overview":
|
|
109
114
|
return snapshot?.health?.replaceAll("_", " ")
|
|
110
115
|
?? resolveProjectReadiness({
|
|
@@ -114,6 +119,7 @@ export function resolveNavStatusSummary(item, {
|
|
|
114
119
|
}).label;
|
|
115
120
|
case "governance":
|
|
116
121
|
return changes > 0 ? `${changes} pending` : "Clean";
|
|
122
|
+
case "history":
|
|
117
123
|
case "activity":
|
|
118
124
|
return backups > 0 ? `${backups} backups` : "No backups";
|
|
119
125
|
case "orchestration":
|
|
@@ -174,6 +180,7 @@ export function buildNavModel({
|
|
|
174
180
|
const mapped = items.map((item, index) => {
|
|
175
181
|
const isSelected = index === navIndex;
|
|
176
182
|
const isCurrent = item.view === currentView
|
|
183
|
+
|| (item.id === "history" && isRunsBranchView(currentView))
|
|
177
184
|
|| (item.id === "orchestration" && isRunsBranchView(currentView));
|
|
178
185
|
return {
|
|
179
186
|
...item,
|
|
@@ -185,11 +192,13 @@ export function buildNavModel({
|
|
|
185
192
|
};
|
|
186
193
|
});
|
|
187
194
|
|
|
195
|
+
const explanation = !selected || selected.id === "home" || selected.id === "overview"
|
|
196
|
+
? ""
|
|
197
|
+
: (selected.description ?? "");
|
|
198
|
+
|
|
188
199
|
return {
|
|
189
200
|
title: "NAVIGATION",
|
|
190
|
-
explanation
|
|
191
|
-
? `${selected.description} (${resolveNavStatusSummary(selected, { dashboard, diagnostics, snapshot })})`
|
|
192
|
-
: "",
|
|
201
|
+
explanation,
|
|
193
202
|
items: mapped
|
|
194
203
|
};
|
|
195
204
|
}
|
|
@@ -319,8 +328,11 @@ export function buildFooterModel({
|
|
|
319
328
|
|
|
320
329
|
// HOME footer must stay on one line at 80 cols (frame already near 24 rows).
|
|
321
330
|
if (view === ORCHESTRATOR_VIEWS.HOME) {
|
|
331
|
+
const homeParts = region === COCKPIT_REGIONS.NAV
|
|
332
|
+
? ["↑↓ Section", "Enter Open", "? Help", "Esc Exit"]
|
|
333
|
+
: ["1·2 Select", "Enter Run", "? Help", "Esc Exit"];
|
|
322
334
|
return {
|
|
323
|
-
text:
|
|
335
|
+
text: homeParts.join(` ${glyphs.bullet} `),
|
|
324
336
|
columns: footerColumns
|
|
325
337
|
};
|
|
326
338
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { COCKPIT_NAV } from "./cockpit-models.js";
|
|
1
|
+
import { COCKPIT_NAV, COCKPIT_SECONDARY } from "./cockpit-models.js";
|
|
2
2
|
import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
|
|
3
3
|
|
|
4
|
+
const PALETTE_NAV_ITEMS = [...COCKPIT_NAV, ...COCKPIT_SECONDARY];
|
|
5
|
+
|
|
4
6
|
export const PALETTE_KINDS = Object.freeze({
|
|
5
7
|
NAVIGATE: "navigate",
|
|
6
8
|
REFRESH: "refresh",
|
|
@@ -12,10 +14,13 @@ const DESTINATION_VIEWS = Object.freeze({
|
|
|
12
14
|
changes: ORCHESTRATOR_VIEWS.CHANGES,
|
|
13
15
|
governance: ORCHESTRATOR_VIEWS.CHANGES,
|
|
14
16
|
"control-center": ORCHESTRATOR_VIEWS.HOME,
|
|
17
|
+
home: ORCHESTRATOR_VIEWS.HOME,
|
|
15
18
|
ides: ORCHESTRATOR_VIEWS.IDES,
|
|
16
19
|
modules: ORCHESTRATOR_VIEWS.MODULES,
|
|
17
20
|
activity: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
21
|
+
history: ORCHESTRATOR_VIEWS.ACTIVITY,
|
|
18
22
|
profile: ORCHESTRATOR_VIEWS.PROFILE,
|
|
23
|
+
settings: ORCHESTRATOR_VIEWS.PROFILE,
|
|
19
24
|
runs: ORCHESTRATOR_VIEWS.RUNS,
|
|
20
25
|
orchestration: ORCHESTRATOR_VIEWS.RUNS,
|
|
21
26
|
usage: ORCHESTRATOR_VIEWS.USAGE,
|
|
@@ -30,12 +35,12 @@ export function canOpenPalette({ loading = false, busy = false, confirming = fal
|
|
|
30
35
|
return !loading && !busy && !confirming;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
/** Optional CTA +
|
|
38
|
+
/** Optional CTA + primary/secondary destinations + Refresh + Help. No write shortcuts. */
|
|
34
39
|
export function buildPaletteActions({
|
|
35
40
|
ctaDestination = null,
|
|
36
41
|
ctaTitle = null,
|
|
37
42
|
ctaDetail = null,
|
|
38
|
-
navItems =
|
|
43
|
+
navItems = PALETTE_NAV_ITEMS
|
|
39
44
|
} = {}) {
|
|
40
45
|
const actions = [];
|
|
41
46
|
if (ctaDestination === "setup") {
|
|
@@ -100,7 +100,8 @@ export function renderCockpitView({
|
|
|
100
100
|
governanceDetailsOpen = false,
|
|
101
101
|
activityDetailsOpen = false,
|
|
102
102
|
contentFocused = false,
|
|
103
|
-
homeDir = null
|
|
103
|
+
homeDir = null,
|
|
104
|
+
hasGlobalState = false
|
|
104
105
|
}) {
|
|
105
106
|
if (palette) {
|
|
106
107
|
return React.createElement(PalettePanel, { model: palette, colorEnabled });
|
|
@@ -112,7 +113,13 @@ export function renderCockpitView({
|
|
|
112
113
|
detailsOpen: overviewDetailsOpen,
|
|
113
114
|
colorEnabled,
|
|
114
115
|
unicode,
|
|
115
|
-
layoutMode
|
|
116
|
+
layoutMode,
|
|
117
|
+
selectedIndex: listIndex,
|
|
118
|
+
contentFocused,
|
|
119
|
+
hasGlobalState,
|
|
120
|
+
snapshot,
|
|
121
|
+
diagnostics,
|
|
122
|
+
dashboard
|
|
116
123
|
});
|
|
117
124
|
case ORCHESTRATOR_VIEWS.USAGE:
|
|
118
125
|
return React.createElement(SemanticUsagePanel, {
|