@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.
Files changed (76) hide show
  1. package/global-template/components/agent-skills/LICENSE +21 -0
  2. package/global-template/components/agent-skills/PROVENANCE.md +26 -0
  3. package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
  4. package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
  5. package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
  6. package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
  7. package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
  8. package/global-template/components/catalog.json +29 -0
  9. package/package.json +5 -2
  10. package/scripts/cockpit-smoke.mjs +1 -1
  11. package/src/cli.js +136 -8
  12. package/src/global/component-builders.js +3 -1
  13. package/src/global/components/agent-skills.js +27 -0
  14. package/src/global/ink/brand/wordmark.js +50 -0
  15. package/src/global/ink/cockpit/primitives.js +45 -35
  16. package/src/global/ink/cockpit-control-center.js +104 -4
  17. package/src/global/ink/cockpit-scan.js +20 -2
  18. package/src/global/ink/cockpit-views.js +2 -1
  19. package/src/global/ink/ecosystem-updates-display.js +37 -0
  20. package/src/global/ink/launch-input.js +32 -1
  21. package/src/global/ink/orchestrator-app.js +14 -5
  22. package/src/global/ink/orchestrator-state.js +17 -2
  23. package/src/global/ink/system-resources-display.js +109 -0
  24. package/src/global/ink/theme.js +13 -8
  25. package/src/global/ink/use-orchestrator-data.js +40 -4
  26. package/src/global/ink/ux/live-activity.js +4 -7
  27. package/src/global/ink/ux/live-alerts.js +2 -5
  28. package/src/global/ink/ux/live-governance.js +2 -6
  29. package/src/global/ink/ux/live-orchestration.js +2 -5
  30. package/src/global/ink/ux/live-overview.js +88 -28
  31. package/src/global/ink/ux/live-settings.js +3 -6
  32. package/src/global/ink/ux/live-usage.js +5 -7
  33. package/src/global/ink/ux/semantic.js +23 -6
  34. package/src/global/mcp/kairo-mcp.js +230 -0
  35. package/src/global/observability/build-companion-snapshot.js +281 -0
  36. package/src/global/observability/build-observability-snapshot.js +24 -0
  37. package/src/global/observability/ecosystem-updates.js +224 -0
  38. package/src/global/observability/gentle-bundle-export.js +71 -0
  39. package/src/global/observability/gentle-bundle-import.js +122 -0
  40. package/src/global/observability/gentle-probe.js +155 -0
  41. package/src/global/observability/graphify-ops.js +133 -0
  42. package/src/global/observability/graphify-parse-cache.js +90 -0
  43. package/src/global/observability/graphify-probe.js +185 -0
  44. package/src/global/observability/hermes-activity.js +163 -0
  45. package/src/global/observability/hermes-probe.js +171 -0
  46. package/src/global/observability/index.js +85 -0
  47. package/src/global/observability/passive-snapshot-flight.js +93 -0
  48. package/src/global/observability/probe-contract.js +38 -0
  49. package/src/global/observability/probe-registry.js +30 -0
  50. package/src/global/observability/resource-advisor.js +71 -0
  51. package/src/global/observability/system-resources.js +171 -0
  52. package/src/global/runtime/alerts/alert-cli.js +31 -0
  53. package/src/global/runtime/alerts/alert-store.js +29 -6
  54. package/src/global/runtime/alerts/alert-validate.js +25 -1
  55. package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
  56. package/src/global/runtime/execution-adapters/claude.js +2 -1
  57. package/src/global/runtime/execution-adapters/codex.js +2 -1
  58. package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
  59. package/src/global/runtime/execution-adapters/cursor.js +2 -1
  60. package/src/global/runtime/execution-adapters/opencode.js +2 -1
  61. package/src/global/runtime/execution-adapters/pi.js +2 -1
  62. package/src/global/runtime/review/index.js +1 -1
  63. package/src/global/runtime/review/review-cli.js +113 -3
  64. package/src/global/runtime/review/review-git.js +142 -11
  65. package/src/global/runtime/review/review-patch.js +2 -0
  66. package/src/global/runtime/review/review-receipts.js +12 -7
  67. package/src/global/runtime/review/review-runner.js +2 -2
  68. package/src/global/runtime/review/review-types.js +8 -5
  69. package/src/global/runtime/review/review-validate.js +5 -1
  70. package/src/global/runtime/run-cli.js +2 -0
  71. package/src/global/runtime/run-manager.js +39 -18
  72. package/src/global/runtime/run-permissions.js +231 -0
  73. package/src/global/runtime/run-profile.js +2 -0
  74. package/src/global/runtime/run-supervisor.js +77 -37
  75. package/src/global/runtime/run-types.js +2 -0
  76. 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
+ }