@kal-elsam/kairo-runtime 0.15.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 CHANGED
@@ -5,6 +5,32 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.16.0 — 2026-08-13 (Kairo Runtime)
9
+
10
+ Minor release. Public `kairo control-plane` command and
11
+ `kairo.control-plane/v1` Gentle companion report. Publish tag:
12
+ `kairo-runtime-v0.16.0`. Extension VSIX stays out of this unit.
13
+
14
+ ### Added
15
+
16
+ - `kairo control-plane [--json] [--client cursor]`: atomic panel report
17
+ (work + Gentle workflow + team + attention) as `kairo.control-plane/v1`.
18
+ - Negotiate `gentle-ai.review-integration/v2` (protocol 2.0 and 2.1) before
19
+ any workflow fetch. Provider states: `connected`, `upgrade_required`,
20
+ `unavailable`, `incompatible`.
21
+ - Official `review status` from Gentle's announced bootstrap argv; pass
22
+ `next_transition` through unaltered. Receipt/gate only when Gentle publishes
23
+ them.
24
+ - `sdd-status --json` projection copies `changeName` / `nextRecommended` only.
25
+
26
+ ### Docs
27
+
28
+ - Gentle companion boundary: Kairo observes `gentle-ai.review-integration/v2`
29
+ and projects official `next_transition` / `sdd-status --json`. Freeze
30
+ `kairo review`, Cockpit receipts, orchestrator, and intelligence routing so
31
+ they do not feed the panel Workflow. Propose upstream `gentle-ai observe --json`
32
+ in Kairo docs only (`docs/gentle-companion.md`).
33
+
8
34
  ## 0.15.0 — 2026-08-12 (Kairo Runtime)
9
35
 
10
36
  Minor release. Declared Fleet board + model configure, and the Cursor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
package/src/cli.js CHANGED
@@ -178,6 +178,15 @@ export async function runCli(argv) {
178
178
  });
179
179
  return;
180
180
  }
181
+ case "control-plane": {
182
+ const { runControlPlaneCli } = await import("./global/control-plane/cli.js");
183
+ await runControlPlaneCli({
184
+ cwd: optionsWithPolicy.cwd,
185
+ json: optionsWithPolicy.json === true,
186
+ mcpClient: optionsWithPolicy.mcpClient ?? "cursor"
187
+ });
188
+ return;
189
+ }
181
190
  case "fleet": {
182
191
  const fleetAction = optionsWithPolicy.fleetAction ?? "show";
183
192
  if (fleetAction === "set") {
@@ -1052,6 +1061,7 @@ function normalizeCommand(command) {
1052
1061
  if (command === "mcp") return "mcp";
1053
1062
  if (command === "connections") return "connections";
1054
1063
  if (command === "next") return "next";
1064
+ if (command === "control-plane") return "control-plane";
1055
1065
  if (command === "fleet") return "fleet";
1056
1066
  if (command === "intelligence" || command === "intel") return "intelligence";
1057
1067
  if (command === "setup") return "setup";
@@ -116,6 +116,7 @@ Bootstrap: see README.md and docs/install.md (curl install.sh or npx ${PACKAGE_N
116
116
  ${cli} components rollback engram-memory|sdd-core --receipt <id> [--dry-run|--yes] [--json]
117
117
  ${cli} connections [--json] [--client cursor]
118
118
  ${cli} next [--json] [--client cursor]
119
+ ${cli} control-plane [--json] [--client cursor]
119
120
  ${cli} fleet [--json] [--verbose] [--include-variants]
120
121
  ${cli} fleet models [--profile] [--json]
121
122
  ${cli} fleet configure [--platforms claude,opencode,cursor|codex] [--from profile|gentle] [--codex-model <id>] [--assignments a=b,...] [--yes] [--json]
@@ -168,6 +169,7 @@ Commands:
168
169
  components List, validate, scaffold, pack, import, or configure integrations (Engram, SDD).
169
170
  connections Companion chips + MCP registration (IDE panel).
170
171
  next Selected work snapshot + integration state (panel contract).
172
+ control-plane Atomic panel report (work + Gentle workflow + team + attention).
171
173
  fleet Declared fleet floor + working activity; configure/set models across CLIs.
172
174
  fleet models [--profile] available vs enabled per tool
173
175
  fleet configure one plan for Claude+OpenCode+Cursor (profile)
@@ -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
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Official Gentle review status v2/v3 only. Never read authority inventory.
3
+ */
4
+ import { isAbsolute } from "node:path";
5
+ import { SUPPORTED_CONTRACT } from "../observability/gentle-probe.js";
6
+
7
+ export const REVIEW_STATUS_SCHEMAS = Object.freeze([
8
+ "gentle-ai.review-integration.status/v2",
9
+ "gentle-ai.review-integration.status/v3"
10
+ ]);
11
+
12
+ const INVENTORY_SCHEMA = "gentle-ai.review-authority-status/v1";
13
+ const UNSAFE_TOKEN = /[|;&$`\n\r]/;
14
+ const PLACEHOLDER = /^<[^>]+>$/;
15
+
16
+ export const GENTLE_224_BOOTSTRAP =
17
+ "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --next-transition";
18
+ export const GENTLE_230_BOOTSTRAP =
19
+ "gentle-ai review status --cwd <repo> --contract gentle-ai.review-integration/v2 --agent claude-code --next-transition";
20
+
21
+ const failArgv = () => ({ ok: false, error: "gentle_incompatible", binary: null, argv: null });
22
+
23
+ export function bootstrapCommandFromProbe(probe) {
24
+ return probe?.evidence?.find((row) => row?.kind === "bootstrap" && row.command)?.command ?? null;
25
+ }
26
+
27
+ export function argvFromBootstrap(command, { repo, binaryPath }) {
28
+ if (typeof command !== "string" || !command.trim() || UNSAFE_TOKEN.test(command)) return failArgv();
29
+ if (typeof binaryPath !== "string" || !isAbsolute(binaryPath)) return failArgv();
30
+ if (typeof repo !== "string" || !repo) return failArgv();
31
+ const tokens = command.trim().split(/\s+/);
32
+ if (tokens[0] !== "gentle-ai") return failArgv();
33
+ const args = [];
34
+ const rest = tokens.slice(1);
35
+ for (let i = 0; i < rest.length; i += 1) {
36
+ const tok = rest[i];
37
+ if (UNSAFE_TOKEN.test(tok)) return failArgv();
38
+ if (tok === "--cwd") {
39
+ const next = rest[i + 1];
40
+ const needsRepo = next === "<repo>" || next == null || next.startsWith("-");
41
+ args.push("--cwd", needsRepo ? repo : next);
42
+ if (!needsRepo || next === "<repo>") i += 1;
43
+ continue;
44
+ }
45
+ if (PLACEHOLDER.test(tok)) return failArgv();
46
+ args.push(tok);
47
+ }
48
+ if (args[0] !== "review" || args[1] !== "status") return failArgv();
49
+ return { ok: true, error: null, binary: binaryPath, argv: args };
50
+ }
51
+
52
+ function isObject(value) {
53
+ return value != null && typeof value === "object" && !Array.isArray(value);
54
+ }
55
+
56
+ function publishedReceipt(receiptField) {
57
+ if (typeof receiptField === "string" && receiptField) return receiptField;
58
+ if (!isObject(receiptField)) return null;
59
+ if (typeof receiptField.id === "string" && receiptField.id) return receiptField.id;
60
+ if (typeof receiptField.digest === "string" && receiptField.digest) return receiptField.digest;
61
+ return null;
62
+ }
63
+
64
+ function publishedGate(payload) {
65
+ if (typeof payload.gate === "string" && payload.gate) return payload.gate;
66
+ if (isObject(payload.receipt) && typeof payload.receipt.gate === "string" && payload.receipt.gate) {
67
+ return payload.receipt.gate;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Fail closed unless schema/contract are official. Pass next_transition through unaltered.
74
+ */
75
+ export function mapOfficialReviewStatus(payload) {
76
+ if (!isObject(payload)) {
77
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
78
+ }
79
+ if (
80
+ payload.schema === INVENTORY_SCHEMA
81
+ || payload.authoritative === true
82
+ || Array.isArray(payload.entries)
83
+ ) {
84
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
85
+ }
86
+ if (!REVIEW_STATUS_SCHEMAS.includes(payload.schema)) {
87
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
88
+ }
89
+ if (payload.contract != null && payload.contract !== SUPPORTED_CONTRACT) {
90
+ return { ok: false, error: "gentle_incompatible", review: null, nextTransition: null };
91
+ }
92
+
93
+ const nextTransition = Object.prototype.hasOwnProperty.call(payload, "next_transition")
94
+ ? payload.next_transition
95
+ : null;
96
+ const receipt = publishedReceipt(payload.receipt);
97
+ const gate = publishedGate(payload);
98
+ const applicability = typeof payload.applicability === "string" ? payload.applicability : null;
99
+ const action = typeof payload.action === "string" ? payload.action : null;
100
+ const receiptStatus = isObject(payload.receipt) && typeof payload.receipt.status === "string"
101
+ ? payload.receipt.status
102
+ : null;
103
+
104
+ const review = (receipt || gate || applicability || action)
105
+ ? {
106
+ lineageId: null,
107
+ state: applicability ?? action,
108
+ status: receiptStatus,
109
+ receipt,
110
+ gate
111
+ }
112
+ : null;
113
+
114
+ return { ok: true, error: null, review, nextTransition };
115
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Official `sdd-status --json` projection. No inferred phase/route/next.
3
+ */
4
+ import { NO_ACTIVE_WORKFLOW, WORKFLOW_KIND } from "./constants.js";
5
+
6
+ export const SDD_STATUS_SCHEMA_PREFIX = "gentle-ai.sdd-status";
7
+ export const SDD_STATUS_ARGS = Object.freeze(["sdd-status", "--json"]);
8
+
9
+ function isObject(value) {
10
+ return value != null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ export function mapOfficialSddStatus(payload) {
14
+ if (!isObject(payload)) {
15
+ return { ok: false, error: "gentle_incompatible", projection: null };
16
+ }
17
+ const schemaName = typeof payload.schemaName === "string"
18
+ ? payload.schemaName
19
+ : (typeof payload.schema === "string" ? payload.schema : null);
20
+ if (!schemaName || !schemaName.startsWith(SDD_STATUS_SCHEMA_PREFIX)) {
21
+ return { ok: false, error: "gentle_incompatible", projection: null };
22
+ }
23
+ const changeName = typeof payload.changeName === "string" && payload.changeName
24
+ ? payload.changeName
25
+ : null;
26
+ const nextRecommended = typeof payload.nextRecommended === "string" && payload.nextRecommended
27
+ ? payload.nextRecommended
28
+ : null;
29
+ return {
30
+ ok: true,
31
+ error: null,
32
+ projection: { schemaName, changeName, nextRecommended }
33
+ };
34
+ }
35
+
36
+ export function applySddProjection(workflow, projection) {
37
+ workflow.sdd = projection;
38
+ workflow.changeName = projection.changeName;
39
+ workflow.phase = null;
40
+ if (projection.changeName) {
41
+ workflow.kind = WORKFLOW_KIND.SDD;
42
+ workflow.active = true;
43
+ workflow.label = "SDD";
44
+ } else if (workflow.kind !== WORKFLOW_KIND.REVIEW) {
45
+ workflow.kind = WORKFLOW_KIND.NONE;
46
+ workflow.active = false;
47
+ workflow.label = NO_ACTIVE_WORKFLOW;
48
+ }
49
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Normalize declared fleets into honest control-plane team nodes.
3
+ * Live requires OpenCode active-agent evidence — never idle sessions.
4
+ */
5
+ import { HONESTY } from "./constants.js";
6
+
7
+ function honestyForNode(node, { platform, hasLiveActivity }) {
8
+ if (node?.opaque === true) return HONESTY.OPAQUE;
9
+ if (platform === "opencode" && hasLiveActivity) return HONESTY.LIVE;
10
+ return HONESTY.DECLARED;
11
+ }
12
+
13
+ export function normalizeTeam(connectionsReport = {}) {
14
+ const fleets = Array.isArray(connectionsReport.fleets) ? connectionsReport.fleets : [];
15
+ const activity = connectionsReport.activity ?? null;
16
+ const hasLiveActivity = Boolean(
17
+ activity
18
+ && (
19
+ (Array.isArray(activity.agents) && activity.agents.some((a) => a?.state === "active"))
20
+ || (typeof activity.activeCount === "number" && activity.activeCount > 0)
21
+ || activity.active === true
22
+ )
23
+ );
24
+
25
+ // Cursor configured agents are declared topology (not live); only opaque nodes stay opaque.
26
+ const platforms = fleets.map((fleet) => {
27
+ const platform = fleet?.platform ?? "unknown";
28
+ const orch = fleet?.orchestrator ?? null;
29
+ return {
30
+ platform,
31
+ honesty: honestyForNode(fleet, { platform, hasLiveActivity }),
32
+ source: fleet?.source ?? null,
33
+ orchestrator: orch
34
+ ? {
35
+ id: orch.id ?? null,
36
+ model: orch.modelShort ?? orch.model ?? null,
37
+ honesty: honestyForNode(orch, { platform, hasLiveActivity }),
38
+ role: orch.mode ?? "orchestrator"
39
+ }
40
+ : null,
41
+ agents: (Array.isArray(fleet?.minions) ? fleet.minions : []).map((m) => ({
42
+ id: m.id,
43
+ model: m.modelShort ?? m.model ?? null,
44
+ role: m.role ?? null,
45
+ honesty: m?.opaque === true && platform === "cursor"
46
+ ? HONESTY.DECLARED
47
+ : honestyForNode(m, { platform, hasLiveActivity: false })
48
+ }))
49
+ };
50
+ });
51
+
52
+ return {
53
+ platforms,
54
+ // Only surface activity when there is live evidence (active workers).
55
+ activity: hasLiveActivity ? activity : null,
56
+ fleetNote: connectionsReport.fleetNote
57
+ ?? "Declared config topology — not live token usage.",
58
+ orchestratorAuthority: connectionsReport.orchestratorAuthority ?? null,
59
+ connections: Array.isArray(connectionsReport.connections)
60
+ ? connectionsReport.connections
61
+ : []
62
+ };
63
+ }
@@ -7,8 +7,15 @@ import {
7
7
  import { normalizeProbeResult } from "./probe-contract.js";
8
8
 
9
9
  export const SUPPORTED_PROTOCOL = Object.freeze({ major: 2, minor: 0 });
10
+ export const SUPPORTED_PROTOCOL_MINORS = Object.freeze([0, 1]);
10
11
  export const SUPPORTED_SCHEMA = "gentle-ai.review-integration.capabilities/v2";
12
+ export const SUPPORTED_SCHEMA_V21 = "gentle-ai.review-integration.capabilities/v2.1";
13
+ export const SUPPORTED_CAPABILITY_SCHEMAS = Object.freeze([
14
+ SUPPORTED_SCHEMA,
15
+ SUPPORTED_SCHEMA_V21
16
+ ]);
11
17
  export const SUPPORTED_CONTRACT = "gentle-ai.review-integration/v2";
18
+ export const ADDITIVE_MINOR_POLICY = "optional-fields-only";
12
19
  export const SUPPORTED_MANDATORY_FEATURES = Object.freeze([
13
20
  "compact_v2_authority", "exact_receipt_replay", "five_delivery_gates",
14
21
  "immutable_snapshot", "legacy_v1_target_scoped_read_only",
@@ -51,7 +58,7 @@ export function evaluateGentleCapabilities(payload) {
51
58
  diagnostics: ["Capabilities payload is not an object."]
52
59
  });
53
60
  }
54
- if (payload.schema !== SUPPORTED_SCHEMA) {
61
+ if (!SUPPORTED_CAPABILITY_SCHEMAS.includes(payload.schema)) {
55
62
  diagnostics.push(`schema mismatch: got ${String(payload.schema)}`);
56
63
  }
57
64
  if (payload.contract !== SUPPORTED_CONTRACT) {
@@ -60,9 +67,30 @@ export function evaluateGentleCapabilities(payload) {
60
67
  if (payload.protocol?.major !== SUPPORTED_PROTOCOL.major) {
61
68
  diagnostics.push(`protocol.major mismatch: got ${String(payload.protocol?.major)}`);
62
69
  }
63
- if (payload.protocol?.minor !== SUPPORTED_PROTOCOL.minor) {
70
+ if (!SUPPORTED_PROTOCOL_MINORS.includes(payload.protocol?.minor)) {
64
71
  diagnostics.push(`protocol.minor mismatch: got ${String(payload.protocol?.minor)}`);
65
72
  }
73
+ const additivePolicy = payload.compatibility?.additive_minor_policy;
74
+ if (additivePolicy != null && additivePolicy !== ADDITIVE_MINOR_POLICY) {
75
+ diagnostics.push(`additive_minor_policy mismatch: got ${String(additivePolicy)}`);
76
+ }
77
+ if (typeof payload.bootstrap?.command === "string" && payload.bootstrap.command) {
78
+ evidence.push({
79
+ kind: "bootstrap",
80
+ command: payload.bootstrap.command,
81
+ required_feature: payload.bootstrap.required_feature ?? null
82
+ });
83
+ }
84
+ const requiredFeature = payload.bootstrap?.required_feature;
85
+ if (typeof requiredFeature === "string" && requiredFeature) {
86
+ const named = [
87
+ ...(Array.isArray(payload.features?.mandatory) ? payload.features.mandatory : []),
88
+ ...(Array.isArray(payload.features?.optional) ? payload.features.optional : [])
89
+ ];
90
+ if (!named.some((feature) => feature?.name === requiredFeature && feature.supported === true)) {
91
+ diagnostics.push(`bootstrap required_feature not supported: ${requiredFeature}`);
92
+ }
93
+ }
66
94
  const mandatory = payload.features?.mandatory;
67
95
  if (!Array.isArray(mandatory)) {
68
96
  diagnostics.push("features.mandatory must be an array.");
@@ -24,7 +24,8 @@ export {
24
24
  runPassiveObservabilitySnapshot
25
25
  } from "./passive-snapshot-flight.js";
26
26
  export {
27
- SUPPORTED_PROTOCOL, SUPPORTED_SCHEMA, SUPPORTED_CONTRACT,
27
+ SUPPORTED_PROTOCOL, SUPPORTED_PROTOCOL_MINORS, SUPPORTED_SCHEMA, SUPPORTED_SCHEMA_V21,
28
+ SUPPORTED_CAPABILITY_SCHEMAS, SUPPORTED_CONTRACT, ADDITIVE_MINOR_POLICY,
28
29
  SUPPORTED_MANDATORY_FEATURES, evaluateGentleCapabilities, probeGentle, createGentleProbe,
29
30
  resolveGentleBinaryPath
30
31
  } from "./gentle-probe.js";