@kal-elsam/kairo-runtime 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +124 -17
  2. package/global-template/components/catalog.json +63 -6
  3. package/global-template/components/sdd-core/personas/teaching.md +42 -0
  4. package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
  5. package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
  6. package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
  7. package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
  8. package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
  9. package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
  10. package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
  11. package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
  12. package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
  13. package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
  14. package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
  15. package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
  16. package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
  17. package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
  18. package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
  19. package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
  20. package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
  21. package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
  22. package/package.json +1 -1
  23. package/scripts/cockpit-smoke.mjs +29 -8
  24. package/scripts/smoke-test.sh +9 -0
  25. package/src/cli.js +54 -5
  26. package/src/global/action-planner.js +17 -4
  27. package/src/global/component-authoring.js +1 -2
  28. package/src/global/component-ecosystem-checks.js +74 -15
  29. package/src/global/component-health.js +56 -0
  30. package/src/global/component-integration-cli.js +302 -0
  31. package/src/global/component-manifest.js +214 -0
  32. package/src/global/component-registry.js +19 -2
  33. package/src/global/component-resolve.js +51 -0
  34. package/src/global/components/sdd-core.js +6 -0
  35. package/src/global/control-plane-proposals.js +229 -0
  36. package/src/global/control-plane-snapshot.js +212 -0
  37. package/src/global/dashboard-guidance.js +27 -3
  38. package/src/global/diff.js +2 -5
  39. package/src/global/global-cli.js +16 -1
  40. package/src/global/global-doctor.js +75 -2
  41. package/src/global/global-installer.js +43 -6
  42. package/src/global/governance-actions.js +212 -0
  43. package/src/global/governance-repair.js +13 -0
  44. package/src/global/ink/cockpit-changes.js +124 -0
  45. package/src/global/ink/cockpit-control-center.js +106 -0
  46. package/src/global/ink/cockpit-controller.js +17 -0
  47. package/src/global/ink/cockpit-enter.js +27 -0
  48. package/src/global/ink/cockpit-focus.js +7 -1
  49. package/src/global/ink/cockpit-models.js +98 -46
  50. package/src/global/ink/cockpit-proposals.js +82 -0
  51. package/src/global/ink/cockpit-recovery.js +120 -0
  52. package/src/global/ink/cockpit-runs.js +47 -0
  53. package/src/global/ink/cockpit-scan.js +60 -0
  54. package/src/global/ink/cockpit-views.js +144 -74
  55. package/src/global/ink/orchestrator-app.js +156 -42
  56. package/src/global/ink/orchestrator-state.js +6 -0
  57. package/src/global/ink/use-orchestrator-data.js +239 -17
  58. package/src/global/integrations/engram-apply.js +105 -0
  59. package/src/global/integrations/engram-evidence.js +218 -0
  60. package/src/global/integrations/engram-exec.js +107 -0
  61. package/src/global/integrations/engram-plan.js +60 -0
  62. package/src/global/integrations/engram-provider.js +22 -0
  63. package/src/global/integrations/engram-receipts.js +116 -0
  64. package/src/global/integrations/engram-rollback.js +131 -0
  65. package/src/global/integrations/index.js +12 -0
  66. package/src/global/integrations/provider-contract.js +83 -0
  67. package/src/global/integrations/provider-registry.js +47 -0
  68. package/src/global/integrations/sdd-apply.js +190 -0
  69. package/src/global/integrations/sdd-destinations.js +95 -0
  70. package/src/global/integrations/sdd-evidence.js +89 -0
  71. package/src/global/integrations/sdd-fs-guard.js +183 -0
  72. package/src/global/integrations/sdd-lifecycle.js +84 -0
  73. package/src/global/integrations/sdd-persona.js +71 -0
  74. package/src/global/integrations/sdd-plan.js +87 -0
  75. package/src/global/integrations/sdd-provider.js +21 -0
  76. package/src/global/integrations/sdd-receipts.js +44 -0
  77. package/src/global/integrations/sdd-rollback.js +269 -0
  78. package/src/global/integrations/sdd-skill-files.js +61 -0
  79. package/src/global/integrations/sdd-state.js +175 -0
  80. package/src/global/integrations/sdd-verify.js +122 -0
  81. package/src/global/intelligence/backends/opencode-catalog.js +212 -0
  82. package/src/global/intelligence/backends/opencode-cli.js +247 -0
  83. package/src/global/intelligence/backends/opencode-evidence.js +129 -0
  84. package/src/global/intelligence/backends/opencode-http.js +152 -0
  85. package/src/global/intelligence/backends/opencode-invoke.js +83 -0
  86. package/src/global/intelligence/backends/opencode-providers.js +67 -0
  87. package/src/global/intelligence/backends/opencode-runtime.js +242 -0
  88. package/src/global/intelligence/context-compiler.js +95 -50
  89. package/src/global/intelligence/index.js +38 -1
  90. package/src/global/intelligence/orchestrate.js +4 -2
  91. package/src/global/intelligence/registry.js +62 -6
  92. package/src/global/intelligence/router.js +188 -70
  93. package/src/global/intelligence/transport-registry.js +147 -0
  94. package/src/global/intelligence/types.js +21 -2
  95. package/src/global/intelligence-cli.js +48 -17
  96. package/src/global/intelligence-session.js +37 -0
  97. package/src/global/json-output.js +1 -0
  98. package/src/global/load-component-catalog.js +10 -28
  99. package/src/global/load-workspace-component-catalog.js +22 -63
  100. package/src/global/state-migration.js +21 -1
  101. package/src/global/state.js +13 -8
  102. package/src/global/status.js +4 -17
  103. package/src/global/sync.js +2 -5
@@ -0,0 +1,242 @@
1
+ import {
2
+ BACKEND_IDS,
3
+ COST_CLASSES,
4
+ PRIVACY_CLASSES,
5
+ TRANSPORT_KINDS,
6
+ createModelDescriptor
7
+ } from "../types.js";
8
+ import {
9
+ listRegisteredModelIds,
10
+ normalizeModelId,
11
+ resolveRuntimeProduct,
12
+ toRuntimeModelRef
13
+ } from "../transport-registry.js";
14
+ import { CAPABILITY_STATES } from "../../capability-states.js";
15
+ import { isExecutableAvailable } from "../../cli-probe.js";
16
+ import { parseOpencodeJsonEvents, runOpencodeJson, sanitizeCliStderr } from "./opencode-cli.js";
17
+ import {
18
+ BILLING_MODELS,
19
+ ENTITLEMENT_STATES,
20
+ collectOpencodeCliEvidence
21
+ } from "./opencode-evidence.js";
22
+
23
+ const PREAMBLE =
24
+ "Answer with analysis only. Do not modify files, run mutating tools, or change the workspace.";
25
+
26
+ /** Non-mutating OpenCode CLI runtime. Override-only; never auto-routed; never reads auth.json. */
27
+ export function createOpencodeRuntimeBackend({
28
+ env = process.env,
29
+ spawnImpl,
30
+ whichImpl = isExecutableAvailable,
31
+ collectCliEvidence = collectOpencodeCliEvidence
32
+ } = {}) {
33
+ return {
34
+ id: BACKEND_IDS.OPENCODE,
35
+ label: "OpenCode CLI",
36
+ local: false,
37
+
38
+ async detect() {
39
+ const cli = collectCliEvidence({ env, whichImpl });
40
+ if (!cli.cliInstalled) {
41
+ return baseDetect({
42
+ state: CAPABILITY_STATES.UNKNOWN,
43
+ detected: false,
44
+ available: false,
45
+ configured: false,
46
+ entitlement: ENTITLEMENT_STATES.UNKNOWN,
47
+ evidence: evidence(false),
48
+ recommendation:
49
+ "Install and authenticate the OpenCode CLI to use Anthropic/Google models via runtime transport (Kairo does not read OpenCode auth files)."
50
+ });
51
+ }
52
+ const configured = cli.authProviders.length > 0;
53
+ return baseDetect({
54
+ state: CAPABILITY_STATES.DETECTED,
55
+ detected: true,
56
+ available: true,
57
+ configured,
58
+ entitlement: configured
59
+ ? ENTITLEMENT_STATES.UNVERIFIED
60
+ : ENTITLEMENT_STATES.UNKNOWN,
61
+ evidence: evidence(true, cli),
62
+ error: cli.error,
63
+ recommendation: configured
64
+ ? `OpenCode CLI configured providers: ${cli.authProviders.join(", ")}. Entitlement remains ${ENTITLEMENT_STATES.UNVERIFIED}. Runtime invoke needs --cloud-consent --yes.`
65
+ : "OpenCode CLI detected but auth list reported no providers. Run `opencode auth login` (Kairo never reads auth.json)."
66
+ });
67
+ },
68
+
69
+ async listModels() {
70
+ const models = listRegisteredModelIds("zen", { runtimeOnly: true })
71
+ .map((modelId) => runtimeModel("zen", modelId));
72
+ for (const modelId of listRegisteredModelIds("go", { runtimeOnly: true })) {
73
+ if (models.some((entry) => normalizeModelId(entry.modelId) === modelId)) continue;
74
+ models.push(runtimeModel("go", modelId));
75
+ }
76
+ return models;
77
+ },
78
+
79
+ async capabilities() {
80
+ const d = await this.detect();
81
+ return {
82
+ id: BACKEND_IDS.OPENCODE,
83
+ local: false,
84
+ cloud: true,
85
+ requiresApiKey: false,
86
+ requiresConsent: true,
87
+ streaming: false,
88
+ tools: false,
89
+ runtime: true,
90
+ transports: [TRANSPORT_KINDS.RUNTIME],
91
+ billingModel: d.billingModel,
92
+ entitlement: d.entitlement,
93
+ configured: d.configured,
94
+ authenticated: false,
95
+ state: d.state,
96
+ hasApiKey: false,
97
+ mutating: false
98
+ };
99
+ },
100
+
101
+ async invoke(contextPack, request = {}) {
102
+ const detection = await this.detect();
103
+ if (!detection.available) return fail(detection.recommendation);
104
+ const modelRef = resolveModelRef(request.modelId);
105
+ if (!modelRef) {
106
+ return fail(
107
+ "Runtime invoke requires --model <provider/model> (e.g. opencode/claude-haiku-4-5)."
108
+ );
109
+ }
110
+ const timeoutMs = request.timeoutMs ?? 120_000;
111
+ try {
112
+ const result = await runOpencodeJson({
113
+ modelRef,
114
+ prompt: buildPrompt(contextPack, request),
115
+ cwd: request.cwd ?? contextPack?.workspaceRoot ?? process.cwd(),
116
+ env,
117
+ spawnImpl,
118
+ timeoutMs,
119
+ terminationGraceMs: request.terminationGraceMs,
120
+ killGraceMs: request.killGraceMs
121
+ });
122
+ const failure = describeRuntimeFailure(result, timeoutMs);
123
+ if (failure) return fail(failure);
124
+
125
+ const parsed = parseOpencodeJsonEvents(result.stdout);
126
+ if (parsed.error) return fail(parsed.error);
127
+ if (!parsed.content) {
128
+ const detail = sanitizeCliStderr(result.stderr) || "no content";
129
+ return fail(`OpenCode CLI produced no text output (${detail}).`);
130
+ }
131
+ return {
132
+ ok: true,
133
+ backendId: BACKEND_IDS.OPENCODE,
134
+ model: modelRef,
135
+ content: parsed.content,
136
+ usage: {
137
+ ...parsed.usage,
138
+ model: modelRef,
139
+ backendId: BACKEND_IDS.OPENCODE,
140
+ fallbackUsed: false
141
+ },
142
+ raw: {
143
+ events: parsed.events,
144
+ status: result.status,
145
+ signal: result.signal
146
+ }
147
+ };
148
+ } catch (error) {
149
+ if (error?.code === "ENOENT") {
150
+ return fail(
151
+ "OpenCode CLI is not installed. Install OpenCode, authenticate it, then retry with --backend opencode."
152
+ );
153
+ }
154
+ return fail(error?.message ?? String(error));
155
+ }
156
+ }
157
+ };
158
+ }
159
+
160
+ function describeRuntimeFailure(result, timeoutMs) {
161
+ if (result.terminationFailed) {
162
+ return `OpenCode CLI process did not terminate after timeout (${timeoutMs}ms SIGTERM/SIGKILL).`;
163
+ }
164
+ if (result.timedOut) {
165
+ return `OpenCode CLI timed out after ${timeoutMs}ms.`;
166
+ }
167
+ if (result.status !== 0) {
168
+ const exitPart = result.status == null
169
+ ? (result.signal ? `signal ${result.signal}` : "exit null")
170
+ : `exit ${result.status}`;
171
+ const detail = sanitizeCliStderr(result.stderr);
172
+ return detail
173
+ ? `OpenCode CLI failed (${exitPart}): ${detail}`
174
+ : `OpenCode CLI failed (${exitPart}).`;
175
+ }
176
+ return null;
177
+ }
178
+ function evidence(cliInstalled, cli = null) {
179
+ return {
180
+ hasApiKey: false,
181
+ cliInstalled,
182
+ authListOk: cli?.authListOk ?? false,
183
+ authProviders: cli?.authProviders ?? [],
184
+ modelsHttpStatus: null
185
+ };
186
+ }
187
+
188
+ function baseDetect(fields) {
189
+ return {
190
+ id: BACKEND_IDS.OPENCODE,
191
+ label: "OpenCode CLI",
192
+ hasApiKey: false,
193
+ cloud: true,
194
+ runtime: true,
195
+ authenticated: false,
196
+ billingModel: BILLING_MODELS.CLI_RUNTIME,
197
+ error: null,
198
+ ...fields
199
+ };
200
+ }
201
+
202
+ function runtimeModel(product, modelId) {
203
+ return createModelDescriptor({
204
+ provider: BACKEND_IDS.OPENCODE,
205
+ modelId: toRuntimeModelRef(product, modelId),
206
+ local: false,
207
+ costClass: COST_CLASSES.PAID,
208
+ privacyClass: PRIVACY_CLASSES.CLOUD,
209
+ transport: TRANSPORT_KINDS.RUNTIME
210
+ });
211
+ }
212
+
213
+ function resolveModelRef(modelId) {
214
+ if (!modelId) return null;
215
+ const raw = String(modelId).trim();
216
+ if (raw.includes("/")) return raw;
217
+ const product = resolveRuntimeProduct(raw);
218
+ return product ? toRuntimeModelRef(product, raw) : null;
219
+ }
220
+
221
+ function buildPrompt(contextPack, request) {
222
+ const chunks = [PREAMBLE];
223
+ if (contextPack?.systemPrompt) chunks.push(contextPack.systemPrompt);
224
+ if (request.prompt) chunks.push(request.prompt);
225
+ else if (Array.isArray(request.messages)) {
226
+ for (const message of request.messages) {
227
+ if (message?.content) chunks.push(String(message.content));
228
+ }
229
+ }
230
+ return chunks.join("\n\n");
231
+ }
232
+
233
+ function fail(message) {
234
+ return {
235
+ ok: false,
236
+ backendId: BACKEND_IDS.OPENCODE,
237
+ model: null,
238
+ content: null,
239
+ error: message,
240
+ usage: null
241
+ };
242
+ }
@@ -30,6 +30,7 @@ const STABLE_DOC_CANDIDATES = [
30
30
 
31
31
  const DEFAULT_STABLE_BUDGET = 6000;
32
32
  const DEFAULT_REQUEST_BUDGET = 4000;
33
+ const TRUNCATION_MARKER = "\n…[truncated]";
33
34
 
34
35
  export async function compileContextPack({
35
36
  workspaceRoot,
@@ -44,29 +45,54 @@ export async function compileContextPack({
44
45
  const project = await detectProject(root);
45
46
  const evidence = [];
46
47
 
47
- const agentsMd = await readEvidenceFile(
48
- root,
49
- workspaceRealRoot,
50
- "AGENTS.md",
51
- evidence,
52
- 2000,
53
- { includePrivate }
54
- );
55
- const stableDocs = await collectStableDocs(root, workspaceRealRoot, evidence, stableBudgetTokens, includePrivate);
48
+ const stableBudget = createBudget(stableBudgetTokens);
49
+ let agentsMd = null;
50
+ const stableDocs = [];
51
+
52
+ for (const candidate of STABLE_DOC_CANDIDATES) {
53
+ const content = await readEvidenceFile(
54
+ root,
55
+ workspaceRealRoot,
56
+ candidate,
57
+ evidence,
58
+ stableBudget,
59
+ { includePrivate, category: "stable" }
60
+ );
61
+ if (!content) continue;
62
+ if (candidate === "AGENTS.md") {
63
+ agentsMd = content;
64
+ } else {
65
+ stableDocs.push({ path: candidate, content: content.text, truncated: content.truncated });
66
+ }
67
+ }
68
+
56
69
  const skills = await listSkillIds(root, evidence);
57
70
  const graphify = await detectGraphify(root, evidence);
58
71
  const engram = await detectEngramHints(root, evidence);
59
72
 
73
+ const requestBudget = createBudget(requestBudgetTokens);
60
74
  const requestFiles = [];
75
+ const seenPaths = new Set();
76
+
61
77
  for (const filePath of relevantPaths) {
62
- const rel = relative(root, resolve(root, filePath));
78
+ const rel = relative(root, resolve(root, filePath)).replace(/\\/g, "/") || ".";
79
+ if (seenPaths.has(rel)) {
80
+ evidence.push({
81
+ kind: "deduped_path",
82
+ path: rel,
83
+ reason: "Duplicate path skipped"
84
+ });
85
+ continue;
86
+ }
87
+ seenPaths.add(rel);
88
+
63
89
  const content = await readEvidenceFile(
64
90
  root,
65
91
  workspaceRealRoot,
66
92
  rel,
67
93
  evidence,
68
- requestBudgetTokens,
69
- { includePrivate }
94
+ requestBudget,
95
+ { includePrivate, category: "request" }
70
96
  );
71
97
  if (content) {
72
98
  requestFiles.push({ path: rel, content: content.text, truncated: content.truncated });
@@ -92,11 +118,7 @@ export async function compileContextPack({
92
118
  engram
93
119
  };
94
120
 
95
- const perRequest = {
96
- task,
97
- files: requestFiles
98
- };
99
-
121
+ const perRequest = { task, files: requestFiles };
100
122
  const systemPrompt = buildSystemPrompt(stable, perRequest);
101
123
  const estimatedTokens = estimateTokens(systemPrompt);
102
124
 
@@ -110,7 +132,9 @@ export async function compileContextPack({
110
132
  estimatedTokens,
111
133
  budgets: {
112
134
  stableBudgetTokens,
113
- requestBudgetTokens
135
+ requestBudgetTokens,
136
+ stableUsedTokens: stableBudget.used,
137
+ requestUsedTokens: requestBudget.used
114
138
  },
115
139
  privacy: {
116
140
  includePrivate,
@@ -131,28 +155,17 @@ export function estimateTokens(text) {
131
155
  return Math.ceil(text.length / 4);
132
156
  }
133
157
 
134
- async function collectStableDocs(root, workspaceRealRoot, evidence, budgetTokens, includePrivate) {
135
- const docs = [];
136
- let used = 0;
137
-
138
- for (const candidate of STABLE_DOC_CANDIDATES) {
139
- if (candidate === "AGENTS.md") continue;
140
- const remaining = Math.max(500, budgetTokens - used);
141
- const content = await readEvidenceFile(
142
- root,
143
- workspaceRealRoot,
144
- candidate,
145
- evidence,
146
- remaining,
147
- { includePrivate }
148
- );
149
- if (!content) continue;
150
- docs.push({ path: candidate, content: content.text, truncated: content.truncated });
151
- used += estimateTokens(content.text);
152
- if (used >= budgetTokens) break;
153
- }
154
-
155
- return docs;
158
+ function createBudget(limitTokens) {
159
+ return {
160
+ limit: Math.max(0, Number(limitTokens) || 0),
161
+ used: 0,
162
+ remaining() {
163
+ return Math.max(0, this.limit - this.used);
164
+ },
165
+ consume(tokens) {
166
+ this.used += tokens;
167
+ }
168
+ };
156
169
  }
157
170
 
158
171
  async function listSkillIds(root, evidence) {
@@ -204,11 +217,11 @@ async function readEvidenceFile(
204
217
  workspaceRealRoot,
205
218
  relativePath,
206
219
  evidence,
207
- maxTokens = 2000,
208
- { includePrivate = false } = {}
220
+ budget,
221
+ { includePrivate = false, category = "request" } = {}
209
222
  ) {
210
223
  const requested = resolve(root, relativePath);
211
- const requestedRelative = relative(root, requested) || ".";
224
+ const requestedRelative = relative(root, requested).replace(/\\/g, "/") || ".";
212
225
  if (!isPathInside(root, requested)) {
213
226
  evidence.push({
214
227
  kind: "rejected_outside_workspace",
@@ -220,6 +233,17 @@ async function readEvidenceFile(
220
233
 
221
234
  if (!existsSync(requested)) return null;
222
235
 
236
+ const remaining = budget.remaining();
237
+ if (remaining <= 0) {
238
+ evidence.push({
239
+ kind: "excluded_budget",
240
+ path: requestedRelative,
241
+ reason: `${category} budget exhausted`,
242
+ tokens: 0
243
+ });
244
+ return null;
245
+ }
246
+
223
247
  let target;
224
248
  try {
225
249
  target = await realpath(requested);
@@ -237,7 +261,7 @@ async function readEvidenceFile(
237
261
  return null;
238
262
  }
239
263
 
240
- const targetRelative = relative(workspaceRealRoot, target) || ".";
264
+ const targetRelative = relative(workspaceRealRoot, target).replace(/\\/g, "/") || ".";
241
265
  if ((isPrivatePath(requestedRelative) || isPrivatePath(targetRelative)) && !includePrivate) {
242
266
  evidence.push({
243
267
  kind: "excluded_private",
@@ -249,22 +273,43 @@ async function readEvidenceFile(
249
273
 
250
274
  try {
251
275
  const raw = await readFile(target, "utf8");
252
- const maxChars = maxTokens * 4;
253
- const truncated = raw.length > maxChars;
254
- const text = truncated ? `${raw.slice(0, maxChars)}\n…[truncated]` : raw;
276
+ const clipped = clipToTokenBudget(raw, remaining);
277
+ budget.consume(clipped.tokens);
255
278
  evidence.push({
256
279
  kind: "file",
257
280
  path: requestedRelative,
258
- truncated,
259
- chars: text.length
281
+ truncated: clipped.truncated,
282
+ chars: clipped.text.length,
283
+ tokens: clipped.tokens
260
284
  });
261
- return { text, truncated };
285
+ return { text: clipped.text, truncated: clipped.truncated, tokens: clipped.tokens };
262
286
  } catch {
263
287
  evidence.push({ kind: "unreadable", path: requestedRelative });
264
288
  return null;
265
289
  }
266
290
  }
267
291
 
292
+ function clipToTokenBudget(raw, maxTokens) {
293
+ const maxChars = Math.max(0, maxTokens) * 4;
294
+ if (raw.length <= maxChars) {
295
+ return { text: raw, truncated: false, tokens: estimateTokens(raw) };
296
+ }
297
+
298
+ const marker = TRUNCATION_MARKER;
299
+ if (marker.length >= maxChars) {
300
+ const text = marker.slice(0, maxChars);
301
+ return { text, truncated: true, tokens: estimateTokens(text) };
302
+ }
303
+
304
+ const bodyChars = maxChars - marker.length;
305
+ const text = `${raw.slice(0, bodyChars)}${marker}`;
306
+ return {
307
+ text,
308
+ truncated: true,
309
+ tokens: estimateTokens(text)
310
+ };
311
+ }
312
+
268
313
  async function resolveWorkspaceRoot(root) {
269
314
  try {
270
315
  return await realpath(root);
@@ -3,16 +3,52 @@ export {
3
3
  COST_CLASSES,
4
4
  PRIVACY_CLASSES,
5
5
  ROUTING_MODES,
6
+ TRANSPORT_KINDS,
6
7
  OPENROUTER_FREE_MODEL,
8
+ OPENCODE_API_KEY_ENV,
9
+ OPENCODE_GO_BASE_URL,
10
+ OPENCODE_ZEN_BASE_URL,
11
+ OPENCODE_GO_DEFAULT_MODEL,
12
+ OPENCODE_ZEN_DEFAULT_FREE_MODEL,
7
13
  DEFAULT_OLLAMA_HOST,
8
14
  createModelDescriptor,
9
15
  createRoutingDecision,
10
16
  createUsageTelemetry
11
17
  } from "./types.js";
12
18
 
19
+ export {
20
+ resolveOpencodeTransport,
21
+ isDirectTransport,
22
+ listRegisteredModelIds,
23
+ normalizeModelId,
24
+ toRuntimeModelRef,
25
+ resolveRuntimeProduct,
26
+ OPENCODE_GO_TRANSPORTS,
27
+ OPENCODE_ZEN_TRANSPORTS
28
+ } from "./transport-registry.js";
29
+
30
+ export {
31
+ ENTITLEMENT_STATES,
32
+ BILLING_MODELS,
33
+ parseAuthListProviders,
34
+ collectOpencodeCliEvidence
35
+ } from "./backends/opencode-evidence.js";
36
+
13
37
  export { createOllamaBackend } from "./backends/ollama.js";
14
38
  export { createOpenRouterBackend } from "./backends/openrouter.js";
15
39
  export { createCustomHttpBackend } from "./backends/custom-http.js";
40
+ export {
41
+ createOpencodeGoBackend,
42
+ createOpencodeZenBackend
43
+ } from "./backends/opencode-providers.js";
44
+
45
+ export { createOpencodeRuntimeBackend } from "./backends/opencode-runtime.js";
46
+
47
+ export {
48
+ runOpencodeJson,
49
+ parseOpencodeJsonEvents,
50
+ sanitizeCliStderr
51
+ } from "./backends/opencode-cli.js";
16
52
 
17
53
  export {
18
54
  createDefaultBackends,
@@ -29,7 +65,8 @@ export {
29
65
 
30
66
  export {
31
67
  resolveRoutingDecision,
32
- classifyTaskWeight
68
+ classifyTaskWeight,
69
+ isDirectProviderEligible
33
70
  } from "./router.js";
34
71
 
35
72
  export {
@@ -19,7 +19,8 @@ export async function runIntelligenceRequest({
19
19
  env = process.env,
20
20
  fetchImpl = globalThis.fetch,
21
21
  backends = null,
22
- tokenBudget = null
22
+ tokenBudget = null,
23
+ sessionOverride = null
23
24
  } = {}) {
24
25
  const customProviders = Array.isArray(profile.customProviders) ? profile.customProviders : [];
25
26
  const backendInstances = backends ?? createDefaultBackends({ env, fetchImpl, customProviders });
@@ -46,7 +47,8 @@ export async function runIntelligenceRequest({
46
47
  contextPack,
47
48
  task: task ?? prompt,
48
49
  cloudConsent: Boolean(cloudConsent),
49
- tokenBudget: tokenBudget ?? profile.tokenBudget ?? null
50
+ tokenBudget: tokenBudget ?? profile.tokenBudget ?? null,
51
+ sessionOverride
50
52
  });
51
53
 
52
54
  const explanation = explainRouting(routing, contextPack, inspections);
@@ -2,16 +2,39 @@ import { BACKEND_IDS } from "./types.js";
2
2
  import { createOllamaBackend } from "./backends/ollama.js";
3
3
  import { createOpenRouterBackend } from "./backends/openrouter.js";
4
4
  import { createCustomHttpBackend } from "./backends/custom-http.js";
5
+ import {
6
+ createOpencodeGoBackend,
7
+ createOpencodeZenBackend
8
+ } from "./backends/opencode-providers.js";
9
+ import { createOpencodeRuntimeBackend } from "./backends/opencode-runtime.js";
10
+ import { collectOpencodeCliEvidence } from "./backends/opencode-evidence.js";
5
11
  import { CAPABILITY_STATES } from "../capability-states.js";
6
12
 
7
13
  export function createDefaultBackends({
8
14
  env = process.env,
9
15
  fetchImpl = globalThis.fetch,
10
- customProviders = []
16
+ customProviders = [],
17
+ whichImpl,
18
+ collectCliEvidence,
19
+ spawnImpl
11
20
  } = {}) {
21
+ const sharedCliEvidence = createSharedCliEvidenceCollector({
22
+ env,
23
+ whichImpl,
24
+ collectCliEvidence
25
+ });
26
+
12
27
  const backends = [
13
28
  createOllamaBackend({ env, fetchImpl }),
14
- createOpenRouterBackend({ env, fetchImpl })
29
+ createOpencodeGoBackend({ env, fetchImpl, collectCliEvidence: sharedCliEvidence }),
30
+ createOpencodeZenBackend({ env, fetchImpl, collectCliEvidence: sharedCliEvidence }),
31
+ createOpenRouterBackend({ env, fetchImpl }),
32
+ createOpencodeRuntimeBackend({
33
+ env,
34
+ whichImpl,
35
+ spawnImpl,
36
+ collectCliEvidence: sharedCliEvidence
37
+ })
15
38
  ];
16
39
 
17
40
  for (const provider of customProviders) {
@@ -29,9 +52,19 @@ export async function inspectIntelligenceBackends({
29
52
  env = process.env,
30
53
  fetchImpl = globalThis.fetch,
31
54
  customProviders = [],
32
- backends = null
55
+ backends = null,
56
+ whichImpl,
57
+ collectCliEvidence,
58
+ spawnImpl
33
59
  } = {}) {
34
- const resolved = backends ?? createDefaultBackends({ env, fetchImpl, customProviders });
60
+ const resolved = backends ?? createDefaultBackends({
61
+ env,
62
+ fetchImpl,
63
+ customProviders,
64
+ whichImpl,
65
+ collectCliEvidence,
66
+ spawnImpl
67
+ });
35
68
  return Promise.all(resolved.map(async (backend) => {
36
69
  const detection = await backend.detect();
37
70
  const models = detection.detected || detection.available
@@ -59,15 +92,25 @@ export function summarizeIntelligenceBackends(inspections) {
59
92
  }
60
93
  }
61
94
 
95
+ const go = inspections.find((entry) => entry.id === BACKEND_IDS.OPENCODE_GO);
96
+ const zen = inspections.find((entry) => entry.id === BACKEND_IDS.OPENCODE_ZEN);
97
+ const openrouter = inspections.find((entry) => entry.id === BACKEND_IDS.OPENROUTER);
98
+
62
99
  return {
63
100
  total: inspections.length,
64
101
  available: inspections.filter((entry) => entry.available).length,
65
102
  localAvailable: inspections.some(
66
103
  (entry) => entry.id === BACKEND_IDS.OLLAMA && entry.available
67
104
  ),
68
- cloudAuthenticated: inspections.some(
69
- (entry) => entry.id === BACKEND_IDS.OPENROUTER && entry.hasApiKey
105
+ cloudConfigured: Boolean(go?.configured || zen?.configured || openrouter?.hasApiKey),
106
+ /** OpenRouter stays key-based; Go/Zen use proven authenticated evidence. */
107
+ cloudAuthenticated: Boolean(
108
+ go?.authenticated || zen?.authenticated || openrouter?.hasApiKey
70
109
  ),
110
+ opencodeGoConfigured: Boolean(go?.configured),
111
+ opencodeZenConfigured: Boolean(zen?.configured),
112
+ opencodeGoAuthenticated: Boolean(go?.authenticated),
113
+ opencodeZenAuthenticated: Boolean(zen?.authenticated),
71
114
  byState
72
115
  };
73
116
  }
@@ -75,3 +118,16 @@ export function summarizeIntelligenceBackends(inspections) {
75
118
  export function resolveBackendById(backends, backendId) {
76
119
  return backends.find((backend) => backend.id === backendId) ?? null;
77
120
  }
121
+
122
+ function createSharedCliEvidenceCollector({
123
+ env,
124
+ whichImpl,
125
+ collectCliEvidence
126
+ }) {
127
+ let cached = null;
128
+ const collector = collectCliEvidence ?? collectOpencodeCliEvidence;
129
+ return () => {
130
+ if (!cached) cached = collector({ env, whichImpl });
131
+ return cached;
132
+ };
133
+ }