@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 +26 -0
- package/package.json +1 -1
- package/src/cli.js +10 -0
- package/src/global/cli-help.js +2 -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/observability/gentle-probe.js +30 -2
- package/src/global/observability/index.js +2 -1
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.
|
|
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";
|
package/src/global/cli-help.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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";
|