@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attention items and ≤2 primary actions. Gentle commands stay verbatim.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
GENTLE_DOCTOR_COMMAND,
|
|
6
|
+
GENTLE_INSTALL_HINT,
|
|
7
|
+
GENTLE_UPGRADE_LABEL,
|
|
8
|
+
NO_ACTIVE_WORKFLOW,
|
|
9
|
+
PROVIDER,
|
|
10
|
+
WORKFLOW_KIND
|
|
11
|
+
} from "./constants.js";
|
|
12
|
+
|
|
13
|
+
function gentlePrimary(workflow) {
|
|
14
|
+
const provider = workflow?.provider;
|
|
15
|
+
if (provider === PROVIDER.UPGRADE_REQUIRED) {
|
|
16
|
+
return {
|
|
17
|
+
item: {
|
|
18
|
+
id: "upgrade-gentle",
|
|
19
|
+
severity: "warning",
|
|
20
|
+
message: "Gentle contract is outdated. Upgrade Gentle — do not approximate workflow."
|
|
21
|
+
},
|
|
22
|
+
action: {
|
|
23
|
+
id: "upgrade-gentle",
|
|
24
|
+
label: GENTLE_UPGRADE_LABEL,
|
|
25
|
+
command: GENTLE_DOCTOR_COMMAND
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (provider === PROVIDER.UNAVAILABLE) {
|
|
30
|
+
return {
|
|
31
|
+
item: {
|
|
32
|
+
id: "install-gentle",
|
|
33
|
+
severity: "warning",
|
|
34
|
+
message: GENTLE_INSTALL_HINT
|
|
35
|
+
},
|
|
36
|
+
action: {
|
|
37
|
+
id: "install-gentle",
|
|
38
|
+
label: "Install Gentle",
|
|
39
|
+
command: null
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (provider === PROVIDER.INCOMPATIBLE) {
|
|
44
|
+
return {
|
|
45
|
+
item: {
|
|
46
|
+
id: "gentle-incompatible",
|
|
47
|
+
severity: "error",
|
|
48
|
+
message: "Gentle response is incompatible. Fail closed."
|
|
49
|
+
},
|
|
50
|
+
action: null
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const command = workflow?.nextTransition?.execute?.command;
|
|
54
|
+
if (provider === PROVIDER.CONNECTED && typeof command === "string" && command) {
|
|
55
|
+
const operation = workflow.nextTransition.execute.operation;
|
|
56
|
+
return {
|
|
57
|
+
item: null,
|
|
58
|
+
action: {
|
|
59
|
+
id: "gentle-next",
|
|
60
|
+
label: typeof operation === "string" && operation ? operation : "Continue Gentle",
|
|
61
|
+
command
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { item: null, action: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildAttention({ work, workflow, team, connections }) {
|
|
69
|
+
const items = [];
|
|
70
|
+
const primaryActions = [];
|
|
71
|
+
const secondaryActions = [
|
|
72
|
+
{ id: "setup", label: "Setup", command: "kairo setup --dry-run" },
|
|
73
|
+
{ id: "models", label: "Models", command: "kairo fleet models" },
|
|
74
|
+
{ id: "catalog", label: "Catalog", command: "kairo fleet" },
|
|
75
|
+
{ id: "doctor", label: "Doctor", command: "kairo doctor" }
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const gentle = gentlePrimary(workflow);
|
|
79
|
+
if (gentle.item) items.push(gentle.item);
|
|
80
|
+
if (gentle.action) primaryActions.push(gentle.action);
|
|
81
|
+
|
|
82
|
+
if (work?.integration?.showRepair === true) {
|
|
83
|
+
items.push({
|
|
84
|
+
id: "repair-integration",
|
|
85
|
+
severity: "error",
|
|
86
|
+
message: work.integration.detail ?? "MCP integration needs repair."
|
|
87
|
+
});
|
|
88
|
+
if (primaryActions.length < 2) {
|
|
89
|
+
primaryActions.push({
|
|
90
|
+
id: "repair",
|
|
91
|
+
label: "Repair",
|
|
92
|
+
command: "kairo mcp install --yes"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (work?.integration?.state === "missing" && primaryActions.length < 2) {
|
|
98
|
+
items.push({
|
|
99
|
+
id: "connect-mcp",
|
|
100
|
+
severity: "warning",
|
|
101
|
+
message: "Kairo MCP is not registered."
|
|
102
|
+
});
|
|
103
|
+
primaryActions.push({
|
|
104
|
+
id: "connect",
|
|
105
|
+
label: "Connect Agent",
|
|
106
|
+
command: "kairo mcp install --yes"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const chip of connections ?? []) {
|
|
111
|
+
if (chip?.state === "error" || chip?.state === "conflict") {
|
|
112
|
+
items.push({
|
|
113
|
+
id: `conn-${chip.id}`,
|
|
114
|
+
severity: "warning",
|
|
115
|
+
message: `${chip.label ?? chip.id}: ${chip.detail ?? chip.state}`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!team?.platforms?.length) {
|
|
121
|
+
items.push({
|
|
122
|
+
id: "no-platforms",
|
|
123
|
+
severity: "info",
|
|
124
|
+
message: "No platforms detected in declared fleet topology."
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (workflow?.kind === WORKFLOW_KIND.NONE && !workflow?.active) {
|
|
129
|
+
items.push({
|
|
130
|
+
id: "no-workflow",
|
|
131
|
+
severity: "info",
|
|
132
|
+
message: NO_ACTIVE_WORKFLOW
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
items,
|
|
138
|
+
primaryActions: primaryActions.slice(0, 2),
|
|
139
|
+
secondaryActions
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build kairo.control-plane/v1 from next + connections/fleet + Gentle adapters.
|
|
3
|
+
*/
|
|
4
|
+
import { buildNextReport } from "../next/next-report.js";
|
|
5
|
+
import { buildConnectionsReport } from "../connections.js";
|
|
6
|
+
import { resolveHomeDir } from "../paths.js";
|
|
7
|
+
import {
|
|
8
|
+
CONTROL_PLANE_SCHEMA,
|
|
9
|
+
NO_ACTIVE_WORKFLOW,
|
|
10
|
+
WORKFLOW_KIND
|
|
11
|
+
} from "./constants.js";
|
|
12
|
+
import { buildAttention } from "./attention.js";
|
|
13
|
+
import { loadGentleWorkflow } from "./gentle-adapters.js";
|
|
14
|
+
import { normalizeTeam } from "./team.js";
|
|
15
|
+
|
|
16
|
+
function sectionOk() {
|
|
17
|
+
return { ok: true, error: null };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sectionErr(error) {
|
|
21
|
+
return { ok: false, error: String(error ?? "section_failed") };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function buildControlPlaneReport({
|
|
25
|
+
homeDir = resolveHomeDir(),
|
|
26
|
+
cwd = process.cwd(),
|
|
27
|
+
client = "cursor",
|
|
28
|
+
provider = "cursor",
|
|
29
|
+
packageRoot = null,
|
|
30
|
+
packageName = null,
|
|
31
|
+
cliVersion = null,
|
|
32
|
+
buildNext = buildNextReport,
|
|
33
|
+
buildConnections = buildConnectionsReport,
|
|
34
|
+
loadWorkflow = loadGentleWorkflow
|
|
35
|
+
} = {}) {
|
|
36
|
+
const diagnostics = [];
|
|
37
|
+
const sections = {
|
|
38
|
+
work: sectionOk(),
|
|
39
|
+
workflow: sectionOk(),
|
|
40
|
+
team: sectionOk(),
|
|
41
|
+
attention: sectionOk()
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let work;
|
|
45
|
+
try {
|
|
46
|
+
work = await buildNext({ homeDir, cwd, provider, client });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
work = {
|
|
49
|
+
schema: "kairo.next/v1",
|
|
50
|
+
ok: false,
|
|
51
|
+
goal: null,
|
|
52
|
+
progress: [],
|
|
53
|
+
now: null,
|
|
54
|
+
blockers: [],
|
|
55
|
+
next: null,
|
|
56
|
+
conversationId: null,
|
|
57
|
+
updatedAt: null,
|
|
58
|
+
integration: {
|
|
59
|
+
state: "broken",
|
|
60
|
+
provider,
|
|
61
|
+
client,
|
|
62
|
+
mcpConnected: false,
|
|
63
|
+
enrolled: false,
|
|
64
|
+
showRepair: true,
|
|
65
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
66
|
+
},
|
|
67
|
+
diagnostics: ["work_build_failed"]
|
|
68
|
+
};
|
|
69
|
+
sections.work = sectionErr("work_build_failed");
|
|
70
|
+
diagnostics.push("work_build_failed");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let connectionsReport;
|
|
74
|
+
try {
|
|
75
|
+
connectionsReport = await buildConnections({
|
|
76
|
+
homeDir,
|
|
77
|
+
workspaceRoot: cwd,
|
|
78
|
+
client,
|
|
79
|
+
packageRoot,
|
|
80
|
+
packageName,
|
|
81
|
+
cliVersion
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
connectionsReport = {
|
|
85
|
+
ok: false,
|
|
86
|
+
connections: [],
|
|
87
|
+
fleets: [],
|
|
88
|
+
activity: null,
|
|
89
|
+
fleetNote: null,
|
|
90
|
+
orchestratorAuthority: null
|
|
91
|
+
};
|
|
92
|
+
sections.team = sectionErr("team_build_failed");
|
|
93
|
+
diagnostics.push("team_build_failed");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const team = normalizeTeam(connectionsReport);
|
|
97
|
+
if (sections.team.ok && team.platforms.length === 0 && connectionsReport?.ok === false) {
|
|
98
|
+
sections.team = sectionErr(connectionsReport.error ?? "team_empty");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const gentle = await loadWorkflow({ cwd });
|
|
102
|
+
let workflow = gentle.workflow;
|
|
103
|
+
if (workflow && gentle.provider && workflow.provider == null) {
|
|
104
|
+
workflow = { ...workflow, provider: gentle.provider };
|
|
105
|
+
}
|
|
106
|
+
if (!gentle.ok) {
|
|
107
|
+
sections.workflow = sectionErr(gentle.error ?? "gentle_unavailable");
|
|
108
|
+
diagnostics.push(gentle.error ?? "gentle_unavailable");
|
|
109
|
+
if (!workflow?.active && !workflow?.review) {
|
|
110
|
+
workflow = {
|
|
111
|
+
kind: WORKFLOW_KIND.NONE,
|
|
112
|
+
active: false,
|
|
113
|
+
label: NO_ACTIVE_WORKFLOW,
|
|
114
|
+
phase: null,
|
|
115
|
+
nextTransition: null,
|
|
116
|
+
changeName: null,
|
|
117
|
+
review: null,
|
|
118
|
+
provider: gentle.provider ?? workflow?.provider ?? null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const attention = buildAttention({
|
|
124
|
+
work,
|
|
125
|
+
workflow,
|
|
126
|
+
team,
|
|
127
|
+
connections: team.connections
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const ok = sections.work.ok || sections.team.ok;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
schema: CONTROL_PLANE_SCHEMA,
|
|
134
|
+
ok,
|
|
135
|
+
generatedAt: new Date().toISOString(),
|
|
136
|
+
client,
|
|
137
|
+
work,
|
|
138
|
+
workflow,
|
|
139
|
+
team,
|
|
140
|
+
attention,
|
|
141
|
+
sections,
|
|
142
|
+
diagnostics
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { normalizeTeam };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveHomeDir } from "../paths.js";
|
|
2
|
+
import { printJson } from "../json-output.js";
|
|
3
|
+
import { commandHeader } from "../brand/index.js";
|
|
4
|
+
import { buildControlPlaneReport } from "./build-report.js";
|
|
5
|
+
|
|
6
|
+
export async function runControlPlaneCli(options = {}) {
|
|
7
|
+
const report = await buildControlPlaneReport({
|
|
8
|
+
homeDir: options.homeDir ?? resolveHomeDir(),
|
|
9
|
+
cwd: options.cwd ?? process.cwd(),
|
|
10
|
+
client: options.mcpClient ?? options.client ?? "cursor",
|
|
11
|
+
provider: options.provider ?? "cursor",
|
|
12
|
+
packageRoot: options.packageRoot ?? null,
|
|
13
|
+
packageName: options.packageName ?? null,
|
|
14
|
+
cliVersion: options.cliVersion ?? null
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (options.json) {
|
|
18
|
+
printJson(report);
|
|
19
|
+
return report;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(commandHeader("Control plane"));
|
|
23
|
+
console.log(`Work · ${report.work?.integration?.state ?? "—"}`);
|
|
24
|
+
console.log(`Workflow · ${report.workflow?.label ?? "—"}`);
|
|
25
|
+
const platforms = report.team?.platforms ?? [];
|
|
26
|
+
console.log(`Team · ${platforms.length} platform(s)`);
|
|
27
|
+
for (const p of platforms) {
|
|
28
|
+
console.log(` ${p.platform} · ${p.honesty} · agents=${p.agents?.length ?? 0}`);
|
|
29
|
+
}
|
|
30
|
+
const primary = report.attention?.primaryActions ?? [];
|
|
31
|
+
if (primary.length) {
|
|
32
|
+
console.log("Primary");
|
|
33
|
+
for (const action of primary) console.log(`- ${action.label}`);
|
|
34
|
+
}
|
|
35
|
+
return report;
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kairo.control-plane/v1 — atomic panel report (work + workflow + team + attention).
|
|
3
|
+
*/
|
|
4
|
+
export const CONTROL_PLANE_SCHEMA = "kairo.control-plane/v1";
|
|
5
|
+
|
|
6
|
+
export const WORKFLOW_KIND = Object.freeze({
|
|
7
|
+
SDD: "sdd",
|
|
8
|
+
REVIEW: "review",
|
|
9
|
+
DIRECT: "direct",
|
|
10
|
+
DELEGATED: "delegated",
|
|
11
|
+
NONE: "none"
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const HONESTY = Object.freeze({
|
|
15
|
+
LIVE: "live",
|
|
16
|
+
DECLARED: "declared",
|
|
17
|
+
OPAQUE: "opaque"
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const NO_ACTIVE_WORKFLOW = "No active workflow";
|
|
21
|
+
|
|
22
|
+
/** Control-plane Gentle provider — distinct from observability probe states. */
|
|
23
|
+
export const PROVIDER = Object.freeze({
|
|
24
|
+
CONNECTED: "connected",
|
|
25
|
+
UPGRADE_REQUIRED: "upgrade_required",
|
|
26
|
+
UNAVAILABLE: "unavailable",
|
|
27
|
+
INCOMPATIBLE: "incompatible"
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const PROVIDER_ERROR = Object.freeze({
|
|
31
|
+
[PROVIDER.UPGRADE_REQUIRED]: "gentle_upgrade_required",
|
|
32
|
+
[PROVIDER.UNAVAILABLE]: "gentle_unavailable",
|
|
33
|
+
[PROVIDER.INCOMPATIBLE]: "gentle_incompatible"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const GENTLE_INSTALL_HINT = "Install gentle-ai separately, then Refresh.";
|
|
37
|
+
export const GENTLE_UPGRADE_LABEL = "Upgrade Gentle";
|
|
38
|
+
export const GENTLE_DOCTOR_COMMAND = "gentle-ai doctor";
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only Gentle adapters for control-plane workflow/review.
|
|
3
|
+
* Negotiate capabilities before any workflow fetch. Never invent authority.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { isAbsolute } from "node:path";
|
|
7
|
+
import { probeGentle, resolveGentleBinaryPath } from "../observability/gentle-probe.js";
|
|
8
|
+
import { WORKFLOW_KIND, PROVIDER } from "./constants.js";
|
|
9
|
+
import {
|
|
10
|
+
emptyGentleWorkflow,
|
|
11
|
+
mapGentleProviderState,
|
|
12
|
+
providerError
|
|
13
|
+
} from "./provider.js";
|
|
14
|
+
import {
|
|
15
|
+
argvFromBootstrap,
|
|
16
|
+
bootstrapCommandFromProbe,
|
|
17
|
+
mapOfficialReviewStatus
|
|
18
|
+
} from "./review-status.js";
|
|
19
|
+
import {
|
|
20
|
+
SDD_STATUS_ARGS,
|
|
21
|
+
applySddProjection,
|
|
22
|
+
mapOfficialSddStatus
|
|
23
|
+
} from "./sdd-status.js";
|
|
24
|
+
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
26
|
+
|
|
27
|
+
export function parseStrictJson(text) {
|
|
28
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(text.trim());
|
|
31
|
+
return parsed != null && typeof parsed === "object" ? parsed : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractJsonPayload(text) {
|
|
38
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
39
|
+
const strict = parseStrictJson(text);
|
|
40
|
+
if (strict) return strict;
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
const fenced = trimmed.match(/```json\s*([\s\S]*?)```/i);
|
|
43
|
+
if (fenced?.[1]) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(fenced[1].trim());
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const start = trimmed.indexOf("{");
|
|
51
|
+
const end = trimmed.lastIndexOf("}");
|
|
52
|
+
if (start >= 0 && end > start) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolvedGentleBinary(probed, command) {
|
|
63
|
+
if (typeof command === "string" && isAbsolute(command)) return command;
|
|
64
|
+
const path = probed?.evidence?.find((row) => row?.kind === "binary")?.path;
|
|
65
|
+
return typeof path === "string" && isAbsolute(path) ? path : resolveGentleBinaryPath();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function runGentleCommand(args, {
|
|
69
|
+
cwd = process.cwd(),
|
|
70
|
+
env = process.env,
|
|
71
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
72
|
+
spawn = spawnSync,
|
|
73
|
+
command,
|
|
74
|
+
strict = false
|
|
75
|
+
} = {}) {
|
|
76
|
+
try {
|
|
77
|
+
if (typeof command !== "string" || !isAbsolute(command)) {
|
|
78
|
+
return { ok: false, error: "gentle_incompatible", payload: null };
|
|
79
|
+
}
|
|
80
|
+
const result = spawn(command, args, {
|
|
81
|
+
cwd,
|
|
82
|
+
env,
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
timeout: timeoutMs,
|
|
85
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
86
|
+
shell: false
|
|
87
|
+
});
|
|
88
|
+
if (result.error) {
|
|
89
|
+
return { ok: false, error: result.error.message || "gentle_spawn_failed", payload: null };
|
|
90
|
+
}
|
|
91
|
+
const payload = strict
|
|
92
|
+
? parseStrictJson(result.stdout ?? "")
|
|
93
|
+
: (extractJsonPayload(result.stdout ?? "") ?? extractJsonPayload(result.stderr ?? ""));
|
|
94
|
+
if (!payload) {
|
|
95
|
+
return { ok: false, error: "gentle_parse_failed", payload: null, status: result.status };
|
|
96
|
+
}
|
|
97
|
+
if (result.status !== 0 && result.status != null) {
|
|
98
|
+
return { ok: false, error: "gentle_nonzero_status", payload, status: result.status };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, payload, status: result.status, error: null };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: error instanceof Error ? error.message : String(error),
|
|
105
|
+
payload: null
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function applyOfficialReview(workflow, mapped) {
|
|
111
|
+
workflow.review = mapped.review;
|
|
112
|
+
workflow.nextTransition = mapped.nextTransition;
|
|
113
|
+
if (workflow.kind === WORKFLOW_KIND.NONE && mapped.nextTransition != null) {
|
|
114
|
+
workflow.kind = WORKFLOW_KIND.REVIEW;
|
|
115
|
+
workflow.active = mapped.nextTransition?.kind === "execute" || mapped.review != null;
|
|
116
|
+
workflow.label = "Review";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function loadGentleWorkflow({
|
|
121
|
+
cwd,
|
|
122
|
+
env,
|
|
123
|
+
timeoutMs,
|
|
124
|
+
spawn,
|
|
125
|
+
command,
|
|
126
|
+
probe = probeGentle,
|
|
127
|
+
runCommand = runGentleCommand
|
|
128
|
+
} = {}) {
|
|
129
|
+
const probed = await probe({ cwd, env });
|
|
130
|
+
const provider = mapGentleProviderState(probed);
|
|
131
|
+
if (provider !== PROVIDER.CONNECTED) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: providerError(provider, probed),
|
|
135
|
+
provider,
|
|
136
|
+
workflow: emptyGentleWorkflow({ provider })
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const parsed = argvFromBootstrap(bootstrapCommandFromProbe(probed), {
|
|
141
|
+
repo: cwd ?? process.cwd(),
|
|
142
|
+
binaryPath: resolvedGentleBinary(probed, command)
|
|
143
|
+
});
|
|
144
|
+
if (!parsed.ok) {
|
|
145
|
+
const incompatible = PROVIDER.INCOMPATIBLE;
|
|
146
|
+
return {
|
|
147
|
+
ok: false, error: "gentle_incompatible", provider: incompatible,
|
|
148
|
+
workflow: emptyGentleWorkflow({ provider: incompatible })
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const workflow = emptyGentleWorkflow({ provider });
|
|
153
|
+
const sdd = runCommand([...SDD_STATUS_ARGS], {
|
|
154
|
+
cwd, env, timeoutMs, spawn, command: parsed.binary, strict: true
|
|
155
|
+
});
|
|
156
|
+
if (sdd.ok) {
|
|
157
|
+
const mappedSdd = mapOfficialSddStatus(sdd.payload);
|
|
158
|
+
if (mappedSdd.ok) applySddProjection(workflow, mappedSdd.projection);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const reviewRun = runCommand(parsed.argv, {
|
|
162
|
+
cwd, env, timeoutMs, spawn, command: parsed.binary, strict: true
|
|
163
|
+
});
|
|
164
|
+
if (reviewRun.ok) {
|
|
165
|
+
const mapped = mapOfficialReviewStatus(reviewRun.payload);
|
|
166
|
+
if (mapped.ok) applyOfficialReview(workflow, mapped);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!sdd.ok && workflow.kind === WORKFLOW_KIND.NONE && workflow.review == null) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
error: sdd.error ?? "gentle_sdd_unavailable",
|
|
173
|
+
provider,
|
|
174
|
+
workflow
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
error: sdd.ok ? null : (sdd.error ?? "gentle_sdd_unavailable"),
|
|
180
|
+
provider,
|
|
181
|
+
workflow
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map Gentle observability probe → control-plane provider.
|
|
3
|
+
* Version is evidence only; v1 contract is upgrade, unknown schema fails closed.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
NO_ACTIVE_WORKFLOW,
|
|
7
|
+
PROVIDER,
|
|
8
|
+
PROVIDER_ERROR,
|
|
9
|
+
WORKFLOW_KIND
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
|
|
12
|
+
const LEGACY_SCHEMA = "gentle-ai.review-integration.capabilities/v1";
|
|
13
|
+
const LEGACY_CONTRACT = "gentle-ai.review-integration/v1";
|
|
14
|
+
|
|
15
|
+
export function emptyGentleWorkflow({ provider = null } = {}) {
|
|
16
|
+
return {
|
|
17
|
+
kind: WORKFLOW_KIND.NONE,
|
|
18
|
+
active: false,
|
|
19
|
+
label: NO_ACTIVE_WORKFLOW,
|
|
20
|
+
phase: null,
|
|
21
|
+
nextTransition: null,
|
|
22
|
+
changeName: null,
|
|
23
|
+
review: null,
|
|
24
|
+
sdd: null,
|
|
25
|
+
provider
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isRecognizedLegacyContract(probe) {
|
|
30
|
+
const evidenceSchemas = (probe?.evidence ?? [])
|
|
31
|
+
.map((row) => row?.schema)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" ");
|
|
34
|
+
const blob = `${(probe?.diagnostics ?? []).join(" ")} ${evidenceSchemas}`;
|
|
35
|
+
return blob.includes(LEGACY_SCHEMA)
|
|
36
|
+
|| blob.includes(LEGACY_CONTRACT)
|
|
37
|
+
|| /protocol\.major mismatch: got 1\b/.test(blob);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mapGentleProviderState(probe) {
|
|
41
|
+
if (probe == null || typeof probe !== "object" || Array.isArray(probe)) {
|
|
42
|
+
return PROVIDER.INCOMPATIBLE;
|
|
43
|
+
}
|
|
44
|
+
if (probe.state === "missing") return PROVIDER.UNAVAILABLE;
|
|
45
|
+
if (probe.state === "available" && probe.contractCompatible === true) {
|
|
46
|
+
return PROVIDER.CONNECTED;
|
|
47
|
+
}
|
|
48
|
+
if (probe.state === "error") {
|
|
49
|
+
const blob = `${probe.error ?? ""} ${(probe.diagnostics ?? []).join(" ")}`;
|
|
50
|
+
if (/parse|not an object|invalid capabilities/i.test(blob)) {
|
|
51
|
+
return PROVIDER.INCOMPATIBLE;
|
|
52
|
+
}
|
|
53
|
+
return PROVIDER.UNAVAILABLE;
|
|
54
|
+
}
|
|
55
|
+
if (probe.state === "incompatible") {
|
|
56
|
+
return isRecognizedLegacyContract(probe)
|
|
57
|
+
? PROVIDER.UPGRADE_REQUIRED
|
|
58
|
+
: PROVIDER.INCOMPATIBLE;
|
|
59
|
+
}
|
|
60
|
+
return PROVIDER.INCOMPATIBLE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function providerError(provider, probe = null) {
|
|
64
|
+
if (provider === PROVIDER.CONNECTED) return null;
|
|
65
|
+
if (provider === PROVIDER.UNAVAILABLE && probe?.state === "error") {
|
|
66
|
+
return "gentle_capabilities_failed";
|
|
67
|
+
}
|
|
68
|
+
return PROVIDER_ERROR[provider] ?? "gentle_incompatible";
|
|
69
|
+
}
|