@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
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,7 @@ export function defaultSddState() {
|
|
|
8
8
|
personaAgentIds: [],
|
|
9
9
|
agentIds: [],
|
|
10
10
|
files: [],
|
|
11
|
+
adopted: [],
|
|
11
12
|
lastReceiptId: null,
|
|
12
13
|
updatedAt: null
|
|
13
14
|
};
|
|
@@ -25,6 +26,7 @@ export function normalizeSddState(raw) {
|
|
|
25
26
|
personaAgentIds,
|
|
26
27
|
agentIds: Array.isArray(raw.agentIds) ? [...raw.agentIds] : [],
|
|
27
28
|
files: Array.isArray(raw.files) ? raw.files.map(normalizeSddFile) : [],
|
|
29
|
+
adopted: Array.isArray(raw.adopted) ? raw.adopted.map(normalizeAdoptedFile).filter(Boolean) : [],
|
|
28
30
|
lastReceiptId: typeof raw.lastReceiptId === "string" ? raw.lastReceiptId : null,
|
|
29
31
|
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null
|
|
30
32
|
};
|
|
@@ -42,6 +44,72 @@ function normalizeSddFile(entry) {
|
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function normalizeAdoptedFile(entry) {
|
|
48
|
+
if (!entry || typeof entry !== "object") return null;
|
|
49
|
+
if (typeof entry.destinationPath !== "string" || !entry.destinationPath) return null;
|
|
50
|
+
if (typeof entry.hash !== "string" || !entry.hash) return null;
|
|
51
|
+
return {
|
|
52
|
+
destinationPath: entry.destinationPath,
|
|
53
|
+
hash: entry.hash,
|
|
54
|
+
skillId: typeof entry.skillId === "string" ? entry.skillId : null,
|
|
55
|
+
agentIds: Array.isArray(entry.agentIds) ? [...entry.agentIds] : [],
|
|
56
|
+
relativePath: typeof entry.relativePath === "string" ? entry.relativePath : "SKILL.md",
|
|
57
|
+
adoptedAt: typeof entry.adoptedAt === "string" ? entry.adoptedAt : null,
|
|
58
|
+
reason: typeof entry.reason === "string" ? entry.reason : null
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Map destinationPath → adopted hash for verify/plan. */
|
|
63
|
+
export function adoptedHashesFromState(sdd) {
|
|
64
|
+
const adopted = normalizeSddState(sdd).adopted;
|
|
65
|
+
return Object.fromEntries(adopted.map((entry) => [entry.destinationPath, entry.hash]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Record conflict findings as adopted disk hashes (no file writes). */
|
|
69
|
+
export function recordSddAdoptions(state, {
|
|
70
|
+
adoptions = [],
|
|
71
|
+
now = () => new Date().toISOString()
|
|
72
|
+
} = {}) {
|
|
73
|
+
const current = normalizeSddState(state?.sdd);
|
|
74
|
+
const byPath = new Map(current.adopted.map((entry) => [entry.destinationPath, entry]));
|
|
75
|
+
for (const entry of adoptions) {
|
|
76
|
+
const normalized = normalizeAdoptedFile({
|
|
77
|
+
...entry,
|
|
78
|
+
adoptedAt: entry.adoptedAt ?? now()
|
|
79
|
+
});
|
|
80
|
+
if (!normalized) continue;
|
|
81
|
+
byPath.set(normalized.destinationPath, normalized);
|
|
82
|
+
}
|
|
83
|
+
const adopted = [...byPath.values()].sort((a, b) =>
|
|
84
|
+
a.destinationPath.localeCompare(b.destinationPath)
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
...(state ?? {}),
|
|
88
|
+
sdd: {
|
|
89
|
+
...current,
|
|
90
|
+
adopted,
|
|
91
|
+
updatedAt: now()
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Drop adopted entries for paths that become Kairo-managed (overwrite/apply). */
|
|
97
|
+
export function clearSddAdoptionsForPaths(state, paths = [], {
|
|
98
|
+
now = () => new Date().toISOString()
|
|
99
|
+
} = {}) {
|
|
100
|
+
const current = normalizeSddState(state?.sdd);
|
|
101
|
+
const drop = new Set(paths);
|
|
102
|
+
const adopted = current.adopted.filter((entry) => !drop.has(entry.destinationPath));
|
|
103
|
+
return {
|
|
104
|
+
...(state ?? {}),
|
|
105
|
+
sdd: {
|
|
106
|
+
...current,
|
|
107
|
+
adopted,
|
|
108
|
+
updatedAt: now()
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
45
113
|
function verifiedNoopHash(file) {
|
|
46
114
|
const disk = file.afterHash ?? file.diskHash ?? file.beforeHash ?? null;
|
|
47
115
|
if (disk == null || file.canonicalHash == null || disk !== file.canonicalHash) return null;
|
|
@@ -116,6 +184,7 @@ export function recordSddMaterialization(state, { receipt, now = () => new Date(
|
|
|
116
184
|
personaAgentIds,
|
|
117
185
|
agentIds: collectAgentIds(files),
|
|
118
186
|
files,
|
|
187
|
+
adopted: current.adopted,
|
|
119
188
|
lastReceiptId: receipt.id ?? current.lastReceiptId,
|
|
120
189
|
updatedAt: now()
|
|
121
190
|
}
|
|
@@ -26,6 +26,7 @@ export async function verifySddConfigure({
|
|
|
26
26
|
homeDir,
|
|
27
27
|
packageRoot,
|
|
28
28
|
trackedFiles = {},
|
|
29
|
+
adoptedFiles = {},
|
|
29
30
|
personaAgentIds = [],
|
|
30
31
|
exists = existsSync,
|
|
31
32
|
readFileImpl = readFile
|
|
@@ -49,14 +50,15 @@ export async function verifySddConfigure({
|
|
|
49
50
|
const fileExists = exists(destinationPath);
|
|
50
51
|
const diskHash = fileExists ? hashBuffer(await readFileImpl(destinationPath)) : null;
|
|
51
52
|
const trackedHash = trackedFiles[destinationPath] ?? null;
|
|
53
|
+
const adoptedHash = adoptedFiles[destinationPath] ?? null;
|
|
52
54
|
const health = classifySddVerifyHealth({
|
|
53
|
-
exists: fileExists, canonicalHash, diskHash, trackedHash
|
|
55
|
+
exists: fileExists, canonicalHash, diskHash, trackedHash, adoptedHash
|
|
54
56
|
});
|
|
55
57
|
findings.push({
|
|
56
58
|
skillId, relativePath: file.relativePath, destinationPath,
|
|
57
59
|
agentIds: [...group.agentIds], kind: group.kind,
|
|
58
60
|
status: health.status, drift: health.drift, reason: health.reason,
|
|
59
|
-
canonicalHash, skillHash, diskHash, trackedHash
|
|
61
|
+
canonicalHash, skillHash, diskHash, trackedHash, adoptedHash
|
|
60
62
|
});
|
|
61
63
|
}
|
|
62
64
|
}
|
|
@@ -66,13 +68,15 @@ export async function verifySddConfigure({
|
|
|
66
68
|
|| compareSkillPaths(a.relativePath, b.relativePath)
|
|
67
69
|
|| compareSkillPaths(a.destinationPath, b.destinationPath));
|
|
68
70
|
|
|
69
|
-
const summary = { configured: 0, missing: 0, drifted: 0, conflict: 0 };
|
|
71
|
+
const summary = { configured: 0, adopted: 0, missing: 0, drifted: 0, conflict: 0 };
|
|
70
72
|
for (const entry of findings) summary[entry.status] += 1;
|
|
71
73
|
|
|
72
74
|
const consumers = normalizePersonaAgentIds(personaAgentIds);
|
|
73
75
|
const incompleteAgentIds = consumers.filter((id) => {
|
|
74
76
|
const mine = findings.filter((e) => e.agentIds.includes(id));
|
|
75
|
-
return !mine.length || mine.some((e) =>
|
|
77
|
+
return !mine.length || mine.some((e) =>
|
|
78
|
+
e.status !== SDD_HEALTH.CONFIGURED && e.status !== SDD_HEALTH.ADOPTED
|
|
79
|
+
);
|
|
76
80
|
});
|
|
77
81
|
let gatePresent = true;
|
|
78
82
|
for (const id of consumers) {
|
|
@@ -100,6 +104,7 @@ export function summarizeSddHealth(summary) {
|
|
|
100
104
|
if (summary.conflict > 0) return SDD_HEALTH.CONFLICT;
|
|
101
105
|
if (summary.missing > 0) return SDD_HEALTH.MISSING;
|
|
102
106
|
if (summary.drifted > 0) return SDD_HEALTH.DRIFTED;
|
|
107
|
+
if ((summary.adopted ?? 0) > 0 && (summary.configured ?? 0) === 0) return SDD_HEALTH.ADOPTED;
|
|
103
108
|
return SDD_HEALTH.CONFIGURED;
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -12,10 +12,20 @@ import { runGraphifyOp } from "../observability/graphify-ops.js";
|
|
|
12
12
|
import { resolveGitHeadSha } from "../observability/graphify-probe.js";
|
|
13
13
|
import { runPassiveObservabilitySnapshot } from "../observability/passive-snapshot-flight.js";
|
|
14
14
|
import { inspectEngramIntegration } from "../integrations/engram-evidence.js";
|
|
15
|
+
import { buildFleetReport } from "../observability/fleet-probe.js";
|
|
16
|
+
import {
|
|
17
|
+
createPublishWorkSnapshotHandler,
|
|
18
|
+
workSnapshotPublishSchema
|
|
19
|
+
} from "./work-snapshot-tool.js";
|
|
20
|
+
import { resolveMcpWorkspaceCwd } from "./resolve-mcp-workspace.js";
|
|
21
|
+
|
|
22
|
+
/** Sole MCP write tool for companion snapshots. */
|
|
23
|
+
export const KAIRO_MCP_WRITE_TOOLS = Object.freeze(["kairo_publish_work_snapshot"]);
|
|
15
24
|
|
|
16
25
|
export const KAIRO_MCP_TOOLS = Object.freeze([
|
|
17
26
|
"kairo_status", "kairo_runs", "kairo_alerts", "kairo_gentle_status",
|
|
18
|
-
"kairo_graph_query", "kairo_graph_path", "kairo_context_summary"
|
|
27
|
+
"kairo_graph_query", "kairo_graph_path", "kairo_context_summary", "kairo_fleet",
|
|
28
|
+
...KAIRO_MCP_WRITE_TOOLS
|
|
19
29
|
]);
|
|
20
30
|
|
|
21
31
|
const empty = z.object({});
|
|
@@ -30,7 +40,8 @@ export const mcpSchemas = Object.freeze({
|
|
|
30
40
|
graph: z.string().min(1), question: z.string().min(1),
|
|
31
41
|
budget: z.number().int().min(1).max(8000).default(2000)
|
|
32
42
|
}),
|
|
33
|
-
graphPath: z.object({ graph: z.string().min(1), from: z.string().min(1), to: z.string().min(1) })
|
|
43
|
+
graphPath: z.object({ graph: z.string().min(1), from: z.string().min(1), to: z.string().min(1) }),
|
|
44
|
+
workSnapshotPublish: workSnapshotPublishSchema
|
|
34
45
|
});
|
|
35
46
|
|
|
36
47
|
const CODE_RE = /^(?:[a-z][a-z0-9_]{0,48}|status=\d+)$/;
|
|
@@ -94,7 +105,11 @@ function graphEnvelope(result) {
|
|
|
94
105
|
|
|
95
106
|
export function createToolHandlers(deps = {}) {
|
|
96
107
|
const homeDir = deps.homeDir ?? resolveHomeDir();
|
|
97
|
-
|
|
108
|
+
// Cursor may spawn MCP under $HOME; prefer VSCODE_CWD / WORKSPACE_FOLDER_PATHS.
|
|
109
|
+
const cwd = resolveMcpWorkspaceCwd({
|
|
110
|
+
cwd: deps.cwd,
|
|
111
|
+
env: deps.env ?? process.env
|
|
112
|
+
});
|
|
98
113
|
const listRuns = deps.listRuns ?? ((o) => listRunRecords(homeDir, o));
|
|
99
114
|
const listAlertRows = deps.listAlerts ?? ((o) => listAlerts({ homeDir, ...o }));
|
|
100
115
|
const listReviews = deps.listReviews ?? (() => listReviewReceipts({ homeDir, limit: 20 }));
|
|
@@ -117,6 +132,7 @@ export function createToolHandlers(deps = {}) {
|
|
|
117
132
|
observabilityContext: { cwd, homeDir, workspaceRoot: cwd, headSha: requestHead() }
|
|
118
133
|
}));
|
|
119
134
|
const gentleProbe = deps.probeGentle ?? ((ctx) => probeGentle(ctx));
|
|
135
|
+
const fleetProbe = deps.buildFleet ?? ((ctx) => buildFleetReport(ctx));
|
|
120
136
|
const graphOp = deps.runGraphifyOp ?? runGraphifyOp;
|
|
121
137
|
const gOpts = () => ({
|
|
122
138
|
cwd, workspaceRoot: cwd, headSha: requestHead(), whichCommand: deps.whichCommand,
|
|
@@ -200,7 +216,36 @@ export function createToolHandlers(deps = {}) {
|
|
|
200
216
|
} catch {
|
|
201
217
|
return soft("degraded", { signals: null, engram: null, links: [], alertsCount: null, nextSafeAction: null });
|
|
202
218
|
}
|
|
203
|
-
}
|
|
219
|
+
},
|
|
220
|
+
async kairo_fleet() {
|
|
221
|
+
try {
|
|
222
|
+
const report = await fleetProbe({ homeDir });
|
|
223
|
+
return mcpResult({
|
|
224
|
+
ok: true,
|
|
225
|
+
code: "ok",
|
|
226
|
+
data: {
|
|
227
|
+
kind: report?.kind ?? "declared",
|
|
228
|
+
note: report?.note ?? null,
|
|
229
|
+
orchestratorAuthority: report?.orchestratorAuthority ?? null,
|
|
230
|
+
fleets: Array.isArray(report?.fleets) ? report.fleets : [],
|
|
231
|
+
activity: report?.activity ?? null,
|
|
232
|
+
generatedAt: report?.generatedAt ?? null
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
return soft("degraded", {
|
|
237
|
+
kind: "declared", fleets: [], activity: null, note: null, orchestratorAuthority: null
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
kairo_publish_work_snapshot: createPublishWorkSnapshotHandler({
|
|
242
|
+
homeDir,
|
|
243
|
+
cwd,
|
|
244
|
+
now: deps.now,
|
|
245
|
+
writeAtomic: deps.writeAtomic,
|
|
246
|
+
publishWorkSnapshot: deps.publishWorkSnapshot,
|
|
247
|
+
mcpResult
|
|
248
|
+
})
|
|
204
249
|
};
|
|
205
250
|
}
|
|
206
251
|
|
|
@@ -213,7 +258,13 @@ export function registerKairoMcpTools(registerTool, deps = {}) {
|
|
|
213
258
|
["kairo_gentle_status", "Gentle probe / companion gentle signal", empty],
|
|
214
259
|
["kairo_graph_query", "Read-only Graphify query", mcpSchemas.graphQuery],
|
|
215
260
|
["kairo_graph_path", "Read-only Graphify path", mcpSchemas.graphPath],
|
|
216
|
-
["kairo_context_summary", "Companion + soft links + alerts count", empty]
|
|
261
|
+
["kairo_context_summary", "Companion + soft links + alerts count", empty],
|
|
262
|
+
["kairo_fleet", "Declared fleet topology + OpenCode live activity", empty],
|
|
263
|
+
[
|
|
264
|
+
"kairo_publish_work_snapshot",
|
|
265
|
+
"Publish kairo.work-snapshot/v1 for the runtime workspace (enrolls conversation)",
|
|
266
|
+
mcpSchemas.workSnapshotPublish
|
|
267
|
+
]
|
|
217
268
|
]) registerTool(name, { description, inputSchema }, h[name]);
|
|
218
269
|
return h;
|
|
219
270
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the workspace path for Kairo MCP identity.
|
|
3
|
+
*
|
|
4
|
+
* Cursor IDE launches global `mcpServers.kairo` with process cwd = $HOME even
|
|
5
|
+
* when the entry sets `cwd: "."`. It does inject the open folder via
|
|
6
|
+
* VSCODE_CWD / WORKSPACE_FOLDER_PATHS — prefer those over process.cwd().
|
|
7
|
+
*/
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { canonicalizeProjectPath } from "../next/project-key.js";
|
|
10
|
+
|
|
11
|
+
function firstNonEmpty(value) {
|
|
12
|
+
if (typeof value !== "string") return null;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed ? trimmed : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse Cursor/VS Code workspace folder env into an ordered path list.
|
|
19
|
+
* WORKSPACE_FOLDER_PATHS uses commas when Cursor opens multiple roots.
|
|
20
|
+
*/
|
|
21
|
+
export function parseWorkspaceFolderPaths(raw) {
|
|
22
|
+
const text = firstNonEmpty(raw);
|
|
23
|
+
if (!text) return [];
|
|
24
|
+
return text
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((part) => part.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ cwd?: string, env?: NodeJS.ProcessEnv }} [options]
|
|
32
|
+
* @returns {string} absolute workspace path
|
|
33
|
+
*/
|
|
34
|
+
export function resolveMcpWorkspaceCwd({
|
|
35
|
+
cwd,
|
|
36
|
+
env = process.env
|
|
37
|
+
} = {}) {
|
|
38
|
+
let chosen;
|
|
39
|
+
if (typeof cwd === "string" && cwd.trim()) {
|
|
40
|
+
chosen = resolve(cwd.trim());
|
|
41
|
+
} else {
|
|
42
|
+
const fromFolders = parseWorkspaceFolderPaths(env.WORKSPACE_FOLDER_PATHS);
|
|
43
|
+
if (fromFolders.length > 0) {
|
|
44
|
+
chosen = resolve(fromFolders[0]);
|
|
45
|
+
} else {
|
|
46
|
+
const vscodeCwd = firstNonEmpty(env.VSCODE_CWD);
|
|
47
|
+
chosen = vscodeCwd ? resolve(vscodeCwd) : resolve(process.cwd());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return canonicalizeProjectPath(chosen);
|
|
51
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed Cursor rule: instruct agents to publish kairo.work-snapshot/v1.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdir, readFile, writeFile, rename, copyFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export const WORK_SNAPSHOT_RULE_FILENAME = "kairo-work-snapshot.mdc";
|
|
10
|
+
|
|
11
|
+
export function resolveCursorRulesDir(homeDir = homedir()) {
|
|
12
|
+
return join(homeDir, ".cursor", "rules");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveWorkSnapshotRulePath(homeDir = homedir()) {
|
|
16
|
+
return join(resolveCursorRulesDir(homeDir), WORK_SNAPSHOT_RULE_FILENAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const WORK_SNAPSHOT_RULE_BODY = `# Kairo work snapshot
|
|
20
|
+
|
|
21
|
+
After each significant turn, publish the true work state with MCP \`kairo_publish_work_snapshot\`:
|
|
22
|
+
|
|
23
|
+
- Required: \`conversationId\`, \`provider\` (\`cursor\`), \`goal\`, \`now\`, \`next\`
|
|
24
|
+
- Optional: \`progress\` (≤3), \`blockers\`, \`delegations\` (only real ones)
|
|
25
|
+
- Workspace identity is derived by Kairo from the runtime — never send \`projectKey\`, paths, or \`cwd\`
|
|
26
|
+
- Never invent work. Never send prompts, transcripts, or tool dumps
|
|
27
|
+
- Reuse the same \`conversationId\` for later turns in this chat
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export function buildWorkSnapshotRuleFile() {
|
|
31
|
+
return `---
|
|
32
|
+
description: Publish Kairo work snapshot after significant Cursor turns
|
|
33
|
+
alwaysApply: true
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
${WORK_SNAPSHOT_RULE_BODY}
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function writeAtomicText(targetPath, text, deps = {}) {
|
|
41
|
+
const write = deps.writeFileFn ?? writeFile;
|
|
42
|
+
const renameFn = deps.renameFn ?? rename;
|
|
43
|
+
const tempPath = join(
|
|
44
|
+
dirname(targetPath),
|
|
45
|
+
`.${WORK_SNAPSHOT_RULE_FILENAME}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`
|
|
46
|
+
);
|
|
47
|
+
await write(tempPath, text, "utf8");
|
|
48
|
+
await renameFn(tempPath, targetPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Plan or apply the managed work-snapshot rule under ~/.cursor/rules/.
|
|
53
|
+
*/
|
|
54
|
+
export async function ensureWorkSnapshotRule({
|
|
55
|
+
homeDir = homedir(),
|
|
56
|
+
apply = false,
|
|
57
|
+
now = () => Date.now(),
|
|
58
|
+
readFileFn = readFile,
|
|
59
|
+
mkdirFn = mkdir,
|
|
60
|
+
copyFileFn = copyFile,
|
|
61
|
+
writeFileFn = writeFile,
|
|
62
|
+
renameFn = rename
|
|
63
|
+
} = {}) {
|
|
64
|
+
const path = resolveWorkSnapshotRulePath(homeDir);
|
|
65
|
+
const desired = buildWorkSnapshotRuleFile();
|
|
66
|
+
let existing = null;
|
|
67
|
+
try {
|
|
68
|
+
existing = await readFileFn(path, "utf8");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error?.code !== "ENOENT") throw error;
|
|
71
|
+
}
|
|
72
|
+
const wouldWrite = existing !== desired;
|
|
73
|
+
const backupPath = wouldWrite && existing != null
|
|
74
|
+
? `${path}.kairo-backup.${now()}`
|
|
75
|
+
: null;
|
|
76
|
+
|
|
77
|
+
if (!apply) {
|
|
78
|
+
return { path, wouldWrite, wrote: false, backupPath: null };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await mkdirFn(dirname(path), { recursive: true });
|
|
82
|
+
if (wouldWrite && existing != null) {
|
|
83
|
+
await copyFileFn(path, backupPath);
|
|
84
|
+
}
|
|
85
|
+
if (wouldWrite) {
|
|
86
|
+
await writeAtomicText(path, desired, { writeFileFn, renameFn });
|
|
87
|
+
}
|
|
88
|
+
return { path, wouldWrite, wrote: wouldWrite, backupPath };
|
|
89
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { publishWorkSnapshot } from "../next/publish-work-snapshot.js";
|
|
3
|
+
|
|
4
|
+
export const workSnapshotPublishSchema = z.object({
|
|
5
|
+
conversationId: z.string().min(1).max(160),
|
|
6
|
+
provider: z.enum(["cursor", "codex", "claude", "opencode", "pi", "other"]),
|
|
7
|
+
goal: z.string().min(1).max(160),
|
|
8
|
+
now: z.string().min(1).max(240),
|
|
9
|
+
next: z.string().min(1).max(240),
|
|
10
|
+
progress: z.array(z.string().max(160)).max(3).optional(),
|
|
11
|
+
blockers: z.array(z.string().max(200)).max(12).optional(),
|
|
12
|
+
delegations: z.array(z.object({
|
|
13
|
+
workId: z.string().max(64).optional(),
|
|
14
|
+
title: z.string().max(160).optional(),
|
|
15
|
+
role: z.enum(["orchestrator", "worker"]).optional(),
|
|
16
|
+
state: z.enum(["assigned", "working", "blocked", "completed", "failed"]).optional()
|
|
17
|
+
}).strict()).max(12).optional()
|
|
18
|
+
}).strict();
|
|
19
|
+
|
|
20
|
+
export function createPublishWorkSnapshotHandler(deps = {}) {
|
|
21
|
+
const publishSnapshot = deps.publishWorkSnapshot ?? ((input) => publishWorkSnapshot(input, {
|
|
22
|
+
homeDir: deps.homeDir,
|
|
23
|
+
cwd: deps.cwd,
|
|
24
|
+
now: deps.now,
|
|
25
|
+
writeAtomic: deps.writeAtomic
|
|
26
|
+
}));
|
|
27
|
+
const toResult = deps.mcpResult;
|
|
28
|
+
if (typeof toResult !== "function") {
|
|
29
|
+
throw new Error("mcpResult dependency is required");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return async function kairo_publish_work_snapshot(args = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const result = await publishSnapshot(args);
|
|
35
|
+
return toResult({
|
|
36
|
+
ok: Boolean(result?.ok),
|
|
37
|
+
code: result?.code ?? "publish_failed",
|
|
38
|
+
data: result?.data ?? null,
|
|
39
|
+
diagnostics: result?.diagnostics ?? [],
|
|
40
|
+
isError: !result?.ok
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return toResult({
|
|
44
|
+
ok: false, code: "publish_failed", data: null,
|
|
45
|
+
diagnostics: ["publish_failed"], isError: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|