insika 0.0.1 → 0.2.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 (277) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +361 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +366 -0
  6. data/docs/AGENTS.md +618 -0
  7. data/docs/ARCHITECTURE.md +333 -0
  8. data/docs/BENCHMARK.md +114 -0
  9. data/docs/CHANNELS.md +453 -0
  10. data/docs/CONTEXT.md +117 -0
  11. data/docs/DEPLOY.md +354 -0
  12. data/docs/EMBEDDING.md +198 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +232 -0
  15. data/docs/OBSERVABILITY.md +374 -0
  16. data/docs/PLUGINS.md +211 -0
  17. data/docs/REFINEMENT.md +477 -0
  18. data/docs/RELEASING.md +70 -0
  19. data/docs/RUNNING-LOCAL.md +153 -0
  20. data/docs/SANDBOX.md +114 -0
  21. data/docs/SECURITY.md +375 -0
  22. data/docs/SKILLS.md +284 -0
  23. data/docs/TOOLS.md +302 -0
  24. data/docs/WHY.md +137 -0
  25. data/docs/WORKFLOWS.md +225 -0
  26. data/docs/build.md +14 -0
  27. data/docs/index.md +68 -0
  28. data/docs/onboarding/start.md +126 -0
  29. data/docs/operate.md +12 -0
  30. data/docs/ship.md +10 -0
  31. data/docs/understand.md +10 -0
  32. data/lib/insika/agent_file_store.rb +125 -0
  33. data/lib/insika/agent_profile.rb +255 -0
  34. data/lib/insika/alert_dispatcher.rb +139 -0
  35. data/lib/insika/allowlist.rb +28 -0
  36. data/lib/insika/baseline_store.rb +74 -0
  37. data/lib/insika/budget_ledger.rb +135 -0
  38. data/lib/insika/capability/resolved_tool.rb +34 -0
  39. data/lib/insika/capability_registry.rb +112 -0
  40. data/lib/insika/channel_delivery.rb +153 -0
  41. data/lib/insika/channel_registry.rb +30 -0
  42. data/lib/insika/channels/relay.rb +178 -0
  43. data/lib/insika/channels/web/widget.js +283 -0
  44. data/lib/insika/channels/web.rb +211 -0
  45. data/lib/insika/channels/webhook.rb +58 -0
  46. data/lib/insika/chat_builder.rb +303 -0
  47. data/lib/insika/checkpoint.rb +13 -0
  48. data/lib/insika/checkpoint_store.rb +153 -0
  49. data/lib/insika/circuit_state.rb +114 -0
  50. data/lib/insika/coercion.rb +58 -0
  51. data/lib/insika/command.rb +32 -0
  52. data/lib/insika/command_bus.rb +39 -0
  53. data/lib/insika/commands/agent_payload.rb +43 -0
  54. data/lib/insika/commands/approve_action.rb +46 -0
  55. data/lib/insika/commands/cancel_task.rb +33 -0
  56. data/lib/insika/commands/create_agent.rb +54 -0
  57. data/lib/insika/commands/create_session.rb +67 -0
  58. data/lib/insika/commands/delete_agent.rb +33 -0
  59. data/lib/insika/commands/delete_agent_file.rb +50 -0
  60. data/lib/insika/commands/delete_data_tool.rb +33 -0
  61. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  62. data/lib/insika/commands/delete_mcp.rb +30 -0
  63. data/lib/insika/commands/delete_skill.rb +43 -0
  64. data/lib/insika/commands/delete_system_file.rb +29 -0
  65. data/lib/insika/commands/gate_refinement.rb +245 -0
  66. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  67. data/lib/insika/commands/import_tools.rb +81 -0
  68. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  69. data/lib/insika/commands/memory_add_note.rb +32 -0
  70. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  71. data/lib/insika/commands/memory_put_fact.rb +35 -0
  72. data/lib/insika/commands/pause_task.rb +29 -0
  73. data/lib/insika/commands/resolve_refinement.rb +126 -0
  74. data/lib/insika/commands/restore_agent_file.rb +36 -0
  75. data/lib/insika/commands/restore_data_tool.rb +34 -0
  76. data/lib/insika/commands/restore_system_file.rb +31 -0
  77. data/lib/insika/commands/resume_task.rb +85 -0
  78. data/lib/insika/commands/revoke_token.rb +39 -0
  79. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  80. data/lib/insika/commands/run_refinement.rb +133 -0
  81. data/lib/insika/commands/send_message.rb +150 -0
  82. data/lib/insika/commands/set_agent_tools.rb +39 -0
  83. data/lib/insika/commands/set_skill_agents.rb +112 -0
  84. data/lib/insika/commands/trigger_workflow.rb +80 -0
  85. data/lib/insika/commands/update_agent.rb +49 -0
  86. data/lib/insika/commands/update_settings.rb +33 -0
  87. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  88. data/lib/insika/commands/upsert_mcp.rb +32 -0
  89. data/lib/insika/commands/write_agent_file.rb +57 -0
  90. data/lib/insika/commands/write_data_tool.rb +43 -0
  91. data/lib/insika/commands/write_golden.rb +58 -0
  92. data/lib/insika/commands/write_skill.rb +60 -0
  93. data/lib/insika/commands/write_system_file.rb +31 -0
  94. data/lib/insika/config_store.rb +89 -0
  95. data/lib/insika/context/builder.rb +166 -0
  96. data/lib/insika/context/catalog_provider.rb +23 -0
  97. data/lib/insika/context/fragment.rb +43 -0
  98. data/lib/insika/context/priority.rb +30 -0
  99. data/lib/insika/context/provider.rb +19 -0
  100. data/lib/insika/context/providers/memory.rb +60 -0
  101. data/lib/insika/context/providers/prompt.rb +105 -0
  102. data/lib/insika/context/providers/request.rb +32 -0
  103. data/lib/insika/context/providers/session.rb +123 -0
  104. data/lib/insika/context/providers/skill.rb +24 -0
  105. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  106. data/lib/insika/context/providers/tool_search.rb +20 -0
  107. data/lib/insika/context_trace_store.rb +92 -0
  108. data/lib/insika/delegation_store.rb +153 -0
  109. data/lib/insika/doctor.rb +539 -0
  110. data/lib/insika/dsl/definition.rb +55 -0
  111. data/lib/insika/dsl/runtime.rb +382 -0
  112. data/lib/insika/dsl/server_boot.rb +98 -0
  113. data/lib/insika/dsl/system.rb +93 -0
  114. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  115. data/lib/insika/dsl.rb +364 -0
  116. data/lib/insika/edge_limiter.rb +268 -0
  117. data/lib/insika/egress_guard.rb +75 -0
  118. data/lib/insika/env_schema.rb +249 -0
  119. data/lib/insika/errors.rb +201 -0
  120. data/lib/insika/evals/assertions.rb +247 -0
  121. data/lib/insika/evals/baseline.rb +69 -0
  122. data/lib/insika/evals/golden.rb +172 -0
  123. data/lib/insika/evals/judge.rb +225 -0
  124. data/lib/insika/evals/pairwise.rb +178 -0
  125. data/lib/insika/evals/report.rb +115 -0
  126. data/lib/insika/evals/runner.rb +141 -0
  127. data/lib/insika/evals/transport.rb +178 -0
  128. data/lib/insika/event.rb +18 -0
  129. data/lib/insika/event_stream.rb +132 -0
  130. data/lib/insika/executor.rb +1995 -0
  131. data/lib/insika/frontmatter.rb +42 -0
  132. data/lib/insika/golden_store.rb +145 -0
  133. data/lib/insika/hooks.rb +48 -0
  134. data/lib/insika/http_client.rb +63 -0
  135. data/lib/insika/inbound_log.rb +84 -0
  136. data/lib/insika/llm_configurator.rb +99 -0
  137. data/lib/insika/llm_provider_store.rb +83 -0
  138. data/lib/insika/loop_detector.rb +143 -0
  139. data/lib/insika/mcp_http_client.rb +67 -0
  140. data/lib/insika/mcp_store.rb +115 -0
  141. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  142. data/lib/insika/memory_store.rb +93 -0
  143. data/lib/insika/message_origin.rb +76 -0
  144. data/lib/insika/middleware.rb +36 -0
  145. data/lib/insika/model_policy.rb +52 -0
  146. data/lib/insika/model_resolver.rb +176 -0
  147. data/lib/insika/model_selection.rb +115 -0
  148. data/lib/insika/onboarding.rb +208 -0
  149. data/lib/insika/outbox_store.rb +166 -0
  150. data/lib/insika/overlay_tool_registry.rb +102 -0
  151. data/lib/insika/pack.rb +102 -0
  152. data/lib/insika/pack_importer.rb +123 -0
  153. data/lib/insika/pending_action_store.rb +120 -0
  154. data/lib/insika/plugin/loader.rb +356 -0
  155. data/lib/insika/plugin.rb +35 -0
  156. data/lib/insika/policy/engine.rb +83 -0
  157. data/lib/insika/policy/policy.rb +120 -0
  158. data/lib/insika/policy_registry.rb +23 -0
  159. data/lib/insika/profile_source.rb +143 -0
  160. data/lib/insika/prompt_catalog.rb +61 -0
  161. data/lib/insika/provider_error_classifier.rb +160 -0
  162. data/lib/insika/queue_policy.rb +167 -0
  163. data/lib/insika/recovery.rb +168 -0
  164. data/lib/insika/refinement/candidate.rb +159 -0
  165. data/lib/insika/refinement/evidence_collector.rb +371 -0
  166. data/lib/insika/refinement/gate.rb +234 -0
  167. data/lib/insika/refinement/panel.rb +222 -0
  168. data/lib/insika/refinement/proposer.rb +262 -0
  169. data/lib/insika/refinement_store.rb +295 -0
  170. data/lib/insika/registry.rb +59 -0
  171. data/lib/insika/reliability.rb +185 -0
  172. data/lib/insika/safety/config.rb +109 -0
  173. data/lib/insika/safety/detectors.rb +176 -0
  174. data/lib/insika/safety/factory.rb +102 -0
  175. data/lib/insika/safety/input_guardrail.rb +102 -0
  176. data/lib/insika/safety/moderator.rb +94 -0
  177. data/lib/insika/safety/output_filter.rb +79 -0
  178. data/lib/insika/safety/output_validator.rb +101 -0
  179. data/lib/insika/safety/safe_responses.rb +47 -0
  180. data/lib/insika/sandbox/boundary.rb +93 -0
  181. data/lib/insika/sandbox/docker.rb +74 -0
  182. data/lib/insika/sandbox/local.rb +33 -0
  183. data/lib/insika/sandbox/runner.rb +80 -0
  184. data/lib/insika/sandbox.rb +85 -0
  185. data/lib/insika/schema_guard.rb +147 -0
  186. data/lib/insika/secret_masking.rb +34 -0
  187. data/lib/insika/server/a2a/agent_card.rb +27 -0
  188. data/lib/insika/server/a2a/app.rb +112 -0
  189. data/lib/insika/server/a2a/client.rb +101 -0
  190. data/lib/insika/server/a2a/errors.rb +32 -0
  191. data/lib/insika/server/a2a/http.rb +42 -0
  192. data/lib/insika/server/a2a/message.rb +27 -0
  193. data/lib/insika/server/a2a/protocol.rb +45 -0
  194. data/lib/insika/server/a2a/remotes.rb +25 -0
  195. data/lib/insika/server/a2a/task_projection.rb +40 -0
  196. data/lib/insika/server/app.rb +1022 -0
  197. data/lib/insika/server/boot.rb +119 -0
  198. data/lib/insika/server/rack_app.rb +118 -0
  199. data/lib/insika/server/responses.rb +165 -0
  200. data/lib/insika/server/sse_body.rb +96 -0
  201. data/lib/insika/server/tenant_auth.rb +61 -0
  202. data/lib/insika/session_actor.rb +162 -0
  203. data/lib/insika/session_store.rb +143 -0
  204. data/lib/insika/settings_store.rb +154 -0
  205. data/lib/insika/shutdown.rb +125 -0
  206. data/lib/insika/skill_catalog.rb +220 -0
  207. data/lib/insika/skill_store.rb +127 -0
  208. data/lib/insika/steer_injector.rb +110 -0
  209. data/lib/insika/store.rb +52 -0
  210. data/lib/insika/stores/memory.rb +123 -0
  211. data/lib/insika/stores/sqlite.rb +183 -0
  212. data/lib/insika/studio/app.rb +1693 -0
  213. data/lib/insika/studio/assets/dist/application.css +1 -0
  214. data/lib/insika/studio/assets/dist/application.js +70 -0
  215. data/lib/insika/studio/forms.rb +335 -0
  216. data/lib/insika/studio/nav_icons.rb +31 -0
  217. data/lib/insika/studio/views/_message.erb +44 -0
  218. data/lib/insika/studio/views/agent_detail.erb +285 -0
  219. data/lib/insika/studio/views/agents.erb +63 -0
  220. data/lib/insika/studio/views/approvals.erb +41 -0
  221. data/lib/insika/studio/views/chats.erb +34 -0
  222. data/lib/insika/studio/views/evals.erb +83 -0
  223. data/lib/insika/studio/views/home.erb +72 -0
  224. data/lib/insika/studio/views/layout.erb +94 -0
  225. data/lib/insika/studio/views/login.erb +17 -0
  226. data/lib/insika/studio/views/mcp.erb +91 -0
  227. data/lib/insika/studio/views/not_found.erb +5 -0
  228. data/lib/insika/studio/views/playground.erb +47 -0
  229. data/lib/insika/studio/views/refinement.erb +234 -0
  230. data/lib/insika/studio/views/session.erb +137 -0
  231. data/lib/insika/studio/views/settings.erb +168 -0
  232. data/lib/insika/studio/views/skills.erb +141 -0
  233. data/lib/insika/studio/views/system_files.erb +65 -0
  234. data/lib/insika/studio/views/task.erb +105 -0
  235. data/lib/insika/studio/views/tasks.erb +33 -0
  236. data/lib/insika/studio/views/tool_edit.erb +107 -0
  237. data/lib/insika/studio/views/tools.erb +89 -0
  238. data/lib/insika/subagent_graph.rb +96 -0
  239. data/lib/insika/system_file_store.rb +96 -0
  240. data/lib/insika/task_actor.rb +128 -0
  241. data/lib/insika/task_store.rb +250 -0
  242. data/lib/insika/telemetry/pricing.rb +104 -0
  243. data/lib/insika/telemetry/recorder.rb +228 -0
  244. data/lib/insika/telemetry.rb +127 -0
  245. data/lib/insika/testing/store_contract.rb +270 -0
  246. data/lib/insika/tick.rb +122 -0
  247. data/lib/insika/token_estimator.rb +16 -0
  248. data/lib/insika/token_store.rb +168 -0
  249. data/lib/insika/tool_assembly.rb +140 -0
  250. data/lib/insika/tool_catalog.rb +89 -0
  251. data/lib/insika/tool_definition.rb +518 -0
  252. data/lib/insika/tool_envelope.rb +140 -0
  253. data/lib/insika/tool_manifest.rb +218 -0
  254. data/lib/insika/tool_output_compressor.rb +100 -0
  255. data/lib/insika/tool_registry.rb +21 -0
  256. data/lib/insika/tool_store.rb +135 -0
  257. data/lib/insika/tool_trace_store.rb +92 -0
  258. data/lib/insika/tools/a2a_remote.rb +48 -0
  259. data/lib/insika/tools/agent_enum.rb +68 -0
  260. data/lib/insika/tools/concurrency.rb +54 -0
  261. data/lib/insika/tools/data_defined_tool.rb +219 -0
  262. data/lib/insika/tools/load_skill.rb +99 -0
  263. data/lib/insika/tools/remember.rb +53 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +75 -0
  266. data/lib/insika/tools/subagents.rb +77 -0
  267. data/lib/insika/tools/tool_search.rb +94 -0
  268. data/lib/insika/turn_output.rb +139 -0
  269. data/lib/insika/turn_state.rb +162 -0
  270. data/lib/insika/turn_timing.rb +56 -0
  271. data/lib/insika/usage_ledger.rb +47 -0
  272. data/lib/insika/version.rb +3 -1
  273. data/lib/insika/wiring/graph.rb +249 -0
  274. data/lib/insika/workflow.rb +185 -0
  275. data/lib/insika/workflow_registry.rb +33 -0
  276. data/lib/insika.rb +220 -4
  277. metadata +412 -8
data/bin/insika ADDED
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # `insika` CLI — strict config + doctor --fix. The operator's
5
+ # DX front door to the strict-config discipline:
6
+ #
7
+ # insika doctor [--fix] [--json] diagnose the deployment config; --fix applies
8
+ # the safe autofixes; exits non-zero on any error
9
+ # insika env [--json] list the known config keys + current (masked) values
10
+ # insika version print the engine version
11
+ # insika help this text
12
+ #
13
+ # It reads the SAME durable backend the server does (INSIKA_DB -> SQLite, else
14
+ # ephemeral memory), building only the config stores it needs — no full app boot, no
15
+ # DEEPSEEK, no seeding. So it is safe to run against a live deployment's volume.
16
+
17
+ require "optparse"
18
+ require_relative "../lib/insika"
19
+
20
+ module Insika
21
+ module CLI
22
+ module_function
23
+
24
+ def main(argv)
25
+ command = argv.shift || "help"
26
+ case command
27
+ when "doctor" then doctor(argv)
28
+ when "env" then env(argv)
29
+ when "refine" then refine(argv)
30
+ when "evals:import" then evals_import(argv)
31
+ when "evals:export" then evals_export(argv)
32
+ when "evals:baseline" then evals_baseline(argv)
33
+ when "version", "--version", "-v" then puts "insika #{Insika::VERSION}"
34
+ when "help", "--help", "-h" then puts usage
35
+ else
36
+ warn "unknown command: #{command}\n\n#{usage}"
37
+ exit 2
38
+ end
39
+ end
40
+
41
+ def usage
42
+ <<~TXT
43
+ insika #{Insika::VERSION} — agent engine CLI
44
+
45
+ Usage: insika <command> [options]
46
+
47
+ Commands:
48
+ doctor [--fix] [--json] diagnose the deployment configuration
49
+ env [--json] list known config keys + current (masked) values
50
+ refine --agent ID [...] report what broke in an agent's real traffic
51
+ evals:import [--dir D] load the golden corpus into the store (editable in Studio)
52
+ evals:export [--dir D] write the stored cases back out as YAML
53
+ evals:baseline <sub> the accepted state per agent: show | import | export
54
+ version print the engine version
55
+ help show this help
56
+
57
+ Environment:
58
+ INSIKA_DB SQLite path (durable). Unset -> ephemeral memory.
59
+ INSIKA_CONFIG_STRICT when truthy, the server refuses boot on any finding.
60
+ TXT
61
+ end
62
+
63
+ # insika doctor [--fix] [--json]
64
+ def doctor(argv)
65
+ opts = { fix: false, json: false }
66
+ OptionParser.new do |o|
67
+ o.banner = "Usage: insika doctor [--fix] [--json]"
68
+ o.on("--fix", "apply the safe autofixes, then re-diagnose") { opts[:fix] = true }
69
+ o.on("--json", "emit the report as JSON") { opts[:json] = true }
70
+ end.parse!(argv)
71
+
72
+ doc = build_doctor
73
+ report = opts[:fix] ? doc.fix!.last : doc.run
74
+
75
+ if opts[:json]
76
+ require "json"
77
+ puts JSON.pretty_generate(report.to_h)
78
+ else
79
+ puts report.to_s(color: $stdout.tty?)
80
+ end
81
+ exit(report.ok? ? 0 : 1)
82
+ end
83
+
84
+ # insika env [--json]
85
+ def env(argv)
86
+ json = false
87
+ OptionParser.new do |o|
88
+ o.banner = "Usage: insika env [--json]"
89
+ o.on("--json", "emit as JSON") { json = true }
90
+ end.parse!(argv)
91
+
92
+ rows = Insika::EnvSchema.known_specs.map do |s|
93
+ raw = ENV[s.name]
94
+ { "key" => s.name, "type" => s.type.to_s, "secret" => s.secret?,
95
+ "set" => !raw.nil?, "value" => display_value(s, raw), "description" => s.description }
96
+ end
97
+
98
+ if json
99
+ require "json"
100
+ puts JSON.pretty_generate(rows)
101
+ else
102
+ rows.each do |r|
103
+ state = r["set"] ? r["value"] : "(unset)"
104
+ puts format("%-30s %-8s %s", r["key"], r["type"], state)
105
+ end
106
+ end
107
+ end
108
+
109
+ # insika refine --agent ID [--last-sessions N] [--since ISO8601] [--full] [--json]
110
+ #
111
+ # reads the agent's own traffic and records a ranked failure
112
+ # report. Read-only with respect to the agent — it writes only the run record.
113
+ # There is no scheduler in the engine: this command and the Studio button
114
+ # are how a run starts, and a cron that wants one calls this.
115
+ def refine(argv)
116
+ opts = { json: false, full: false }
117
+ OptionParser.new do |o|
118
+ o.banner = "Usage: insika refine --agent ID [options]"
119
+ o.on("--agent ID", "agent whose traffic to read (required)") { |v| opts[:agent] = v }
120
+ o.on("--last-sessions N", Integer, "read the N most recent conversations") { |v| opts[:last_sessions] = v }
121
+ o.on("--since ISO8601", "read only turns from this instant on") { |v| opts[:since] = v }
122
+ o.on("--full", "ignore the previous run and read the whole window") { opts[:full] = true }
123
+ o.on("--exclude PREFIXES", "comma-separated session-id prefixes to drop (e.g. loadtest-,debug-)") do |v|
124
+ opts[:exclude_sessions] = v.split(",").map(&:strip).reject(&:empty?)
125
+ end
126
+ o.on("--json", "emit the report as JSON") { opts[:json] = true }
127
+ end.parse!(argv)
128
+
129
+ abort "insika refine: --agent is required" if Insika::Coercion.blank?(opts[:agent])
130
+
131
+ # A misspelled agent or a bad window is operator error, not a bug: one line and
132
+ # exit 2 (like an unknown command), never a Ruby backtrace. Exit 1 stays what it
133
+ # means everywhere else in this CLI — "it ran and found trouble".
134
+ run = begin
135
+ run_refinement(opts)
136
+ rescue Insika::Error, ArgumentError => e
137
+ warn "insika refine: #{e.message}"
138
+ exit 2
139
+ end
140
+ if opts[:json]
141
+ require "json"
142
+ puts JSON.pretty_generate(run.to_h)
143
+ else
144
+ puts refine_report(run)
145
+ end
146
+ exit(run.status == :failed ? 1 : 0)
147
+ end
148
+
149
+ # insika evals:import [--dir evals/golden] [--keep-existing]
150
+ #
151
+ # The corpus on disk is the SEED and the export format; the store is what a
152
+ # deployment runs and what the Studio edits. Importing is how a
153
+ # fresh deploy gets the cases, and how a broken edit is undone.
154
+ def evals_import(argv)
155
+ opts = { dir: File.expand_path("../evals/golden", __dir__), overwrite: true }
156
+ OptionParser.new do |o|
157
+ o.banner = "Usage: insika evals:import [--dir DIR] [--keep-existing]"
158
+ o.on("--dir DIR", "corpus directory (default evals/golden)") { |v| opts[:dir] = v }
159
+ o.on("--keep-existing", "do not overwrite a case already in the store") { opts[:overwrite] = false }
160
+ end.parse!(argv)
161
+
162
+ ids = golden_store.import_dir(opts[:dir], overwrite: opts[:overwrite])
163
+ puts "imported #{ids.size} case(s) from #{opts[:dir]}"
164
+ puts ids.sort.map { |id| " #{id}" }.join("\n") unless ids.empty?
165
+ rescue Insika::Evals::GoldenLoader::InvalidGolden => e
166
+ warn "insika evals:import: #{e.message}"
167
+ exit 2
168
+ end
169
+
170
+ # insika evals:export [--dir DIR] — the store back to YAML, one file per case under
171
+ # <dir>/<agent>/<id>.yml. Same format `evals:import` reads: no converter to keep
172
+ # honest, so a case authored in the Studio can be reviewed in a pull request.
173
+ def evals_export(argv)
174
+ opts = { dir: File.expand_path("../evals/golden", __dir__), force: false }
175
+ OptionParser.new do |o|
176
+ o.banner = "Usage: insika evals:export [--dir DIR] [--force]"
177
+ o.on("--dir DIR", "destination (default evals/golden)") { |v| opts[:dir] = v }
178
+ o.on("--force", "rewrite an existing corpus (drops the files' comments)") { opts[:force] = true }
179
+ end.parse!(argv)
180
+
181
+ paths = golden_store.export_dir(opts[:dir], force: opts[:force])
182
+ puts "exported #{paths.size} case(s) to #{opts[:dir]}"
183
+ rescue Insika::ValidationError => e
184
+ warn "insika evals:export: #{e.message}"
185
+ exit 2
186
+ end
187
+
188
+ # insika evals:baseline show|import|export [--file evals/baseline.json]
189
+ #
190
+ # The accepted state of a golden set used to live ONLY in `evals/baseline.json`,
191
+ # which is fine for the CLI (it runs from a checkout) and useless for the refinement
192
+ # gate, which runs inside a deployment and has to know what "no
193
+ # regression" means without one. So it became a per-agent record.
194
+ #
195
+ # `import` splits the one file into those records, resolving each case's agent from
196
+ # the golden store — which is why the corpus has to be imported first. A case the
197
+ # store does not know is REPORTED, not guessed at: silently dropping it would shrink
198
+ # the accepted state, and a smaller baseline is a weaker gate.
199
+ def evals_baseline(argv)
200
+ sub = argv.shift || "show"
201
+ opts = { file: File.expand_path("../evals/baseline.json", __dir__) }
202
+ OptionParser.new do |o|
203
+ o.banner = "Usage: insika evals:baseline show|import|export [--file PATH]"
204
+ o.on("--file PATH", "the baseline JSON (default evals/baseline.json)") { |v| opts[:file] = v }
205
+ end.parse!(argv)
206
+
207
+ case sub
208
+ when "show" then baseline_show
209
+ when "import" then baseline_import(opts[:file])
210
+ when "export" then baseline_export(opts[:file])
211
+ else
212
+ warn "insika evals:baseline: unknown subcommand '#{sub}' (show|import|export)"
213
+ exit 2
214
+ end
215
+ rescue Insika::Error => e
216
+ warn "insika evals:baseline: #{e.message}"
217
+ exit 2
218
+ end
219
+
220
+ def baseline_show
221
+ store = baseline_store
222
+ agents = store.agents
223
+ return puts("no baseline recorded — the refinement gate cannot run (insika evals:baseline import)") if agents.empty?
224
+
225
+ agents.sort.each do |agent|
226
+ record = store.get(agent)
227
+ puts format("%-24s %3d case(s) recorded %s", agent, (record["cases"] || {}).size, record["at"])
228
+ end
229
+ end
230
+
231
+ def baseline_import(path)
232
+ raise Insika::ValidationError, "no such file: #{path}" unless File.exist?(path)
233
+
234
+ blob = JSON.parse(File.read(path))
235
+ by_agent = Hash.new { |h, k| h[k] = {} }
236
+ unknown = []
237
+ (blob["cases"] || {}).each do |case_id, entry|
238
+ golden = golden_store.find(case_id)
239
+ next unknown << case_id if golden.nil?
240
+
241
+ by_agent[golden.agent][case_id] = entry
242
+ end
243
+
244
+ by_agent.each { |agent, cases| baseline_store.put(agent, { "at" => blob["at"], "cases" => cases }) }
245
+ puts "imported #{by_agent.values.sum(&:size)} case(s) into #{by_agent.size} agent baseline(s) from #{path}"
246
+ by_agent.keys.sort.each { |a| puts " #{a} (#{by_agent[a].size})" }
247
+ return if unknown.empty?
248
+
249
+ warn " #{unknown.size} case(s) are not in the golden store and were NOT recorded: #{unknown.sort.join(', ')}"
250
+ warn " run `insika evals:import` first, then import the baseline again"
251
+ end
252
+
253
+ def baseline_export(path)
254
+ store = baseline_store
255
+ merged = store.agents.each_with_object({}) { |a, acc| acc.merge!(store.get(a)["cases"] || {}) }
256
+ at = store.agents.filter_map { |a| store.get(a)["at"] }.max
257
+ File.write(path, JSON.pretty_generate({ "at" => at, "cases" => merged }))
258
+ puts "exported #{merged.size} case(s) from #{store.agents.size} agent baseline(s) to #{path}"
259
+ end
260
+
261
+ # -- helpers -------------------------------------------------------
262
+
263
+ def baseline_store
264
+ backend = Insika::Wiring::Graph.backend_from_env
265
+ Insika::BaselineStore.new(config_store: Insika::ConfigStore.new(store: backend))
266
+ end
267
+
268
+ def golden_store
269
+ backend = Insika::Wiring::Graph.backend_from_env
270
+ Insika::GoldenStore.new(config_store: Insika::ConfigStore.new(store: backend))
271
+ end
272
+
273
+ # Builds the handler over the real durable backend — same "no app boot" rule as
274
+ # `doctor`: no DEEPSEEK, no seeding, no reactor. Safe against a live volume,
275
+ # because the only write is the run record.
276
+ def run_refinement(opts)
277
+ backend = Insika::Wiring::Graph.backend_from_env
278
+ config_store = Insika::ConfigStore.new(store: backend)
279
+ profiles = Insika::StoredProfileSource.new(config_store: config_store)
280
+ handler = Insika::Commands::RunRefinement.new(
281
+ profiles: profiles,
282
+ refinement_store: Insika::RefinementStore.new(store: backend),
283
+ collector: Insika::Refinement::EvidenceCollector.new(
284
+ task_store: Insika::TaskStore.new(store: backend),
285
+ session_store: Insika::SessionStore.new(store: backend),
286
+ tool_trace_store: Insika::ToolTraceStore.new(store: backend),
287
+ profiles: profiles,
288
+ settings_store: Insika::SettingsStore.new(config_store: config_store)
289
+ ),
290
+ event_stream: Insika::EventStream.new
291
+ )
292
+ handler.call(Insika::Command.build(:run_refinement, {
293
+ agent: opts[:agent], since: opts[:since],
294
+ last_sessions: opts[:last_sessions], full: opts[:full],
295
+ exclude_sessions: opts[:exclude_sessions]
296
+ }.compact, transport: :cli))
297
+ end
298
+
299
+ INDENT = " " * 23 # aligns the provenance line under the finding's title
300
+
301
+ def refine_report(run)
302
+ head = "#{run.agent_id} — #{run.status} (#{refine_window(run.window)})"
303
+ return "#{head}\n #{run.error}" if run.status == :failed
304
+ return "#{head}\n nothing found in this window" if run.findings.empty?
305
+
306
+ rows = run.findings.map do |f|
307
+ line = format(" %-14s ×%-4d %s", f["kind"], f["count"], f["title"])
308
+ line += "\n#{INDENT}#{f['detail']}" if f["detail"]
309
+ line += "\n#{INDENT}sessions: #{Array(f['sessions']).join(', ')}" if Array(f["sessions"]).any?
310
+ line
311
+ end
312
+ ([head] + rows).join("\n")
313
+ end
314
+
315
+ # An empty window in the record means "the collector's own default" — resolve it
316
+ # for display instead of printing the word "default".
317
+ def refine_window(window)
318
+ return "since #{window['since']}" if window["since"]
319
+
320
+ n = window["last_sessions"] || Insika::Refinement::EvidenceCollector::DEFAULT_WINDOW
321
+ "last #{n} session(s)"
322
+ end
323
+
324
+ # Config stores over the real durable backend — no app boot / no DEEPSEEK.
325
+ def build_doctor
326
+ backend = Insika::Wiring::Graph.backend_from_env
327
+ config_store = Insika::ConfigStore.new(store: backend)
328
+ Insika::Doctor.new(
329
+ env: ENV,
330
+ settings_store: Insika::SettingsStore.new(config_store: config_store),
331
+ llm_provider_store: Insika::LLMProviderStore.new(config_store: config_store),
332
+ tool_store: Insika::ToolStore.new(config_store: config_store),
333
+ # Prompt-file corruption is invisible from the outside (the file is there, the
334
+ # agent answers) — the sweep is the only thing that names it.
335
+ agent_file_store: Insika::AgentFileStore.new(config_store: config_store),
336
+ # Skills + profiles: the eagerness/drift checks are about the RELATION between
337
+ # a skill and the agents holding it, so neither half alone is enough. The
338
+ # catalog brings the DISK seeds into the sweep (deploy/skills in a deployment,
339
+ # skills/ in the minimal wiring — whichever exists here); without it a stale
340
+ # `eager:` or a Natura-in-shared-body sitting in a seed pack is invisible.
341
+ skill_store: Insika::SkillStore.new(config_store: config_store),
342
+ skill_catalog: Insika::SkillCatalog.new(skill_roots),
343
+ profile_source: Insika::StoredProfileSource.new(config_store: config_store),
344
+ backend: backend
345
+ )
346
+ end
347
+
348
+ # The conventional seed roots, relative to where the operator runs the CLI —
349
+ # same "no app boot" rule as the rest of `doctor`, so the composition root that
350
+ # actually declares them is never loaded.
351
+ def skill_roots
352
+ %w[deploy/skills skills].map { |p| File.join(Dir.pwd, p) }.select { |p| Dir.exist?(p) }
353
+ end
354
+
355
+ def display_value(spec, raw)
356
+ return "(unset)" if raw.nil?
357
+
358
+ spec.secret? ? "•••• (set, #{raw.to_s.length} chars)" : raw
359
+ end
360
+ end
361
+ end
362
+
363
+ # No `if $PROGRAM_NAME == __FILE__` guard: the RubyGems wrapper `load`s this file
364
+ # (Gem.activate_bin_path), so under `gem install` the guard is false and the CLI
365
+ # would silently do nothing. This file is only ever an entrypoint.
366
+ Insika::CLI.main(ARGV)