@kal-elsam/kairo-runtime 0.14.0 → 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 +84 -0
- package/bin/kairo-runtime.js +0 -0
- package/bin/kairo.js +0 -0
- package/package.json +1 -1
- package/src/cli.js +172 -8
- package/src/global/check-resolutions.js +31 -0
- package/src/global/cli-help.js +19 -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/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/paths.js +2 -1
|
@@ -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);
|
|
@@ -16,6 +16,7 @@ export const SDD_FILE_OUTCOMES = Object.freeze({
|
|
|
16
16
|
|
|
17
17
|
export const SDD_HEALTH = Object.freeze({
|
|
18
18
|
CONFIGURED: "configured",
|
|
19
|
+
ADOPTED: "adopted",
|
|
19
20
|
MISSING: "missing",
|
|
20
21
|
DRIFTED: "drifted",
|
|
21
22
|
CONFLICT: "conflict"
|
|
@@ -23,13 +24,16 @@ export const SDD_HEALTH = Object.freeze({
|
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Classify one destination file against canonical bytes and optional tracked hash.
|
|
26
|
-
* Untracked or user-modified files are conflicts and must never be overwritten
|
|
27
|
+
* Untracked or user-modified files are conflicts and must never be overwritten
|
|
28
|
+
* unless overwriteConflicts is requested at apply time.
|
|
29
|
+
* adoptedHash (disk content accepted as-is) yields NOOP without claiming managed.
|
|
27
30
|
*/
|
|
28
31
|
export function classifySddSkillFile({
|
|
29
32
|
exists,
|
|
30
33
|
canonicalHash,
|
|
31
34
|
diskHash = null,
|
|
32
|
-
trackedHash = null
|
|
35
|
+
trackedHash = null,
|
|
36
|
+
adoptedHash = null
|
|
33
37
|
} = {}) {
|
|
34
38
|
if (!exists) {
|
|
35
39
|
return {
|
|
@@ -38,6 +42,13 @@ export function classifySddSkillFile({
|
|
|
38
42
|
};
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
if (adoptedHash != null && adoptedHash === diskHash) {
|
|
46
|
+
return {
|
|
47
|
+
action: SDD_PLAN_ACTIONS.NOOP,
|
|
48
|
+
reason: "Adopted disk bytes; preserving byte-for-byte."
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
if (trackedHash == null) {
|
|
42
53
|
return {
|
|
43
54
|
action: SDD_PLAN_ACTIONS.CONFLICT,
|
|
@@ -70,11 +81,19 @@ export function classifySddVerifyHealth({
|
|
|
70
81
|
exists,
|
|
71
82
|
canonicalHash,
|
|
72
83
|
diskHash = null,
|
|
73
|
-
trackedHash = null
|
|
84
|
+
trackedHash = null,
|
|
85
|
+
adoptedHash = null
|
|
74
86
|
} = {}) {
|
|
75
87
|
if (!exists) {
|
|
76
88
|
return { status: SDD_HEALTH.MISSING, drift: null, reason: "Destination missing on disk." };
|
|
77
89
|
}
|
|
90
|
+
if (adoptedHash != null && adoptedHash === diskHash) {
|
|
91
|
+
return {
|
|
92
|
+
status: SDD_HEALTH.ADOPTED,
|
|
93
|
+
drift: null,
|
|
94
|
+
reason: "Disk matches adopted hash (not Kairo-managed)."
|
|
95
|
+
};
|
|
96
|
+
}
|
|
78
97
|
if (trackedHash == null) {
|
|
79
98
|
return { status: SDD_HEALTH.CONFLICT, drift: null, reason: "Pre-existing untracked file." };
|
|
80
99
|
}
|
|
@@ -25,6 +25,8 @@ export async function planSddConfigure({
|
|
|
25
25
|
persona = "off",
|
|
26
26
|
personaAgentIds = [],
|
|
27
27
|
trackedFiles = {},
|
|
28
|
+
adoptedFiles = {},
|
|
29
|
+
overwriteConflicts = false,
|
|
28
30
|
preservePersona = false,
|
|
29
31
|
dryRun = true,
|
|
30
32
|
exists = existsSync,
|
|
@@ -50,16 +52,31 @@ export async function planSddConfigure({
|
|
|
50
52
|
for (const group of destinationGroups) {
|
|
51
53
|
const destinationPath = join(group.root, skillId, ...file.relativePath.split("/"));
|
|
52
54
|
const trackedHash = trackedFiles[destinationPath] ?? null;
|
|
55
|
+
const adoptedHash = adoptedFiles[destinationPath] ?? null;
|
|
53
56
|
const fileExists = exists(destinationPath);
|
|
54
57
|
const diskHash = fileExists ? hashBuffer(await readFileImpl(destinationPath)) : null;
|
|
55
|
-
|
|
56
|
-
exists: fileExists, canonicalHash, diskHash, trackedHash
|
|
58
|
+
let classification = classifySddSkillFile({
|
|
59
|
+
exists: fileExists, canonicalHash, diskHash, trackedHash, adoptedHash
|
|
57
60
|
});
|
|
61
|
+
let overwrote = false;
|
|
62
|
+
if (
|
|
63
|
+
overwriteConflicts
|
|
64
|
+
&& classification.action === SDD_PLAN_ACTIONS.CONFLICT
|
|
65
|
+
&& fileExists
|
|
66
|
+
) {
|
|
67
|
+
classification = {
|
|
68
|
+
action: SDD_PLAN_ACTIONS.UPDATE,
|
|
69
|
+
reason: "Overwrite conflicts requested; backup then replace with canonical."
|
|
70
|
+
};
|
|
71
|
+
overwrote = true;
|
|
72
|
+
}
|
|
58
73
|
actions.push({
|
|
59
74
|
skillId, relativePath: file.relativePath, destinationPath,
|
|
60
75
|
agentIds: [...group.agentIds], kind: group.kind,
|
|
61
76
|
action: classification.action, reason: classification.reason,
|
|
62
|
-
canonicalHash, skillHash, diskHash, trackedHash,
|
|
77
|
+
canonicalHash, skillHash, diskHash, trackedHash, adoptedHash,
|
|
78
|
+
overwrote, overwriteConflicts: Boolean(overwriteConflicts),
|
|
79
|
+
writes: false, executes: false
|
|
63
80
|
});
|
|
64
81
|
}
|
|
65
82
|
}
|
|
@@ -78,6 +95,7 @@ export async function planSddConfigure({
|
|
|
78
95
|
return {
|
|
79
96
|
provider: "sdd-core", componentId: "sdd-core", dryRun: Boolean(dryRun),
|
|
80
97
|
executes: false, writes: false, requestedPersona: persona, preservePersona,
|
|
98
|
+
overwriteConflicts: Boolean(overwriteConflicts),
|
|
81
99
|
persona: personaTransition.persona,
|
|
82
100
|
personaPath: resolveCanonicalTeachingPersonaPath(packageRoot),
|
|
83
101
|
personaActive: personaTransition.after.length > 0, personaTransition, agentIds, actions,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { formatCliCommand } from "../brand/cli.js";
|
|
2
|
+
import { resolution, RESOLUTION_KIND, RESOLUTION_SAFETY } from "../check-resolutions.js";
|
|
3
|
+
import { SDD_HEALTH } from "./sdd-evidence.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build panel/CLI resolution buttons for sdd-core:skills from verify findings.
|
|
7
|
+
* Agent list is derived from conflict/drifted findings so buttons scope correctly.
|
|
8
|
+
*/
|
|
9
|
+
export function buildSddSkillResolutions(verification = {}) {
|
|
10
|
+
const summary = verification.summary ?? {};
|
|
11
|
+
const conflictCount = summary.conflict ?? 0;
|
|
12
|
+
const driftedCount = summary.drifted ?? 0;
|
|
13
|
+
if (conflictCount === 0 && driftedCount === 0) return [];
|
|
14
|
+
|
|
15
|
+
const agentIds = conflictAgentIds(verification.findings ?? []);
|
|
16
|
+
const agentsFlag = agentIds.length ? ` --agents ${agentIds.join(",")}` : "";
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
resolution(
|
|
20
|
+
"sdd-diff",
|
|
21
|
+
"Ver diff",
|
|
22
|
+
formatCliCommand(`components diff sdd-core${agentsFlag}`),
|
|
23
|
+
{
|
|
24
|
+
kind: RESOLUTION_KIND.RUN,
|
|
25
|
+
safety: RESOLUTION_SAFETY.READ_ONLY,
|
|
26
|
+
detail: "Read-only: canonical Kairo skills vs disk."
|
|
27
|
+
}
|
|
28
|
+
),
|
|
29
|
+
resolution(
|
|
30
|
+
"sdd-adopt",
|
|
31
|
+
"Conservar el mío",
|
|
32
|
+
formatCliCommand(`components adopt sdd-core${agentsFlag} --yes`),
|
|
33
|
+
{
|
|
34
|
+
kind: RESOLUTION_KIND.CONFIGURE,
|
|
35
|
+
safety: RESOLUTION_SAFETY.CONSENT,
|
|
36
|
+
detail: "Adopt disk bytes into Kairo state without overwriting files. Button click is consent."
|
|
37
|
+
}
|
|
38
|
+
),
|
|
39
|
+
resolution(
|
|
40
|
+
"sdd-overwrite",
|
|
41
|
+
"Usar versión Kairo",
|
|
42
|
+
formatCliCommand(`components configure sdd-core${agentsFlag} --overwrite-conflicts --yes`),
|
|
43
|
+
{
|
|
44
|
+
kind: RESOLUTION_KIND.CONFIGURE,
|
|
45
|
+
safety: RESOLUTION_SAFETY.DESTRUCTIVE,
|
|
46
|
+
detail: "Backup then replace conflicting files with canonical Kairo skills."
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
resolution(
|
|
50
|
+
"doctor",
|
|
51
|
+
"Doctor",
|
|
52
|
+
formatCliCommand("doctor"),
|
|
53
|
+
{ kind: RESOLUTION_KIND.RUN, safety: RESOLUTION_SAFETY.READ_ONLY }
|
|
54
|
+
),
|
|
55
|
+
resolution(
|
|
56
|
+
"refresh",
|
|
57
|
+
"Refresh",
|
|
58
|
+
null,
|
|
59
|
+
{ kind: RESOLUTION_KIND.REFRESH, safety: RESOLUTION_SAFETY.READ_ONLY }
|
|
60
|
+
)
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function conflictAgentIds(findings) {
|
|
65
|
+
const ids = new Set();
|
|
66
|
+
for (const finding of findings) {
|
|
67
|
+
if (finding?.status !== SDD_HEALTH.CONFLICT && finding?.status !== SDD_HEALTH.DRIFTED) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const id of finding.agentIds ?? []) ids.add(id);
|
|
71
|
+
}
|
|
72
|
+
return [...ids].sort();
|
|
73
|
+
}
|