@kal-elsam/kairo-runtime 0.10.0 → 0.12.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/global-template/components/agent-skills/LICENSE +21 -0
- package/global-template/components/agent-skills/PROVENANCE.md +26 -0
- package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
- package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
- package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
- package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
- package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
- package/global-template/components/catalog.json +29 -0
- package/package.json +5 -2
- package/scripts/cockpit-smoke.mjs +1 -1
- package/src/cli.js +136 -8
- package/src/global/component-builders.js +3 -1
- package/src/global/components/agent-skills.js +27 -0
- package/src/global/ink/brand/wordmark.js +50 -0
- package/src/global/ink/cockpit/primitives.js +45 -35
- package/src/global/ink/cockpit-control-center.js +104 -4
- package/src/global/ink/cockpit-scan.js +20 -2
- package/src/global/ink/cockpit-views.js +2 -1
- package/src/global/ink/ecosystem-updates-display.js +37 -0
- package/src/global/ink/launch-input.js +32 -1
- package/src/global/ink/orchestrator-app.js +14 -5
- package/src/global/ink/orchestrator-state.js +17 -2
- package/src/global/ink/system-resources-display.js +109 -0
- package/src/global/ink/theme.js +13 -8
- package/src/global/ink/use-orchestrator-data.js +40 -4
- package/src/global/ink/ux/live-activity.js +4 -7
- package/src/global/ink/ux/live-alerts.js +2 -5
- package/src/global/ink/ux/live-governance.js +2 -6
- package/src/global/ink/ux/live-orchestration.js +2 -5
- package/src/global/ink/ux/live-overview.js +88 -28
- package/src/global/ink/ux/live-settings.js +3 -6
- package/src/global/ink/ux/live-usage.js +5 -7
- package/src/global/ink/ux/semantic.js +23 -6
- package/src/global/mcp/kairo-mcp.js +230 -0
- package/src/global/observability/build-companion-snapshot.js +281 -0
- package/src/global/observability/build-observability-snapshot.js +24 -0
- package/src/global/observability/ecosystem-updates.js +224 -0
- package/src/global/observability/gentle-bundle-export.js +71 -0
- package/src/global/observability/gentle-bundle-import.js +122 -0
- package/src/global/observability/gentle-probe.js +155 -0
- package/src/global/observability/graphify-ops.js +133 -0
- package/src/global/observability/graphify-parse-cache.js +90 -0
- package/src/global/observability/graphify-probe.js +185 -0
- package/src/global/observability/hermes-activity.js +163 -0
- package/src/global/observability/hermes-probe.js +171 -0
- package/src/global/observability/index.js +85 -0
- package/src/global/observability/passive-snapshot-flight.js +93 -0
- package/src/global/observability/probe-contract.js +38 -0
- package/src/global/observability/probe-registry.js +30 -0
- package/src/global/observability/resource-advisor.js +71 -0
- package/src/global/observability/system-resources.js +171 -0
- package/src/global/runtime/alerts/alert-cli.js +31 -0
- package/src/global/runtime/alerts/alert-store.js +29 -6
- package/src/global/runtime/alerts/alert-validate.js +25 -1
- package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
- package/src/global/runtime/execution-adapters/claude.js +2 -1
- package/src/global/runtime/execution-adapters/codex.js +2 -1
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
- package/src/global/runtime/execution-adapters/cursor.js +2 -1
- package/src/global/runtime/execution-adapters/opencode.js +2 -1
- package/src/global/runtime/execution-adapters/pi.js +2 -1
- package/src/global/runtime/review/index.js +1 -1
- package/src/global/runtime/review/review-cli.js +113 -3
- package/src/global/runtime/review/review-git.js +142 -11
- package/src/global/runtime/review/review-patch.js +2 -0
- package/src/global/runtime/review/review-receipts.js +12 -7
- package/src/global/runtime/review/review-runner.js +2 -2
- package/src/global/runtime/review/review-types.js +8 -5
- package/src/global/runtime/review/review-validate.js +5 -1
- package/src/global/runtime/run-cli.js +2 -0
- package/src/global/runtime/run-manager.js +39 -18
- package/src/global/runtime/run-permissions.js +231 -0
- package/src/global/runtime/run-profile.js +2 -0
- package/src/global/runtime/run-supervisor.js +77 -37
- package/src/global/runtime/run-types.js +2 -0
- package/src/global/updates-cli.js +41 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { fetchPublishedVersion } from "../npm-registry.js";
|
|
4
|
+
import { harnessHomePaths } from "../paths.js";
|
|
5
|
+
import { probeCommand as defaultProbeCommand } from "../cli-probe.js";
|
|
6
|
+
|
|
7
|
+
export const ECOSYSTEM_UPDATES_CACHE_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
export const ECOSYSTEM_UPDATES_TIMEOUT_MS = 8000;
|
|
9
|
+
export const AGENT_SKILLS_PINNED_REV = "d2478bf0c73a6357df39a3ed6aff16acaa218843";
|
|
10
|
+
export const AGENT_SKILLS_UPSTREAM =
|
|
11
|
+
"https://api.github.com/repos/addyosmani/agent-skills/commits/HEAD";
|
|
12
|
+
|
|
13
|
+
const TOOL_IDS = Object.freeze(["kairo", "hermes", "gentle", "skills"]);
|
|
14
|
+
|
|
15
|
+
function emptyTool(id, state = "unavailable") {
|
|
16
|
+
return {
|
|
17
|
+
id, state, installed: null, latest: null, updateAvailable: false,
|
|
18
|
+
provenance: null, error: state === "error" || state === "unavailable" ? state : null
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function envelope(partial = {}) {
|
|
23
|
+
return {
|
|
24
|
+
state: "error", checkedAt: null, cacheHit: false, diagnostics: [],
|
|
25
|
+
tools: Object.fromEntries(TOOL_IDS.map((id) => [id, emptyTool(id)])),
|
|
26
|
+
...partial
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function whichAbsolute(command, env, probeCommand) {
|
|
31
|
+
const which = probeCommand("which", [command], { env, timeoutMs: 3000 });
|
|
32
|
+
if (!which?.ok) return "";
|
|
33
|
+
const path = String(which.stdout ?? "").trim().split(/\r?\n/)[0] ?? "";
|
|
34
|
+
return isAbsolute(path) ? path : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compareSemver(a, b) {
|
|
38
|
+
const parse = (v) => String(v).replace(/^v/, "").split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
39
|
+
const aa = parse(a), bb = parse(b);
|
|
40
|
+
for (let i = 0; i < Math.max(aa.length, bb.length); i += 1) {
|
|
41
|
+
const d = (aa[i] ?? 0) - (bb[i] ?? 0);
|
|
42
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseGentleUpdateOutput(stdout) {
|
|
48
|
+
const tools = [];
|
|
49
|
+
for (const line of String(stdout ?? "").split(/\r?\n/)) {
|
|
50
|
+
const m = line.match(/\[(ok|UP)\]\s+(\S+)\s+installed:\s+(\S+)\s+latest:\s+(\S+)/i);
|
|
51
|
+
if (m) tools.push({ name: m[2], installed: m[3], latest: m[4], updateAvailable: m[1].toUpperCase() === "UP" });
|
|
52
|
+
}
|
|
53
|
+
return tools;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function parseHermesUpdateCheck(stdout, stderr, ok) {
|
|
57
|
+
const text = `${stdout ?? ""}\n${stderr ?? ""}`;
|
|
58
|
+
if (/update available|behind|new version|commits? behind/i.test(text)) {
|
|
59
|
+
return { state: "available", updateAvailable: true };
|
|
60
|
+
}
|
|
61
|
+
if (/up to date|already up.to.date|no update/i.test(text)) {
|
|
62
|
+
return { state: "available", updateAvailable: false };
|
|
63
|
+
}
|
|
64
|
+
if (!ok || /failed|fatal|error|unable/i.test(text)) {
|
|
65
|
+
return { state: "error", updateAvailable: false, error: "check_failed" };
|
|
66
|
+
}
|
|
67
|
+
return { state: "partial", updateAvailable: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function checkKairo({ packageName, installedVersion, fetchVersion }) {
|
|
71
|
+
try {
|
|
72
|
+
const latest = await fetchVersion(packageName);
|
|
73
|
+
if (typeof latest !== "string" || !latest) {
|
|
74
|
+
return { ...emptyTool("kairo", "error"), installed: installedVersion, error: "malformed", provenance: "npm" };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: "kairo", state: "available", installed: installedVersion, latest,
|
|
78
|
+
updateAvailable: compareSemver(latest, installedVersion) > 0,
|
|
79
|
+
provenance: "npm-registry", error: null
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const offline = /ENOTFOUND|ECONN|network|fetch|HTTP|timed?\s*out/i.test(String(err?.message ?? err));
|
|
83
|
+
return {
|
|
84
|
+
...emptyTool("kairo", offline ? "unavailable" : "error"),
|
|
85
|
+
installed: installedVersion, provenance: "npm-registry",
|
|
86
|
+
error: offline ? "offline" : "check_failed"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkHermes({ env, probeCommand, timeoutMs }) {
|
|
92
|
+
const bin = whichAbsolute("hermes", env, probeCommand);
|
|
93
|
+
if (!bin) return emptyTool("hermes", "unavailable");
|
|
94
|
+
const res = probeCommand(bin, ["update", "--check"], { env, timeoutMs });
|
|
95
|
+
if (res?.timedOut) return { ...emptyTool("hermes", "error"), error: "timeout", provenance: bin };
|
|
96
|
+
const parsed = parseHermesUpdateCheck(res?.stdout, res?.stderr, Boolean(res?.ok));
|
|
97
|
+
return {
|
|
98
|
+
id: "hermes", state: parsed.state, installed: null, latest: null,
|
|
99
|
+
updateAvailable: parsed.updateAvailable === true, provenance: bin, error: parsed.error ?? null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkGentle({ env, probeCommand, timeoutMs }) {
|
|
104
|
+
const bin = whichAbsolute("gentle-ai", env, probeCommand);
|
|
105
|
+
if (!bin) return emptyTool("gentle", "unavailable");
|
|
106
|
+
const res = probeCommand(bin, ["update"], { env, timeoutMs });
|
|
107
|
+
if (res?.timedOut) return { ...emptyTool("gentle", "error"), error: "timeout", provenance: bin };
|
|
108
|
+
if (!res?.ok && !res?.stdout) return { ...emptyTool("gentle", "error"), error: "check_failed", provenance: bin };
|
|
109
|
+
const rows = parseGentleUpdateOutput(res.stdout);
|
|
110
|
+
if (rows.length === 0) return { ...emptyTool("gentle", "partial"), provenance: bin, error: "malformed" };
|
|
111
|
+
const root = rows.find((r) => r.name === "gentle-ai") ?? rows[0];
|
|
112
|
+
return {
|
|
113
|
+
id: "gentle", state: "available", installed: root.installed, latest: root.latest,
|
|
114
|
+
updateAvailable: rows.some((r) => r.updateAvailable), provenance: bin, error: null
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function checkSkills({ pinnedRev, fetchJson }) {
|
|
119
|
+
try {
|
|
120
|
+
const meta = await fetchJson(AGENT_SKILLS_UPSTREAM);
|
|
121
|
+
const sha = typeof meta?.sha === "string" ? meta.sha : null;
|
|
122
|
+
if (!sha || !/^[0-9a-f]{7,40}$/i.test(sha)) {
|
|
123
|
+
return { ...emptyTool("skills", "error"), installed: pinnedRev, error: "malformed", provenance: "github" };
|
|
124
|
+
}
|
|
125
|
+
const exactDiff = sha.toLowerCase() !== pinnedRev.toLowerCase()
|
|
126
|
+
&& !(pinnedRev.length >= 7 && sha.toLowerCase().startsWith(pinnedRev.toLowerCase()));
|
|
127
|
+
return {
|
|
128
|
+
id: "skills", state: "available", installed: pinnedRev, latest: sha,
|
|
129
|
+
updateAvailable: exactDiff, provenance: "github-commits-api", error: null
|
|
130
|
+
};
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const offline = /ENOTFOUND|ECONN|network|fetch|HTTP|403|429|timed?\s*out/i.test(String(err?.message ?? err));
|
|
133
|
+
return {
|
|
134
|
+
...emptyTool("skills", offline ? "unavailable" : "error"),
|
|
135
|
+
installed: pinnedRev, provenance: "github-commits-api",
|
|
136
|
+
error: offline ? "offline" : "check_failed"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function aggregateState(tools) {
|
|
142
|
+
const states = TOOL_IDS.map((id) => tools[id]?.state);
|
|
143
|
+
if (states.every((s) => s === "unavailable")) return "unavailable";
|
|
144
|
+
if (states.some((s) => s === "error" || s === "partial" || s === "unavailable")) return "partial";
|
|
145
|
+
return "available";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Read-only ecosystem update detection. Never invokes mutating updaters. */
|
|
149
|
+
export async function loadEcosystemUpdates({
|
|
150
|
+
packageName = "@kal-elsam/kairo-runtime",
|
|
151
|
+
installedVersion = "0.0.0",
|
|
152
|
+
homeDir,
|
|
153
|
+
env = process.env,
|
|
154
|
+
nowMs = Date.now(),
|
|
155
|
+
cacheTtlMs = ECOSYSTEM_UPDATES_CACHE_MS,
|
|
156
|
+
timeoutMs = ECOSYSTEM_UPDATES_TIMEOUT_MS,
|
|
157
|
+
forceRefresh = false,
|
|
158
|
+
pinnedSkillsRev = AGENT_SKILLS_PINNED_REV,
|
|
159
|
+
probeCommand = defaultProbeCommand,
|
|
160
|
+
fetchVersion = fetchPublishedVersion,
|
|
161
|
+
fetchJson = defaultFetchJson,
|
|
162
|
+
readCacheFile = async (p) => { try { return JSON.parse(await readFile(p, "utf8")); } catch { return null; } },
|
|
163
|
+
writeCacheFile = async (p, payload) => {
|
|
164
|
+
try {
|
|
165
|
+
await mkdir(join(p, ".."), { recursive: true });
|
|
166
|
+
await writeFile(p, `${JSON.stringify(payload)}\n`, "utf8");
|
|
167
|
+
} catch { /* best-effort */ }
|
|
168
|
+
}
|
|
169
|
+
} = {}) {
|
|
170
|
+
const cachePath = homeDir
|
|
171
|
+
? join(harnessHomePaths(homeDir).root, "cache", "ecosystem-updates.json")
|
|
172
|
+
: null;
|
|
173
|
+
|
|
174
|
+
if (!forceRefresh && cachePath) {
|
|
175
|
+
const cached = await readCacheFile(cachePath);
|
|
176
|
+
const checkedAt = cached?.checkedAt ? Date.parse(cached.checkedAt) : NaN;
|
|
177
|
+
if (Number.isFinite(checkedAt) && (nowMs - checkedAt) < cacheTtlMs && cached?.tools) {
|
|
178
|
+
return envelope({
|
|
179
|
+
state: cached.state ?? aggregateState(cached.tools),
|
|
180
|
+
checkedAt: cached.checkedAt, cacheHit: true,
|
|
181
|
+
diagnostics: Array.isArray(cached.diagnostics) ? cached.diagnostics : [],
|
|
182
|
+
tools: cached.tools
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const perToolTimeout = Math.max(1000, Math.floor(timeoutMs / 2));
|
|
188
|
+
try {
|
|
189
|
+
const settled = await Promise.race([
|
|
190
|
+
Promise.all([
|
|
191
|
+
checkKairo({ packageName, installedVersion, fetchVersion }),
|
|
192
|
+
Promise.resolve().then(() => checkHermes({ env, probeCommand, timeoutMs: perToolTimeout })),
|
|
193
|
+
Promise.resolve().then(() => checkGentle({ env, probeCommand, timeoutMs: perToolTimeout })),
|
|
194
|
+
checkSkills({ pinnedRev: pinnedSkillsRev, fetchJson })
|
|
195
|
+
]),
|
|
196
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeoutMs))
|
|
197
|
+
]);
|
|
198
|
+
const tools = { kairo: settled[0], hermes: settled[1], gentle: settled[2], skills: settled[3] };
|
|
199
|
+
const payload = envelope({
|
|
200
|
+
state: aggregateState(tools),
|
|
201
|
+
checkedAt: new Date(nowMs).toISOString(),
|
|
202
|
+
cacheHit: false, diagnostics: [], tools
|
|
203
|
+
});
|
|
204
|
+
if (cachePath) await writeCacheFile(cachePath, payload);
|
|
205
|
+
return payload;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const isTimeout = /timeout/i.test(String(err?.message ?? err));
|
|
208
|
+
return envelope({
|
|
209
|
+
state: isTimeout ? "error" : "unavailable",
|
|
210
|
+
checkedAt: new Date(nowMs).toISOString(),
|
|
211
|
+
cacheHit: false,
|
|
212
|
+
diagnostics: [isTimeout ? "ecosystem updates timeout" : "ecosystem updates error"],
|
|
213
|
+
tools: Object.fromEntries(TOOL_IDS.map((id) => [id, emptyTool(id, isTimeout ? "error" : "unavailable")]))
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function defaultFetchJson(url) {
|
|
219
|
+
const response = await fetch(url, {
|
|
220
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "kairo-ecosystem-updates" }
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
|
|
223
|
+
return response.json();
|
|
224
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { probeCommand as defaultProbeCommand } from "../cli-probe.js";
|
|
3
|
+
import { probeGentle as defaultProbeGentle } from "./gentle-probe.js";
|
|
4
|
+
|
|
5
|
+
const EXPORT_TIMEOUT_MS = 60_000;
|
|
6
|
+
|
|
7
|
+
/** Require negotiated absolute binary evidence — never fall back to PATH names. */
|
|
8
|
+
export function resolveNegotiatedGentleBinary(probe) {
|
|
9
|
+
const path = (probe?.evidence ?? []).find((e) => e.kind === "binary")?.path ?? null;
|
|
10
|
+
return typeof path === "string" && path && isAbsolute(path) ? path : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fail(code, diagnostics, extra = {}) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false, exitCode: 2, code,
|
|
16
|
+
diagnostics: diagnostics.map(String),
|
|
17
|
+
providerStatus: null, timedOut: false, ...extra
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Transport-only export: invoke Gentle review-bundle-export.
|
|
23
|
+
* Never reads/rewrites bundle bytes; never surfaces raw provider stderr.
|
|
24
|
+
*/
|
|
25
|
+
export async function exportGentleReviewBundle({
|
|
26
|
+
lineage, outPath, cwd = process.cwd(), env = process.env,
|
|
27
|
+
probeGentle = defaultProbeGentle, probeCommand = defaultProbeCommand
|
|
28
|
+
} = {}) {
|
|
29
|
+
if (typeof lineage !== "string" || !lineage.trim()) {
|
|
30
|
+
return fail("invalid_request", ["Missing lineage id."], { lineage: lineage ?? null, outPath: outPath ?? null });
|
|
31
|
+
}
|
|
32
|
+
if (typeof outPath !== "string" || !outPath.trim()) {
|
|
33
|
+
return fail("invalid_request", ["Missing --out path."], { lineage, outPath: outPath ?? null });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const probe = await probeGentle({ cwd, env });
|
|
37
|
+
if (probe.state !== "available") {
|
|
38
|
+
const code = ["missing", "incompatible", "error"].includes(probe.state)
|
|
39
|
+
? `gentle_${probe.state}` : "gentle_error";
|
|
40
|
+
return fail(code, probe.diagnostics?.length ? probe.diagnostics : [`Gentle probe state: ${probe.state}`], {
|
|
41
|
+
lineage, outPath
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const binary = resolveNegotiatedGentleBinary(probe);
|
|
46
|
+
if (!binary) {
|
|
47
|
+
return fail("gentle_binary_unbound", [
|
|
48
|
+
"Negotiated Gentle binary evidence missing or not absolute; refusing PATH fallback."
|
|
49
|
+
], { lineage, outPath });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const provider = probeCommand(binary, [
|
|
53
|
+
"review-bundle-export", "--cwd", cwd, "--lineage", lineage, "--out", outPath
|
|
54
|
+
], { cwd, env, timeoutMs: EXPORT_TIMEOUT_MS });
|
|
55
|
+
|
|
56
|
+
if (!provider.ok) {
|
|
57
|
+
const diagnostics = ["provider_error"];
|
|
58
|
+
if (provider.timedOut) diagnostics.push("timed_out");
|
|
59
|
+
if (provider.status != null) diagnostics.push(`status=${provider.status}`);
|
|
60
|
+
return fail("provider_error", diagnostics, {
|
|
61
|
+
lineage, outPath,
|
|
62
|
+
providerStatus: provider.status ?? null,
|
|
63
|
+
timedOut: Boolean(provider.timedOut)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
ok: true, exitCode: 0, code: "exported", lineage, outPath,
|
|
69
|
+
diagnostics: [], providerStatus: provider.status, timedOut: false, binary
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { probeCommand as defaultProbeCommand } from "../cli-probe.js";
|
|
2
|
+
import {
|
|
3
|
+
CONSENT_TYPES,
|
|
4
|
+
PermissionAuthorityError,
|
|
5
|
+
UNSAFE_OPERATIONS,
|
|
6
|
+
authorizeUnsafeOperation as defaultAuthorizeUnsafeOperation
|
|
7
|
+
} from "../runtime/run-permissions.js";
|
|
8
|
+
import { probeGentle as defaultProbeGentle } from "./gentle-probe.js";
|
|
9
|
+
import { resolveNegotiatedGentleBinary } from "./gentle-bundle-export.js";
|
|
10
|
+
|
|
11
|
+
const IMPORT_TIMEOUT_MS = 60_000;
|
|
12
|
+
|
|
13
|
+
function fail(code, diagnostics, extra = {}) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
exitCode: 2,
|
|
17
|
+
code,
|
|
18
|
+
mutationOutcome: "not_started",
|
|
19
|
+
diagnostics: diagnostics.map(String),
|
|
20
|
+
providerStatus: null,
|
|
21
|
+
timedOut: false,
|
|
22
|
+
permissionAuthority: null,
|
|
23
|
+
...extra
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Consent-gated transport: invoke Gentle review-bundle-import.
|
|
29
|
+
* Provider decides bundle format. Never parses bytes or surfaces raw stderr.
|
|
30
|
+
*/
|
|
31
|
+
export async function importGentleReviewBundle({
|
|
32
|
+
bundlePath,
|
|
33
|
+
cwd = process.cwd(),
|
|
34
|
+
env = process.env,
|
|
35
|
+
confirmImport = false,
|
|
36
|
+
probeGentle = defaultProbeGentle,
|
|
37
|
+
probeCommand = defaultProbeCommand,
|
|
38
|
+
authorizeUnsafeOperation = defaultAuthorizeUnsafeOperation
|
|
39
|
+
} = {}) {
|
|
40
|
+
if (typeof bundlePath !== "string" || !bundlePath.trim()) {
|
|
41
|
+
return fail("invalid_request", ["Missing --bundle path."], {
|
|
42
|
+
bundlePath: bundlePath ?? null
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let permissionAuthority = null;
|
|
47
|
+
try {
|
|
48
|
+
({ permissionAuthority } = authorizeUnsafeOperation({
|
|
49
|
+
operation: UNSAFE_OPERATIONS.GENTLE_BUNDLE_IMPORT,
|
|
50
|
+
confirmed: Boolean(confirmImport),
|
|
51
|
+
source: "cli",
|
|
52
|
+
consentType: CONSENT_TYPES.CLI_CONFIRM_IMPORT
|
|
53
|
+
}));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof PermissionAuthorityError) {
|
|
56
|
+
return fail(error.code ?? "import_consent_required", [error.message], {
|
|
57
|
+
bundlePath, cwd
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const probe = await probeGentle({ cwd, env });
|
|
64
|
+
if (probe.state !== "available") {
|
|
65
|
+
const code = ["missing", "incompatible", "error"].includes(probe.state)
|
|
66
|
+
? `gentle_${probe.state}` : "gentle_error";
|
|
67
|
+
return fail(
|
|
68
|
+
code,
|
|
69
|
+
probe.diagnostics?.length ? probe.diagnostics : [`Gentle probe state: ${probe.state}`],
|
|
70
|
+
{ bundlePath, cwd, permissionAuthority }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const binary = resolveNegotiatedGentleBinary(probe);
|
|
75
|
+
if (!binary) {
|
|
76
|
+
return fail("gentle_binary_unbound", [
|
|
77
|
+
"Negotiated Gentle binary evidence missing or not absolute; refusing PATH fallback."
|
|
78
|
+
], { bundlePath, cwd, permissionAuthority });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let provider;
|
|
82
|
+
try {
|
|
83
|
+
provider = probeCommand(binary, [
|
|
84
|
+
"review-bundle-import", "--cwd", cwd, "--bundle", bundlePath
|
|
85
|
+
], { cwd, env, timeoutMs: IMPORT_TIMEOUT_MS });
|
|
86
|
+
} catch {
|
|
87
|
+
return fail("mutation_outcome_unknown", ["provider_error", "spawn_interrupted"], {
|
|
88
|
+
mutationOutcome: "unknown",
|
|
89
|
+
bundlePath, cwd, binary, permissionAuthority
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const timedOut = Boolean(provider?.timedOut);
|
|
94
|
+
const status = provider?.status ?? null;
|
|
95
|
+
const committed = provider?.ok === true && status === 0 && timedOut !== true;
|
|
96
|
+
if (!committed) {
|
|
97
|
+
const diagnostics = ["provider_error"];
|
|
98
|
+
if (timedOut) diagnostics.push("timed_out");
|
|
99
|
+
if (status != null) diagnostics.push(`status=${status}`);
|
|
100
|
+
else diagnostics.push("status_unknown");
|
|
101
|
+
return fail("mutation_outcome_unknown", diagnostics, {
|
|
102
|
+
mutationOutcome: "unknown",
|
|
103
|
+
bundlePath, cwd, binary, permissionAuthority,
|
|
104
|
+
providerStatus: status,
|
|
105
|
+
timedOut
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
exitCode: 0,
|
|
112
|
+
code: "imported",
|
|
113
|
+
mutationOutcome: "committed",
|
|
114
|
+
bundlePath,
|
|
115
|
+
cwd,
|
|
116
|
+
binary,
|
|
117
|
+
diagnostics: [],
|
|
118
|
+
providerStatus: status,
|
|
119
|
+
timedOut: false,
|
|
120
|
+
permissionAuthority
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
isExecutableAvailable,
|
|
4
|
+
parseVersionFromOutput,
|
|
5
|
+
probeCommand as defaultProbeCommand
|
|
6
|
+
} from "../cli-probe.js";
|
|
7
|
+
import { normalizeProbeResult } from "./probe-contract.js";
|
|
8
|
+
|
|
9
|
+
export const SUPPORTED_PROTOCOL = Object.freeze({ major: 2, minor: 0 });
|
|
10
|
+
export const SUPPORTED_SCHEMA = "gentle-ai.review-integration.capabilities/v2";
|
|
11
|
+
export const SUPPORTED_CONTRACT = "gentle-ai.review-integration/v2";
|
|
12
|
+
export const SUPPORTED_MANDATORY_FEATURES = Object.freeze([
|
|
13
|
+
"compact_v2_authority", "exact_receipt_replay", "five_delivery_gates",
|
|
14
|
+
"immutable_snapshot", "legacy_v1_target_scoped_read_only",
|
|
15
|
+
"repository_independent_capabilities", "restart_safe_projection",
|
|
16
|
+
"sdd_receipt_binding", "target_scoped_status", "uniform_failure_envelope"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const SUPPORT_SET = new Set(SUPPORTED_MANDATORY_FEATURES);
|
|
20
|
+
|
|
21
|
+
function result(partial) {
|
|
22
|
+
return normalizeProbeResult({
|
|
23
|
+
id: "gentle", version: null, contractCompatible: null,
|
|
24
|
+
diagnostics: [], evidence: [], error: null, ...partial
|
|
25
|
+
}, "gentle");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function defaultWhichAbsolute(command, env) {
|
|
29
|
+
if (!isExecutableAvailable(command, { env })) return "";
|
|
30
|
+
const which = defaultProbeCommand("which", [command], { env, timeoutMs: 3000 });
|
|
31
|
+
const path = String(which.stdout ?? "").trim().split(/\r?\n/)[0] ?? "";
|
|
32
|
+
return isAbsolute(path) ? path : "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve absolute Gentle binary; never return a bare command name. */
|
|
36
|
+
export function resolveGentleBinaryPath(command = "gentle-ai", env = process.env, {
|
|
37
|
+
whichCommand = defaultWhichAbsolute
|
|
38
|
+
} = {}) {
|
|
39
|
+
const resolved = whichCommand(command, env) || null;
|
|
40
|
+
return resolved && isAbsolute(resolved) ? resolved : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Package version is evidence only — never the compatibility gate. */
|
|
44
|
+
export function evaluateGentleCapabilities(payload) {
|
|
45
|
+
const diagnostics = [];
|
|
46
|
+
const evidence = [{ kind: "capabilities", schema: payload?.schema ?? null }];
|
|
47
|
+
const version = payload?.package?.version ?? null;
|
|
48
|
+
if (payload == null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
49
|
+
return result({
|
|
50
|
+
state: "error", version, evidence, error: "invalid capabilities payload",
|
|
51
|
+
diagnostics: ["Capabilities payload is not an object."]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (payload.schema !== SUPPORTED_SCHEMA) {
|
|
55
|
+
diagnostics.push(`schema mismatch: got ${String(payload.schema)}`);
|
|
56
|
+
}
|
|
57
|
+
if (payload.contract !== SUPPORTED_CONTRACT) {
|
|
58
|
+
diagnostics.push(`contract mismatch: got ${String(payload.contract)}`);
|
|
59
|
+
}
|
|
60
|
+
if (payload.protocol?.major !== SUPPORTED_PROTOCOL.major) {
|
|
61
|
+
diagnostics.push(`protocol.major mismatch: got ${String(payload.protocol?.major)}`);
|
|
62
|
+
}
|
|
63
|
+
if (payload.protocol?.minor !== SUPPORTED_PROTOCOL.minor) {
|
|
64
|
+
diagnostics.push(`protocol.minor mismatch: got ${String(payload.protocol?.minor)}`);
|
|
65
|
+
}
|
|
66
|
+
const mandatory = payload.features?.mandatory;
|
|
67
|
+
if (!Array.isArray(mandatory)) {
|
|
68
|
+
diagnostics.push("features.mandatory must be an array.");
|
|
69
|
+
} else {
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (const feature of mandatory) {
|
|
72
|
+
const name = feature?.name;
|
|
73
|
+
seen.add(name);
|
|
74
|
+
if (typeof name !== "string" || !SUPPORT_SET.has(name)) {
|
|
75
|
+
diagnostics.push(`unknown mandatory feature: ${String(name)}`);
|
|
76
|
+
} else if (feature.supported !== true) {
|
|
77
|
+
diagnostics.push(`mandatory feature not supported: ${name}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const required of SUPPORTED_MANDATORY_FEATURES) {
|
|
81
|
+
if (!seen.has(required)) diagnostics.push(`missing mandatory feature: ${required}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (diagnostics.length) {
|
|
85
|
+
return result({ state: "incompatible", version, contractCompatible: false, diagnostics, evidence });
|
|
86
|
+
}
|
|
87
|
+
return result({ state: "available", version, contractCompatible: true, evidence });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function probeGentle({
|
|
91
|
+
env = process.env,
|
|
92
|
+
cwd = process.cwd(),
|
|
93
|
+
whichCommand = defaultWhichAbsolute,
|
|
94
|
+
probeCommand = defaultProbeCommand
|
|
95
|
+
} = {}) {
|
|
96
|
+
const path = resolveGentleBinaryPath("gentle-ai", env, { whichCommand });
|
|
97
|
+
if (!path) {
|
|
98
|
+
return result({
|
|
99
|
+
state: "missing",
|
|
100
|
+
diagnostics: [
|
|
101
|
+
"gentle-ai absolute binary not resolved. Install Gentle AI separately, then re-run the probe."
|
|
102
|
+
],
|
|
103
|
+
evidence: [{ kind: "binary", path: null }]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const evidence = [{ kind: "binary", path }];
|
|
107
|
+
const versionResult = probeCommand(path, ["--version"], { cwd, env, timeoutMs: 5000 });
|
|
108
|
+
const version = parseVersionFromOutput(`${versionResult.stdout}\n${versionResult.stderr}`);
|
|
109
|
+
evidence.push({ kind: "version", version, ok: versionResult.ok });
|
|
110
|
+
|
|
111
|
+
const capsResult = probeCommand(
|
|
112
|
+
path,
|
|
113
|
+
["review", "capabilities", "--contract", SUPPORTED_CONTRACT],
|
|
114
|
+
{ cwd, env, timeoutMs: 8000 }
|
|
115
|
+
);
|
|
116
|
+
evidence.push({ kind: "capabilities", ok: capsResult.ok, status: capsResult.status });
|
|
117
|
+
// Failed exit / timeout never promotes stdout into a negotiated capability.
|
|
118
|
+
if (capsResult.timedOut || capsResult.status !== 0) {
|
|
119
|
+
return result({
|
|
120
|
+
state: "error", version, evidence,
|
|
121
|
+
diagnostics: [
|
|
122
|
+
capsResult.timedOut
|
|
123
|
+
? "gentle-ai review capabilities timed out"
|
|
124
|
+
: "gentle-ai review capabilities failed"
|
|
125
|
+
],
|
|
126
|
+
error: capsResult.error ?? (capsResult.timedOut ? "timeout" : `exit ${capsResult.status}`)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
let payload;
|
|
130
|
+
try {
|
|
131
|
+
payload = JSON.parse(String(capsResult.stdout ?? "").trim());
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return result({
|
|
134
|
+
state: "error", version, evidence, error: err?.message ?? String(err),
|
|
135
|
+
diagnostics: ["Failed to parse capabilities JSON."]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const evaluated = evaluateGentleCapabilities(payload);
|
|
139
|
+
return result({
|
|
140
|
+
...evaluated,
|
|
141
|
+
version: evaluated.version ?? version,
|
|
142
|
+
evidence: [...evidence, ...(evaluated.evidence ?? [])]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createGentleProbe(deps = {}) {
|
|
147
|
+
return {
|
|
148
|
+
id: "gentle",
|
|
149
|
+
declaredEvents: Object.freeze([]),
|
|
150
|
+
declaredActions: Object.freeze([]),
|
|
151
|
+
async probe(context = {}) {
|
|
152
|
+
return probeGentle({ ...deps, ...context, env: context.env ?? deps.env ?? process.env });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|