@kal-elsam/kairo-runtime 0.14.0 → 0.16.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 +110 -0
- package/bin/kairo-runtime.js +0 -0
- package/bin/kairo.js +0 -0
- package/package.json +1 -1
- package/src/cli.js +182 -8
- package/src/global/check-resolutions.js +31 -0
- package/src/global/cli-help.js +21 -2
- 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/control-plane/attention.js +141 -0
- package/src/global/control-plane/build-report.js +146 -0
- package/src/global/control-plane/cli.js +36 -0
- package/src/global/control-plane/constants.js +38 -0
- package/src/global/control-plane/gentle-adapters.js +183 -0
- package/src/global/control-plane/provider.js +69 -0
- package/src/global/control-plane/review-status.js +115 -0
- package/src/global/control-plane/sdd-status.js +49 -0
- package/src/global/control-plane/team.js +63 -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 +1 -1
- package/src/global/ink/cockpit-models.js +4 -1
- package/src/global/ink/orchestrator-app.js +21 -2
- package/src/global/ink/ux/live-overview.js +5 -11
- 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/observability/gentle-probe.js +30 -2
- package/src/global/observability/index.js +2 -1
- package/src/global/paths.js +2 -1
|
@@ -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
|
+
}
|
|
@@ -328,8 +328,11 @@ export function buildFooterModel({
|
|
|
328
328
|
|
|
329
329
|
// HOME footer must stay on one line at 80 cols (frame already near 24 rows).
|
|
330
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"];
|
|
331
334
|
return {
|
|
332
|
-
text:
|
|
335
|
+
text: homeParts.join(` ${glyphs.bullet} `),
|
|
333
336
|
columns: footerColumns
|
|
334
337
|
};
|
|
335
338
|
}
|
|
@@ -63,8 +63,7 @@ export function OrchestratorApp({
|
|
|
63
63
|
const [ui, dispatch] = useReducer(
|
|
64
64
|
reduceCockpitUi,
|
|
65
65
|
createCockpitUiState({
|
|
66
|
-
layoutMode: layoutMode ?? LAYOUT_MODES.COMPACT
|
|
67
|
-
region: COCKPIT_REGIONS.NAV
|
|
66
|
+
layoutMode: layoutMode ?? LAYOUT_MODES.COMPACT
|
|
68
67
|
})
|
|
69
68
|
);
|
|
70
69
|
const data = useOrchestratorData({
|
|
@@ -166,6 +165,26 @@ export function OrchestratorApp({
|
|
|
166
165
|
return;
|
|
167
166
|
}
|
|
168
167
|
|
|
168
|
+
if (ui.view === ORCHESTRATOR_VIEWS.HOME && !ui.paletteOpen) {
|
|
169
|
+
const digit = inputKey === "1" ? 0 : inputKey === "2" ? 1 : -1;
|
|
170
|
+
if (digit >= 0) {
|
|
171
|
+
const buttons = buildOverviewButtons({
|
|
172
|
+
hasGlobalState,
|
|
173
|
+
snapshot: data.snapshot,
|
|
174
|
+
diagnostics: data.diagnostics,
|
|
175
|
+
dashboard: data.dashboard
|
|
176
|
+
});
|
|
177
|
+
const selected = buttons[digit];
|
|
178
|
+
const intent = selected?.intent ?? null;
|
|
179
|
+
if (intent === "setup") {
|
|
180
|
+
finish({ cancelled: false, action: "setup" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (intent && openDestination(intent)) return;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
169
188
|
if (inputKey === " " && ui.view === ORCHESTRATOR_VIEWS.HOME && !ui.paletteOpen) {
|
|
170
189
|
dispatch({ type: "toggle-overview-details" });
|
|
171
190
|
return;
|
|
@@ -94,12 +94,6 @@ export function adaptControlCenterToOverview(model = {}, options = {}) {
|
|
|
94
94
|
label: `Alerts · ${model.alerts.headline ?? `${model.alerts.count} open`}`
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
-
if (rest.length > 0) {
|
|
98
|
-
metrics.push({
|
|
99
|
-
id: "more",
|
|
100
|
-
label: `${rest.length} more in Details · Space`
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
97
|
if (metrics.length === 0) {
|
|
104
98
|
metrics.push({ id: "quiet", label: "Nothing else needs you right now" });
|
|
105
99
|
}
|
|
@@ -201,13 +195,13 @@ export function SemanticOverviewPanel({
|
|
|
201
195
|
marginBottom: index === view.buttons.length - 1 ? 0 : 1
|
|
202
196
|
},
|
|
203
197
|
React.createElement(Text, {
|
|
204
|
-
bold:
|
|
205
|
-
color:
|
|
206
|
-
}, `${
|
|
198
|
+
bold: selected,
|
|
199
|
+
color: colorEnabled && selected ? COCKPIT_COLORS.interactive : undefined
|
|
200
|
+
}, `${selected ? glyphs.focus : " "} [${index + 1}] ${button.label}${focused ? " ← Press Enter" : ""}`),
|
|
207
201
|
button.detail
|
|
208
202
|
? React.createElement(Text, {
|
|
209
203
|
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
210
|
-
}, `
|
|
204
|
+
}, ` ${button.detail}`)
|
|
211
205
|
: null
|
|
212
206
|
);
|
|
213
207
|
})
|
|
@@ -223,7 +217,7 @@ export function SemanticOverviewPanel({
|
|
|
223
217
|
),
|
|
224
218
|
React.createElement(Details, {
|
|
225
219
|
open: detailsOpen,
|
|
226
|
-
summary:
|
|
220
|
+
summary: `More info (${view.details.length})`,
|
|
227
221
|
lines: view.details,
|
|
228
222
|
colorEnabled,
|
|
229
223
|
focused: false,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { CONTROL_PLANE_HEALTH } from "../../control-plane-snapshot.js";
|
|
5
5
|
import { formatCliCommand } from "../../brand/cli.js";
|
|
6
6
|
|
|
7
|
-
export const OVERVIEW_NEED_LIMIT =
|
|
7
|
+
export const OVERVIEW_NEED_LIMIT = 2;
|
|
8
8
|
|
|
9
9
|
/** Prefixes that never belong on the first screen (machine / internals). */
|
|
10
10
|
export const DETAILS_ONLY_PREFIXES = [
|
|
@@ -215,12 +215,17 @@ function jsonKeyEvidence(path, keyPath, kind) {
|
|
|
215
215
|
try {
|
|
216
216
|
let cursor = JSON.parse(readFileSync(path, "utf8"));
|
|
217
217
|
for (const key of keyPath) {
|
|
218
|
-
if (cursor == null || typeof cursor !== "object") {
|
|
218
|
+
if (cursor == null || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
219
219
|
return { path, kind, present: false, conflict: true, keyPath, detail: "invalid structure" };
|
|
220
220
|
}
|
|
221
|
+
// Missing key = unconfigured evidence, not a conflict (file may use another MCP path).
|
|
222
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, key)) {
|
|
223
|
+
return { path, kind, present: false, conflict: false, keyPath };
|
|
224
|
+
}
|
|
221
225
|
cursor = cursor[key];
|
|
222
226
|
}
|
|
223
|
-
|
|
227
|
+
const present = cursor != null && typeof cursor === "object" && !Array.isArray(cursor);
|
|
228
|
+
return { path, kind, present, conflict: false, keyPath };
|
|
224
229
|
} catch {
|
|
225
230
|
return { path, kind, present: false, conflict: true, keyPath, detail: "unreadable json" };
|
|
226
231
|
}
|
|
@@ -17,7 +17,8 @@ const APPLYING_ACTIONS = new Set([SDD_PLAN_ACTIONS.CREATE, SDD_PLAN_ACTIONS.UPDA
|
|
|
17
17
|
|
|
18
18
|
export async function applySddConfigure({
|
|
19
19
|
requestedAgentIds = null, detectedAgentIds = [], homeDir, packageRoot, persona = "off",
|
|
20
|
-
personaAgentIds = [], trackedFiles = {},
|
|
20
|
+
personaAgentIds = [], trackedFiles = {}, adoptedFiles = {}, overwriteConflicts = false,
|
|
21
|
+
preservePersona = false, dryRun = false, yes = false,
|
|
21
22
|
json = false, interactive = null, receiptId = null, plan = planSddConfigure,
|
|
22
23
|
confirm = promptApplyConfirmation, saveReceipt = saveSddReceipt,
|
|
23
24
|
now = () => new Date().toISOString()
|
|
@@ -28,14 +29,16 @@ export async function applySddConfigure({
|
|
|
28
29
|
|
|
29
30
|
const planned = await plan({
|
|
30
31
|
requestedAgentIds, detectedAgentIds, homeDir, packageRoot, persona, personaAgentIds,
|
|
31
|
-
trackedFiles, preservePersona, dryRun: true
|
|
32
|
+
trackedFiles, adoptedFiles, overwriteConflicts, preservePersona, dryRun: true
|
|
32
33
|
});
|
|
33
34
|
if (dryRun) return { ...planned, applied: false, cancelled: false, receipt: null };
|
|
34
35
|
|
|
35
36
|
if (shouldPromptApplyConfirmation({ applying: true, dryRun, json, confirm: yes, interactive })) {
|
|
36
37
|
const accepted = await confirm({
|
|
37
38
|
command: "components configure sdd-core",
|
|
38
|
-
question:
|
|
39
|
+
question: overwriteConflicts
|
|
40
|
+
? "Overwrite conflicting SDD skills with canonical Kairo copies (backups first)? [Y/n]: "
|
|
41
|
+
: "Materialize SDD skills for the planned agents? [Y/n]: "
|
|
39
42
|
});
|
|
40
43
|
if (!accepted) return { ...planned, applied: false, cancelled: true, receipt: null };
|
|
41
44
|
}
|
|
@@ -66,7 +69,8 @@ export async function applySddConfigure({
|
|
|
66
69
|
try {
|
|
67
70
|
const managedRoot = resolveSddSkillRoot(action.agentIds[0], homeDir);
|
|
68
71
|
const outcome = await materializeOne(action, {
|
|
69
|
-
homeDir, packageRoot, managedRoot, receiptId: resolvedReceiptId
|
|
72
|
+
homeDir, packageRoot, managedRoot, receiptId: resolvedReceiptId,
|
|
73
|
+
overwriteConflicts: Boolean(action.overwrote || overwriteConflicts)
|
|
70
74
|
});
|
|
71
75
|
if (outcome.conflict) {
|
|
72
76
|
files.push({
|
|
@@ -78,7 +82,8 @@ export async function applySddConfigure({
|
|
|
78
82
|
if (outcome.backup) backups.push(outcome.backup);
|
|
79
83
|
files.push({
|
|
80
84
|
...record, applied: true, skipped: false, outcome: SDD_FILE_OUTCOMES.APPLIED,
|
|
81
|
-
afterHash: outcome.afterHash, parentRealpath: outcome.parentRealpath
|
|
85
|
+
afterHash: outcome.afterHash, parentRealpath: outcome.parentRealpath,
|
|
86
|
+
overwrote: Boolean(action.overwrote)
|
|
82
87
|
});
|
|
83
88
|
} catch (error) {
|
|
84
89
|
failed = { skillId: action.skillId, destinationPath: action.destinationPath, error: error.message };
|
|
@@ -141,7 +146,9 @@ async function readCanonicalBytes(action, packageRoot) {
|
|
|
141
146
|
));
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
async function materializeOne(action, {
|
|
149
|
+
async function materializeOne(action, {
|
|
150
|
+
homeDir, packageRoot, managedRoot, receiptId, overwriteConflicts = false
|
|
151
|
+
}) {
|
|
145
152
|
const chain = await assertSafePathChain(action.destinationPath, managedRoot, homeDir);
|
|
146
153
|
if (!chain.ok) return { conflict: chain.reason };
|
|
147
154
|
|
|
@@ -169,7 +176,10 @@ async function materializeOne(action, { homeDir, packageRoot, managedRoot, recei
|
|
|
169
176
|
return { conflict: "Managed destination disappeared after planning; preserving byte-for-byte." };
|
|
170
177
|
}
|
|
171
178
|
const snap = await snapshotRegularFile(action.destinationPath);
|
|
172
|
-
|
|
179
|
+
const expectedHash = overwriteConflicts || action.overwrote
|
|
180
|
+
? snap.hash
|
|
181
|
+
: action.trackedHash;
|
|
182
|
+
if (snap.hash !== expectedHash) {
|
|
173
183
|
return { conflict: "Managed file changed after planning; preserving byte-for-byte." };
|
|
174
184
|
}
|
|
175
185
|
const parent = await parentRealpath(action.destinationPath);
|