@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
|
@@ -46,6 +46,7 @@ import { LAYOUT_MODES } from "./layout.js";
|
|
|
46
46
|
import { CHANGES_PHASE } from "./cockpit-changes.js";
|
|
47
47
|
import { RECOVERY_PHASE, listRecoverySnapshots } from "./cockpit-recovery.js";
|
|
48
48
|
import { SETTINGS_PHASE } from "./cockpit-settings.js";
|
|
49
|
+
import { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./ux/overview-actions.js";
|
|
49
50
|
|
|
50
51
|
export function OrchestratorApp({
|
|
51
52
|
homeDir,
|
|
@@ -62,8 +63,7 @@ export function OrchestratorApp({
|
|
|
62
63
|
const [ui, dispatch] = useReducer(
|
|
63
64
|
reduceCockpitUi,
|
|
64
65
|
createCockpitUiState({
|
|
65
|
-
layoutMode: layoutMode ?? LAYOUT_MODES.COMPACT
|
|
66
|
-
region: COCKPIT_REGIONS.NAV
|
|
66
|
+
layoutMode: layoutMode ?? LAYOUT_MODES.COMPACT
|
|
67
67
|
})
|
|
68
68
|
);
|
|
69
69
|
const data = useOrchestratorData({
|
|
@@ -165,6 +165,26 @@ export function OrchestratorApp({
|
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
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
|
+
|
|
168
188
|
if (inputKey === " " && ui.view === ORCHESTRATOR_VIEWS.HOME && !ui.paletteOpen) {
|
|
169
189
|
dispatch({ type: "toggle-overview-details" });
|
|
170
190
|
return;
|
|
@@ -245,7 +265,9 @@ export function OrchestratorApp({
|
|
|
245
265
|
return;
|
|
246
266
|
}
|
|
247
267
|
|
|
248
|
-
const listLength = ui.view === ORCHESTRATOR_VIEWS.
|
|
268
|
+
const listLength = ui.view === ORCHESTRATOR_VIEWS.HOME
|
|
269
|
+
? OVERVIEW_BUTTON_COUNT
|
|
270
|
+
: ui.view === ORCHESTRATOR_VIEWS.RUNS
|
|
249
271
|
? RUNS_HUB_ITEMS.length
|
|
250
272
|
: ui.view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
|
|
251
273
|
? (data.dashboard?.activeRuns ?? []).length
|
|
@@ -301,6 +323,21 @@ export function OrchestratorApp({
|
|
|
301
323
|
return;
|
|
302
324
|
}
|
|
303
325
|
}
|
|
326
|
+
if (routed.type === "enter-home-button") {
|
|
327
|
+
const buttons = buildOverviewButtons({
|
|
328
|
+
hasGlobalState,
|
|
329
|
+
snapshot: data.snapshot,
|
|
330
|
+
diagnostics: data.diagnostics,
|
|
331
|
+
dashboard: data.dashboard
|
|
332
|
+
});
|
|
333
|
+
const selected = buttons[Math.min(Math.max(0, ui.listIndex), buttons.length - 1)];
|
|
334
|
+
const intent = selected?.intent ?? null;
|
|
335
|
+
if (intent === "setup") {
|
|
336
|
+
finish({ cancelled: false, action: "setup" });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (intent && openDestination(intent)) return;
|
|
340
|
+
}
|
|
304
341
|
dispatch(routed);
|
|
305
342
|
return;
|
|
306
343
|
}
|
|
@@ -583,7 +620,8 @@ export function OrchestratorApp({
|
|
|
583
620
|
governanceDetailsOpen: ui.governanceDetailsOpen,
|
|
584
621
|
activityDetailsOpen: ui.activityDetailsOpen,
|
|
585
622
|
contentFocused: ui.region === COCKPIT_REGIONS.CONTENT,
|
|
586
|
-
homeDir
|
|
623
|
+
homeDir,
|
|
624
|
+
hasGlobalState
|
|
587
625
|
})
|
|
588
626
|
)
|
|
589
627
|
);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live semantic Overview for Cockpit HOME — product cover.
|
|
3
|
-
*
|
|
3
|
+
* Content owns the button focus mark when region=content.
|
|
4
4
|
* ASCII wordmark only here (wide/compact); minimal is textual.
|
|
5
5
|
*
|
|
6
|
-
* Rule: show purpose +
|
|
6
|
+
* Rule: show purpose + two buttons + a few plain-language needs.
|
|
7
7
|
* Machine/system noise stays out of the first screen (Details only).
|
|
8
8
|
*/
|
|
9
9
|
import React from "react";
|
|
10
10
|
import { Box, Text } from "ink";
|
|
11
11
|
import { DASHBOARD_PURPOSE } from "../../dashboard-guidance.js";
|
|
12
12
|
import { LAYOUT_MODES } from "../layout.js";
|
|
13
|
-
import { COCKPIT_COLORS } from "../theme.js";
|
|
13
|
+
import { COCKPIT_COLORS, resolveGlyphs } from "../theme.js";
|
|
14
14
|
import {
|
|
15
15
|
overviewBrandTitle,
|
|
16
16
|
shouldShowWordmark,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
mapHealthTone,
|
|
25
25
|
partitionCompanionLines
|
|
26
26
|
} from "./overview-needs.js";
|
|
27
|
+
import { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./overview-actions.js";
|
|
27
28
|
|
|
28
29
|
export {
|
|
29
30
|
humanizeCompanionNeed,
|
|
@@ -33,6 +34,8 @@ export {
|
|
|
33
34
|
partitionCompanionLines
|
|
34
35
|
} from "./overview-needs.js";
|
|
35
36
|
|
|
37
|
+
export { buildOverviewButtons, OVERVIEW_BUTTON_COUNT } from "./overview-actions.js";
|
|
38
|
+
|
|
36
39
|
/** Safe Details lines — leftovers + raw signals; never invent paths/IDs. */
|
|
37
40
|
export function buildOverviewDetails(model = {}, companionRest = []) {
|
|
38
41
|
const lines = [];
|
|
@@ -59,13 +62,19 @@ export function buildOverviewDetails(model = {}, companionRest = []) {
|
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Pure adapter: buildControlCenterModel → semantic overview props.
|
|
62
|
-
* First screen = purpose +
|
|
65
|
+
* First screen = purpose + two buttons + plain needs. Machine noise → Details.
|
|
63
66
|
*/
|
|
64
|
-
export function adaptControlCenterToOverview(model = {}) {
|
|
67
|
+
export function adaptControlCenterToOverview(model = {}, options = {}) {
|
|
65
68
|
const status = model.status ?? model.health ?? {};
|
|
66
69
|
const next = model.nextAction ?? model.cta ?? {};
|
|
67
70
|
const { needs, rest } = partitionCompanionLines(model.companion?.lines ?? []);
|
|
68
71
|
const primary = humanizePrimary(next);
|
|
72
|
+
const buttons = buildOverviewButtons({
|
|
73
|
+
hasGlobalState: options.hasGlobalState,
|
|
74
|
+
snapshot: options.snapshot,
|
|
75
|
+
diagnostics: options.diagnostics,
|
|
76
|
+
dashboard: options.dashboard
|
|
77
|
+
});
|
|
69
78
|
|
|
70
79
|
const activity = model.activity?.headline;
|
|
71
80
|
const metrics = [];
|
|
@@ -85,12 +94,6 @@ export function adaptControlCenterToOverview(model = {}) {
|
|
|
85
94
|
label: `Alerts · ${model.alerts.headline ?? `${model.alerts.count} open`}`
|
|
86
95
|
});
|
|
87
96
|
}
|
|
88
|
-
if (rest.length > 0) {
|
|
89
|
-
metrics.push({
|
|
90
|
-
id: "more",
|
|
91
|
-
label: `${rest.length} more in Details · Space`
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
97
|
if (metrics.length === 0) {
|
|
95
98
|
metrics.push({ id: "quiet", label: "Nothing else needs you right now" });
|
|
96
99
|
}
|
|
@@ -104,6 +107,7 @@ export function adaptControlCenterToOverview(model = {}) {
|
|
|
104
107
|
body: status.summaryLine ?? ""
|
|
105
108
|
},
|
|
106
109
|
primary,
|
|
110
|
+
buttons,
|
|
107
111
|
metrics,
|
|
108
112
|
details: buildOverviewDetails(model, rest)
|
|
109
113
|
};
|
|
@@ -136,14 +140,30 @@ export function SemanticOverviewPanel({
|
|
|
136
140
|
detailsOpen = false,
|
|
137
141
|
colorEnabled = true,
|
|
138
142
|
unicode = true,
|
|
139
|
-
layoutMode = LAYOUT_MODES.COMPACT
|
|
143
|
+
layoutMode = LAYOUT_MODES.COMPACT,
|
|
144
|
+
selectedIndex = 0,
|
|
145
|
+
contentFocused = false,
|
|
146
|
+
hasGlobalState = false,
|
|
147
|
+
snapshot = null,
|
|
148
|
+
diagnostics = null,
|
|
149
|
+
dashboard = null
|
|
140
150
|
}) {
|
|
141
|
-
const view = adaptControlCenterToOverview(model
|
|
151
|
+
const view = adaptControlCenterToOverview(model, {
|
|
152
|
+
hasGlobalState,
|
|
153
|
+
snapshot,
|
|
154
|
+
diagnostics,
|
|
155
|
+
dashboard
|
|
156
|
+
});
|
|
142
157
|
const showArt = shouldShowWordmark(layoutMode);
|
|
143
158
|
const brandTitle = overviewBrandTitle(layoutMode);
|
|
144
159
|
const isWide = layoutMode === LAYOUT_MODES.WIDE;
|
|
145
160
|
const mark = renderWordmark({ layoutMode, colorEnabled, unicode });
|
|
146
161
|
const status = renderCallout(view, colorEnabled);
|
|
162
|
+
const safeIndex = Math.min(
|
|
163
|
+
Math.max(0, selectedIndex),
|
|
164
|
+
Math.max(0, view.buttons.length - 1)
|
|
165
|
+
);
|
|
166
|
+
const glyphs = resolveGlyphs(unicode);
|
|
147
167
|
|
|
148
168
|
const hero = showArt
|
|
149
169
|
? (isWide
|
|
@@ -166,18 +186,25 @@ export function SemanticOverviewPanel({
|
|
|
166
186
|
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
167
187
|
}, view.purpose),
|
|
168
188
|
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
...view.buttons.map((button, index) => {
|
|
190
|
+
const selected = index === safeIndex;
|
|
191
|
+
const focused = contentFocused && selected;
|
|
192
|
+
return React.createElement(Box, {
|
|
193
|
+
key: button.id,
|
|
194
|
+
flexDirection: "column",
|
|
195
|
+
marginBottom: index === view.buttons.length - 1 ? 0 : 1
|
|
196
|
+
},
|
|
197
|
+
React.createElement(Text, {
|
|
198
|
+
bold: selected,
|
|
199
|
+
color: colorEnabled && selected ? COCKPIT_COLORS.interactive : undefined
|
|
200
|
+
}, `${selected ? glyphs.focus : " "} [${index + 1}] ${button.label}${focused ? " ← Press Enter" : ""}`),
|
|
201
|
+
button.detail
|
|
202
|
+
? React.createElement(Text, {
|
|
203
|
+
color: colorEnabled ? COCKPIT_COLORS.muted : undefined
|
|
204
|
+
}, ` ${button.detail}`)
|
|
205
|
+
: null
|
|
206
|
+
);
|
|
207
|
+
})
|
|
181
208
|
),
|
|
182
209
|
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
183
210
|
React.createElement(ActionList, {
|
|
@@ -190,7 +217,7 @@ export function SemanticOverviewPanel({
|
|
|
190
217
|
),
|
|
191
218
|
React.createElement(Details, {
|
|
192
219
|
open: detailsOpen,
|
|
193
|
-
summary:
|
|
220
|
+
summary: `More info (${view.details.length})`,
|
|
194
221
|
lines: view.details,
|
|
195
222
|
colorEnabled,
|
|
196
223
|
focused: false,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two explicit Home buttons — prepare + configure.
|
|
3
|
+
* No writes here: intents only open existing consent-gated flows.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const OVERVIEW_BUTTON_COUNT = 2;
|
|
7
|
+
|
|
8
|
+
function changeCount(snapshot) {
|
|
9
|
+
if (!snapshot?.diff?.hasChanges) return 0;
|
|
10
|
+
return snapshot.diff.changeCount
|
|
11
|
+
?? snapshot.diff.changes?.length
|
|
12
|
+
?? 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function detectedAgentCount({ snapshot = null, diagnostics = null, dashboard = null } = {}) {
|
|
16
|
+
if (Number.isFinite(snapshot?.coverage?.detectedAgents)) {
|
|
17
|
+
return snapshot.coverage.detectedAgents;
|
|
18
|
+
}
|
|
19
|
+
if (Number.isFinite(diagnostics?.diagnostics?.detected)) {
|
|
20
|
+
return diagnostics.diagnostics.detected;
|
|
21
|
+
}
|
|
22
|
+
return (dashboard?.providers ?? []).filter((entry) => entry?.available).length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildPrepareButton({
|
|
26
|
+
hasGlobalState = false,
|
|
27
|
+
snapshot = null,
|
|
28
|
+
diagnostics = null,
|
|
29
|
+
dashboard = null
|
|
30
|
+
} = {}) {
|
|
31
|
+
const detected = detectedAgentCount({ snapshot, diagnostics, dashboard });
|
|
32
|
+
const needsSetup = hasGlobalState === false || detected === 0;
|
|
33
|
+
if (needsSetup) {
|
|
34
|
+
return {
|
|
35
|
+
id: "prepare",
|
|
36
|
+
label: "Set up Kairo",
|
|
37
|
+
detail: "Detect your agents and install what is missing.",
|
|
38
|
+
intent: "setup"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pending = changeCount(snapshot);
|
|
43
|
+
if (pending > 0) {
|
|
44
|
+
return {
|
|
45
|
+
id: "prepare",
|
|
46
|
+
label: `Repair ${pending} change${pending === 1 ? "" : "s"}`,
|
|
47
|
+
detail: "You will see the exact plan before anything is written.",
|
|
48
|
+
intent: "governance"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: "prepare",
|
|
54
|
+
label: "Everything is ready",
|
|
55
|
+
detail: "Open History to see what changed, or Configure to adjust.",
|
|
56
|
+
intent: "history"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CONFIGURE_BUTTON = Object.freeze({
|
|
61
|
+
id: "configure",
|
|
62
|
+
label: "Configure",
|
|
63
|
+
detail: "Agents, Obsidian vault, integrations.",
|
|
64
|
+
intent: "settings"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Exactly two buttons for Home. First button changes face by state.
|
|
69
|
+
*/
|
|
70
|
+
export function buildOverviewButtons({
|
|
71
|
+
hasGlobalState = false,
|
|
72
|
+
snapshot = null,
|
|
73
|
+
diagnostics = null,
|
|
74
|
+
dashboard = null
|
|
75
|
+
} = {}) {
|
|
76
|
+
return [
|
|
77
|
+
buildPrepareButton({ hasGlobalState, snapshot, diagnostics, dashboard }),
|
|
78
|
+
{ ...CONFIGURE_BUTTON }
|
|
79
|
+
];
|
|
80
|
+
}
|
|
@@ -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
|
+
}
|