insika 0.1.0 → 0.3.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +199 -5
  3. data/README.md +8 -2
  4. data/bin/insika +231 -13
  5. data/docs/AGENTS.md +505 -6
  6. data/docs/API.md +56 -0
  7. data/docs/CHANNELS.md +100 -10
  8. data/docs/CONTEXT.md +147 -19
  9. data/docs/DEPLOY.md +34 -11
  10. data/docs/EMBEDDING.md +11 -7
  11. data/docs/EVALS.md +20 -1
  12. data/docs/FACTS.md +135 -0
  13. data/docs/HARVEST.md +117 -0
  14. data/docs/LOADTEST.md +17 -10
  15. data/docs/OBSERVABILITY.md +65 -2
  16. data/docs/REFINEMENT.md +9 -9
  17. data/docs/RELEASING.md +34 -7
  18. data/docs/RUNNING-LOCAL.md +4 -4
  19. data/docs/SECURITY.md +85 -11
  20. data/docs/SKILLS.md +189 -3
  21. data/docs/SOAK.md +127 -0
  22. data/docs/TOOLS.md +70 -2
  23. data/docs/WHY.md +1 -1
  24. data/docs/WORKFLOWS.md +2 -2
  25. data/docs/domain.md +115 -0
  26. data/docs/index.md +2 -2
  27. data/docs/onboarding/start.md +1 -1
  28. data/lib/insika/agent_profile.rb +228 -26
  29. data/lib/insika/alert_dispatcher.rb +139 -0
  30. data/lib/insika/balloon_splitter.rb +102 -0
  31. data/lib/insika/baseline_store.rb +2 -2
  32. data/lib/insika/budget_ledger.rb +166 -0
  33. data/lib/insika/cache_series_store.rb +49 -0
  34. data/lib/insika/channel_delivery.rb +132 -24
  35. data/lib/insika/channel_registry.rb +1 -1
  36. data/lib/insika/channels/relay.rb +80 -6
  37. data/lib/insika/channels/web/widget.js +2 -2
  38. data/lib/insika/channels/web.rb +9 -9
  39. data/lib/insika/channels/webhook.rb +58 -0
  40. data/lib/insika/chat_builder.rb +145 -13
  41. data/lib/insika/checkpoint_store.rb +16 -0
  42. data/lib/insika/circuit_state.rb +114 -0
  43. data/lib/insika/coercion.rb +8 -0
  44. data/lib/insika/commands/agent_payload.rb +6 -4
  45. data/lib/insika/commands/cancel_followup.rb +49 -0
  46. data/lib/insika/commands/create_agent.rb +2 -2
  47. data/lib/insika/commands/create_session.rb +1 -1
  48. data/lib/insika/commands/delete_llm_provider.rb +1 -1
  49. data/lib/insika/commands/delete_skill.rb +43 -0
  50. data/lib/insika/commands/delete_tenant_data.rb +95 -0
  51. data/lib/insika/commands/export_customer_memory.rb +48 -0
  52. data/lib/insika/commands/forget_customer.rb +117 -0
  53. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  54. data/lib/insika/commands/gate_harvest.rb +138 -0
  55. data/lib/insika/commands/gate_refinement.rb +12 -12
  56. data/lib/insika/commands/import_mcp_tools.rb +1 -1
  57. data/lib/insika/commands/import_tools.rb +4 -4
  58. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  59. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  60. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  61. data/lib/insika/commands/memory_put_fact.rb +23 -4
  62. data/lib/insika/commands/promote_harvest.rb +130 -0
  63. data/lib/insika/commands/record_outcome.rb +46 -0
  64. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  65. data/lib/insika/commands/reject_harvest.rb +38 -0
  66. data/lib/insika/commands/resolve_proposal.rb +108 -0
  67. data/lib/insika/commands/resolve_refinement.rb +1 -1
  68. data/lib/insika/commands/revoke_contact.rb +49 -0
  69. data/lib/insika/commands/revoke_token.rb +39 -0
  70. data/lib/insika/commands/rollback_harvest.rb +86 -0
  71. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  72. data/lib/insika/commands/run_distillation.rb +186 -0
  73. data/lib/insika/commands/run_harvest.rb +393 -0
  74. data/lib/insika/commands/run_refinement.rb +5 -5
  75. data/lib/insika/commands/send_message.rb +112 -15
  76. data/lib/insika/commands/session_purge.rb +67 -0
  77. data/lib/insika/commands/set_agent_tools.rb +1 -1
  78. data/lib/insika/commands/set_skill_agents.rb +60 -19
  79. data/lib/insika/commands/trigger_workflow.rb +1 -1
  80. data/lib/insika/commands/update_agent.rb +1 -1
  81. data/lib/insika/commands/write_data_tool.rb +1 -1
  82. data/lib/insika/commands/write_golden.rb +1 -1
  83. data/lib/insika/commands/write_skill.rb +19 -9
  84. data/lib/insika/config_store.rb +8 -4
  85. data/lib/insika/contact_store.rb +183 -0
  86. data/lib/insika/context/builder.rb +23 -5
  87. data/lib/insika/context/fragment.rb +31 -3
  88. data/lib/insika/context/priority.rb +6 -2
  89. data/lib/insika/context/provider.rb +17 -3
  90. data/lib/insika/context/providers/briefing.rb +96 -0
  91. data/lib/insika/context/providers/memory.rb +16 -7
  92. data/lib/insika/context/providers/prompt.rb +30 -2
  93. data/lib/insika/context/providers/request.rb +1 -1
  94. data/lib/insika/context/providers/session.rb +17 -2
  95. data/lib/insika/context/providers/skill.rb +7 -1
  96. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  97. data/lib/insika/context/providers/tool_search.rb +2 -0
  98. data/lib/insika/context_trace_store.rb +128 -0
  99. data/lib/insika/delegation_store.rb +2 -2
  100. data/lib/insika/distill.rb +224 -0
  101. data/lib/insika/distill_engine.rb +169 -0
  102. data/lib/insika/doctor.rb +962 -7
  103. data/lib/insika/dsl/runtime.rb +20 -11
  104. data/lib/insika/dsl/server_boot.rb +74 -4
  105. data/lib/insika/dsl/system.rb +1 -1
  106. data/lib/insika/dsl.rb +152 -15
  107. data/lib/insika/edge_limiter.rb +167 -8
  108. data/lib/insika/egress_guard.rb +3 -3
  109. data/lib/insika/env_schema.rb +22 -12
  110. data/lib/insika/errors.rb +72 -5
  111. data/lib/insika/evals/assertions.rb +15 -14
  112. data/lib/insika/evals/baseline.rb +3 -3
  113. data/lib/insika/evals/golden.rb +8 -8
  114. data/lib/insika/evals/judge.rb +7 -7
  115. data/lib/insika/evals/pairwise.rb +21 -9
  116. data/lib/insika/evals/report.rb +2 -2
  117. data/lib/insika/evals/runner.rb +6 -6
  118. data/lib/insika/evals/transport.rb +2 -2
  119. data/lib/insika/event_stream.rb +23 -5
  120. data/lib/insika/evidence.rb +183 -0
  121. data/lib/insika/executor.rb +1092 -160
  122. data/lib/insika/followup_engine.rb +207 -0
  123. data/lib/insika/followup_policy.rb +221 -0
  124. data/lib/insika/followup_store.rb +306 -0
  125. data/lib/insika/frontmatter.rb +1 -1
  126. data/lib/insika/funnel_declaration.rb +106 -0
  127. data/lib/insika/funnel_fold.rb +179 -0
  128. data/lib/insika/funnel_store.rb +163 -0
  129. data/lib/insika/golden_store.rb +3 -3
  130. data/lib/insika/grounding/matcher.rb +69 -0
  131. data/lib/insika/grounding.rb +44 -0
  132. data/lib/insika/harvest/conversion_gate.rb +159 -0
  133. data/lib/insika/harvest/criterion.rb +98 -0
  134. data/lib/insika/harvest/gate.rb +194 -0
  135. data/lib/insika/harvest/negative_list.rb +199 -0
  136. data/lib/insika/harvest.rb +241 -0
  137. data/lib/insika/harvest_engine.rb +193 -0
  138. data/lib/insika/harvest_store.rb +548 -0
  139. data/lib/insika/http_client.rb +3 -3
  140. data/lib/insika/inbound_log.rb +1 -1
  141. data/lib/insika/llm_configurator.rb +3 -3
  142. data/lib/insika/loop_detector.rb +143 -0
  143. data/lib/insika/mcp_http_client.rb +4 -4
  144. data/lib/insika/mcp_tool_ingestor.rb +6 -6
  145. data/lib/insika/media.rb +298 -0
  146. data/lib/insika/memory_audit_store.rb +85 -0
  147. data/lib/insika/memory_store.rb +264 -23
  148. data/lib/insika/message_origin.rb +8 -3
  149. data/lib/insika/model_resolver.rb +1 -1
  150. data/lib/insika/model_selection.rb +5 -4
  151. data/lib/insika/model_visible.rb +87 -0
  152. data/lib/insika/model_visible_trace_store.rb +66 -0
  153. data/lib/insika/onboarding.rb +8 -3
  154. data/lib/insika/outbox_store.rb +44 -6
  155. data/lib/insika/outcome_store.rb +147 -0
  156. data/lib/insika/overlay_tool_registry.rb +3 -4
  157. data/lib/insika/pack.rb +3 -3
  158. data/lib/insika/pack_importer.rb +17 -15
  159. data/lib/insika/packaging.rb +163 -0
  160. data/lib/insika/parity/criterion.rb +79 -0
  161. data/lib/insika/parity/verdict.rb +318 -0
  162. data/lib/insika/pending_action_store.rb +1 -1
  163. data/lib/insika/plugin/loader.rb +2 -2
  164. data/lib/insika/policy/policy.rb +1 -1
  165. data/lib/insika/prefix_fingerprint.rb +58 -0
  166. data/lib/insika/profile_source.rb +34 -7
  167. data/lib/insika/proposal_store.rb +271 -0
  168. data/lib/insika/provider_error_classifier.rb +160 -0
  169. data/lib/insika/queue_policy.rb +6 -3
  170. data/lib/insika/recovery.rb +47 -6
  171. data/lib/insika/refinement/candidate.rb +4 -4
  172. data/lib/insika/refinement/evidence_collector.rb +6 -6
  173. data/lib/insika/refinement/gate.rb +7 -7
  174. data/lib/insika/refinement/panel.rb +7 -7
  175. data/lib/insika/refinement/proposer.rb +10 -10
  176. data/lib/insika/refinement_store.rb +12 -12
  177. data/lib/insika/reliability.rb +211 -0
  178. data/lib/insika/retention.rb +281 -0
  179. data/lib/insika/routing.rb +101 -0
  180. data/lib/insika/safety/config.rb +46 -6
  181. data/lib/insika/safety/corpus.rb +255 -0
  182. data/lib/insika/safety/detectors.rb +34 -115
  183. data/lib/insika/safety/factory.rb +18 -5
  184. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  185. data/lib/insika/safety/grounding_validator.rb +49 -0
  186. data/lib/insika/safety/input_guardrail.rb +20 -5
  187. data/lib/insika/safety/moderator.rb +19 -11
  188. data/lib/insika/safety/output_filter.rb +10 -6
  189. data/lib/insika/safety/output_validator.rb +13 -7
  190. data/lib/insika/safety/safe_responses.rb +1 -1
  191. data/lib/insika/sandbox/boundary.rb +2 -2
  192. data/lib/insika/sandbox.rb +1 -1
  193. data/lib/insika/schema_guard.rb +35 -0
  194. data/lib/insika/server/app.rb +366 -54
  195. data/lib/insika/server/boot.rb +4 -4
  196. data/lib/insika/server/rack_app.rb +31 -7
  197. data/lib/insika/server/responses.rb +58 -9
  198. data/lib/insika/server/tenant_auth.rb +61 -0
  199. data/lib/insika/session_actor.rb +11 -7
  200. data/lib/insika/session_store.rb +66 -3
  201. data/lib/insika/settings_store.rb +15 -5
  202. data/lib/insika/shadow_pair_store.rb +258 -0
  203. data/lib/insika/shutdown.rb +4 -4
  204. data/lib/insika/skill_catalog.rb +131 -20
  205. data/lib/insika/skill_store.rb +70 -22
  206. data/lib/insika/soak/envelope.rb +140 -0
  207. data/lib/insika/soak/report.rb +392 -0
  208. data/lib/insika/soak/runner.rb +554 -0
  209. data/lib/insika/steer_injector.rb +1 -1
  210. data/lib/insika/store.rb +11 -2
  211. data/lib/insika/stores/memory.rb +6 -0
  212. data/lib/insika/stores/sqlite.rb +8 -0
  213. data/lib/insika/studio/app.rb +1058 -75
  214. data/lib/insika/studio/assets/dist/application.css +1 -1
  215. data/lib/insika/studio/assets/dist/application.js +27 -26
  216. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  217. data/lib/insika/studio/forms.rb +274 -22
  218. data/lib/insika/studio/nav_icons.rb +7 -2
  219. data/lib/insika/studio/views/_message.erb +2 -2
  220. data/lib/insika/studio/views/agent_detail.erb +629 -86
  221. data/lib/insika/studio/views/agents.erb +11 -7
  222. data/lib/insika/studio/views/approvals.erb +4 -1
  223. data/lib/insika/studio/views/chats.erb +4 -1
  224. data/lib/insika/studio/views/customer.erb +94 -0
  225. data/lib/insika/studio/views/customers.erb +32 -0
  226. data/lib/insika/studio/views/evals.erb +4 -1
  227. data/lib/insika/studio/views/facts.erb +133 -0
  228. data/lib/insika/studio/views/followups.erb +125 -0
  229. data/lib/insika/studio/views/funnel.erb +106 -0
  230. data/lib/insika/studio/views/harvest.erb +234 -0
  231. data/lib/insika/studio/views/home.erb +2 -1
  232. data/lib/insika/studio/views/layout.erb +1 -0
  233. data/lib/insika/studio/views/parity.erb +147 -0
  234. data/lib/insika/studio/views/playground.erb +7 -1
  235. data/lib/insika/studio/views/refinement.erb +4 -4
  236. data/lib/insika/studio/views/session.erb +133 -3
  237. data/lib/insika/studio/views/settings.erb +9 -12
  238. data/lib/insika/studio/views/skills.erb +66 -12
  239. data/lib/insika/studio/views/system_files.erb +1 -1
  240. data/lib/insika/studio/views/task.erb +13 -0
  241. data/lib/insika/studio/views/tasks.erb +4 -1
  242. data/lib/insika/studio/views/tools.erb +0 -1
  243. data/lib/insika/subagent_graph.rb +3 -3
  244. data/lib/insika/task_actor.rb +3 -3
  245. data/lib/insika/task_store.rb +22 -2
  246. data/lib/insika/telemetry/pricing.rb +3 -3
  247. data/lib/insika/telemetry/recorder.rb +1 -1
  248. data/lib/insika/telemetry.rb +2 -2
  249. data/lib/insika/testing/store_contract.rb +54 -33
  250. data/lib/insika/tick.rb +146 -0
  251. data/lib/insika/token_store.rb +168 -0
  252. data/lib/insika/tool_assembly.rb +5 -5
  253. data/lib/insika/tool_definition.rb +25 -15
  254. data/lib/insika/tool_envelope.rb +70 -1
  255. data/lib/insika/tool_manifest.rb +11 -7
  256. data/lib/insika/tool_output_compressor.rb +100 -0
  257. data/lib/insika/tool_store.rb +1 -1
  258. data/lib/insika/tool_trace_store.rb +1 -1
  259. data/lib/insika/tools/concurrency.rb +2 -2
  260. data/lib/insika/tools/data_defined_tool.rb +14 -5
  261. data/lib/insika/tools/generate_image.rb +44 -0
  262. data/lib/insika/tools/load_skill.rb +61 -3
  263. data/lib/insika/tools/schedule_followup.rb +164 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +4 -4
  266. data/lib/insika/tools/subagents.rb +1 -1
  267. data/lib/insika/tools/tts.rb +47 -0
  268. data/lib/insika/tools/update_briefing.rb +126 -0
  269. data/lib/insika/turn_output.rb +2 -2
  270. data/lib/insika/turn_state.rb +54 -13
  271. data/lib/insika/turn_timing.rb +24 -4
  272. data/lib/insika/usage_ledger.rb +1 -1
  273. data/lib/insika/version.rb +1 -1
  274. data/lib/insika/vitals.rb +84 -0
  275. data/lib/insika/wiring/graph.rb +372 -34
  276. data/lib/insika/workflow.rb +1 -1
  277. data/lib/insika/workflow_registry.rb +1 -1
  278. data/lib/insika.rb +122 -16
  279. metadata +95 -2
  280. data/lib/insika/server/admin_auth.rb +0 -29
@@ -1,7 +1,7 @@
1
1
  <%# Settings — drill-down (T2): section list | selected section's form(s).
2
2
  @section (general|models|llm) comes from ?s= and drives the detail. %>
3
3
  <% sections = [
4
- ["general", "General", "streaming, timeouts, compaction"],
4
+ ["general", "General", "streaming, timeouts"],
5
5
  ["models", "Model defaults", "platform model layer (Chat › Agent › platform)"],
6
6
  ["edge", "Edge limits", "rate-limit per chat, token ceiling per agent"],
7
7
  ["evals", "Evals", "who grades a rubric, and how a panel agrees"],
@@ -27,7 +27,7 @@
27
27
  <button type="submit" form="set-general" class="btn primary" data-turbo-submits-with="Saving…">Save</button>
28
28
  </div>
29
29
  <div class="drill-pane-body">
30
- <p class="muted">Streaming, timeouts and compaction — saved via <code>update_settings</code> (durable, applies next turn).</p>
30
+ <p class="muted">Streaming and timeouts — saved via <code>update_settings</code> (durable, applies next turn).</p>
31
31
  <form id="set-general" method="post" action="/studio/settings">
32
32
  <%== csrf_tag %>
33
33
  <label class="check">
@@ -39,12 +39,9 @@
39
39
  <label>max_retries<input type="text" name="max_retries" value="<%= @settings["max_retries"] %>" inputmode="numeric"></label>
40
40
  <label>turn_timeout (s)<input type="text" name="turn_timeout" value="<%= @settings["turn_timeout"] %>" inputmode="numeric"></label>
41
41
  <label>tool_timeout (s)<input type="text" name="tool_timeout" value="<%= @settings["tool_timeout"] %>" inputmode="numeric"></label>
42
+ <label>Memory TTL (days)<input type="text" name="memory_ttl_days" value="<%= @settings["memory_ttl_days"] %>" inputmode="numeric" placeholder="blank = off"></label>
42
43
  </div>
43
- <label class="check">
44
- <input type="checkbox" name="compaction_enabled" value="1"<%== " checked" if @settings.dig("compaction", "enabled") %>>
45
- context compaction
46
- </label>
47
- <label>compaction.keep_last (messages)<input type="text" name="keep_last" value="<%= @settings.dig("compaction", "keep_last") %>" inputmode="numeric"></label>
44
+ <p class="muted">Memory TTL: how many days a customer memory cell lives before the daily sweep prunes it (per-fact <code>expires_at</code> overrides always win). Editing here sets the platform default for <strong>every</strong> cell; a per-tenant map authored in the settings record is replaced by this save.</p>
48
45
  </form>
49
46
  </div>
50
47
 
@@ -58,11 +55,11 @@
58
55
  <form id="set-models" method="post" action="/studio/settings/models">
59
56
  <%== csrf_tag %>
60
57
  <div class="grid-2">
61
- <label>default_model <span class="muted">(agents without their own model)</span><input type="text" name="default_model" value="<%= @settings["default_model"] %>" placeholder="deepseek-chat"></label>
58
+ <label>default_model <span class="muted">(agents without their own model)</span><input type="text" name="default_model" value="<%= @settings["default_model"] %>" placeholder="deepseek-v4-flash"></label>
62
59
  <label>default_provider <span class="muted">(optional)</span><input type="text" name="default_provider" value="<%= @settings["default_provider"] %>" placeholder="deepseek"></label>
63
60
  </div>
64
- <label>fallback_models <span class="muted">(ordered chain; <code>provider/model</code> or <code>model</code>, comma or newline)</span><input type="text" name="fallback_models" value="<%= Array(@settings["fallback_models"]).join(", ") %>" placeholder="deepseek/deepseek-chat, openai/gpt-4o-mini"></label>
65
- <label>utility_model <span class="muted">(cheap internal tasks — titles, distillation)</span><input type="text" name="utility_model" value="<%= @settings["utility_model"] %>" placeholder="deepseek-chat"></label>
61
+ <label>fallback_models <span class="muted">(ordered chain; <code>provider/model</code> or <code>model</code>, comma or newline)</span><input type="text" name="fallback_models" value="<%= Array(@settings["fallback_models"]).join(", ") %>" placeholder="deepseek/deepseek-v4-flash, openai/gpt-4o-mini"></label>
62
+ <label>utility_model <span class="muted">(cheap internal tasks — titles, distillation)</span><input type="text" name="utility_model" value="<%= @settings["utility_model"] %>" placeholder="deepseek-v4-flash"></label>
66
63
  <label>reasoning <span class="muted">(global default — off/on/effort; blank = provider default)</span>
67
64
  <%== thinking_select("thinking", @settings["thinking"], "provider default") %>
68
65
  </label>
@@ -107,7 +104,7 @@
107
104
  <p class="muted">Who grades a <a href="/studio/evals">case</a>'s rubric. <strong>One model per line</strong> as <code>provider/model</code> — several lines make a PANEL, and two different models disagreeing is the signal worth having (sampling one model N times mostly measures its own variance). Empty = deterministic assertions only, and rubric'd cases read as <code>judge_pending</code>.</p>
108
105
  <form id="set-evals" method="post" action="/studio/settings/evals">
109
106
  <%== csrf_tag %>
110
- <label>judges<textarea name="judges" rows="4" spellcheck="false" placeholder="deepseek/deepseek-chat&#10;openai/gpt-5-mini"><%= Array(@settings.dig("evals", "judges")).map { |j| [j["provider"], j["model"]].compact.reject(&:empty?).join("/") }.join("\n") %></textarea></label>
107
+ <label>judges<textarea name="judges" rows="4" spellcheck="false" placeholder="deepseek/deepseek-v4-flash&#10;openai/gpt-5-mini"><%= Array(@settings.dig("evals", "judges")).map { |j| [j["provider"], j["model"]].compact.reject(&:empty?).join("/") }.join("\n") %></textarea></label>
111
108
  <div class="grid-2">
112
109
  <label>aggregate <span class="muted">(how the panel's scores combine)</span>
113
110
  <select name="aggregate">
@@ -147,7 +144,7 @@
147
144
  <label>auth_header<input type="text" name="auth_header" value="<%= p["auth_header"] %>" placeholder="Authorization"></label>
148
145
  </div>
149
146
  <label>api_key<input type="password" name="api_key" value="<%= p["api_key"] %>" autocomplete="off"></label>
150
- <label>models <span class="muted">(comma or newline)</span><input type="text" name="models" value="<%= Array(p["models"]).join(", ") %>" placeholder="deepseek-chat, deepseek-reasoner"></label>
147
+ <label>models <span class="muted">(comma or newline)</span><input type="text" name="models" value="<%= Array(p["models"]).join(", ") %>" placeholder="deepseek-v4-flash, deepseek-reasoner"></label>
151
148
  <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Saving…">Save provider</button></div>
152
149
  </form>
153
150
  </div>
@@ -1,6 +1,9 @@
1
1
  <%# Skills — drill-down (T2): master list | detail editor + availability.
2
2
  Every skill route renders THIS view (index / new / :name); @selected drives
3
- which skill is open (nil = none, "" = new). %>
3
+ which skill is open (nil = none, "" = new). @scope_agent set = editing THAT
4
+ agent's own version of the skill, not the shared one. %>
5
+ <% scope = @scope_agent %>
6
+ <% form_action = scope ? "/studio/agents/#{Rack::Utils.escape(scope)}/skills/#{Rack::Utils.escape(@selected.to_s)}" : "/studio/skills" %>
4
7
  <div class="drill" data-controller="list-filter">
5
8
  <aside class="drill-pane drill-master">
6
9
  <div class="drill-pane-head">
@@ -36,15 +39,29 @@
36
39
  <% new = @selected == "" %>
37
40
  <div class="drill-pane-head">
38
41
  <div class="title">
39
- <nav class="crumbs"><a href="/studio/skills">skills</a> / <span><%= new ? "new" : @selected %></span></nav>
40
- <strong class="mono"><%= new ? "New skill" : @selected %></strong>
42
+ <strong class="mono"><%= new ? "New skill" : @selected %><% if scope %> <span class="pill warn">specialized for <%= scope %></span><% end %></strong>
41
43
  </div>
42
44
  <button type="submit" form="skill-form" class="btn primary" data-turbo-submits-with="Saving…">Save skill</button>
43
45
  </div>
44
46
  <div class="drill-pane-body">
45
- <form id="skill-form" method="post" action="/studio/skills" data-controller="dirty-guard">
47
+ <%# The specialization is one MORE record, not a fork: the name stays the bare
48
+ shared name everywhere the model and the operator see it, and deleting the
49
+ override falls back to the shared body. %>
50
+ <% if scope %>
51
+ <div class="drill-section">
52
+ <p class="muted">This is <strong><%= scope %></strong>'s own version of <code><%= @selected %></code>. Every
53
+ other agent keeps the <a href="/studio/skills/<%= Rack::Utils.escape(@selected.to_s) %>">shared skill</a>,
54
+ and the name does not change — the allowlist, <code>&lt;available_skills&gt;</code> and
55
+ <code>load_skill</code> all still say <code><%= @selected %></code>.</p>
56
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(scope) %>/skills/<%= Rack::Utils.escape(@selected.to_s) %>/delete">
57
+ <%== csrf_tag %>
58
+ <div class="actions"><button type="submit" data-turbo-confirm="Remove this specialization? <%= scope %> falls back to the shared skill.">Stop specializing</button></div>
59
+ </form>
60
+ </div>
61
+ <% end %>
62
+ <form id="skill-form" method="post" action="<%= form_action %>" data-controller="dirty-guard">
46
63
  <%== csrf_tag %>
47
- <% unless new %><input type="hidden" name="name" value="<%= @selected %>"><% end %>
64
+ <% if !new && !scope %><input type="hidden" name="name" value="<%= @selected %>"><% end %>
48
65
  <% if new %>
49
66
  <label>name <span class="muted">(canonical — same as <code>name:</code> in the frontmatter)</span>
50
67
  <input type="text" name="name" placeholder="my-skill" required>
@@ -56,29 +73,66 @@
56
73
  </div>
57
74
  </form>
58
75
 
59
- <% unless new %>
76
+ <% if !new && !scope %>
60
77
  <div class="drill-section">
61
78
  <div class="drill-section-head">
62
79
  <h3>Availability</h3>
63
- <span class="muted">which agents can <code>load_skill</code> this — an "all" allowlist is left untouched</span>
80
+ <span class="muted">per agent: <strong>allowed</strong> = can <code>load_skill</code> it; <strong>always on</strong> = the body
81
+ is in the prompt every turn. Both are the AGENT's decision, so one shared skill can be
82
+ always-on for one and discretionary for another. An "all" allowlist is left untouched.</span>
64
83
  </div>
65
84
  <form method="post" action="/studio/skills/<%= Rack::Utils.escape(@selected) %>/agents">
66
85
  <%== csrf_tag %>
67
86
  <% if @agents.empty? %>
68
87
  <p class="muted">No agents configured.</p>
69
88
  <% else %>
70
- <div class="check-grid">
89
+ <div class="check-grid agent-grid">
71
90
  <% @agents.each do |a| %>
72
- <label class="check">
73
- <input type="checkbox" name="agent_ids[]" value="<%= a.id %>"<%== " checked" if skill_enabled_for?(a, @selected) %>>
74
- <span class="mono"><%= a.id %></span>
75
- </label>
91
+ <% own = Array((@specialized || {})[@selected]).include?(a.id) %>
92
+ <div class="agent-check">
93
+ <div class="agent-check-name">
94
+ <span class="mono"><%= a.id %></span>
95
+ <% if own %>
96
+ <a class="pill warn" href="/studio/agents/<%= Rack::Utils.escape(a.id) %>/skills/<%= Rack::Utils.escape(@selected) %>">own version</a>
97
+ <% end %>
98
+ </div>
99
+ <label class="check tool-check">
100
+ <input type="checkbox" name="agent_ids[]" value="<%= a.id %>"<%== " checked" if skill_enabled_for?(a, @selected) %>>
101
+ <span class="mono">allowed</span>
102
+ </label>
103
+ <label class="check tool-check">
104
+ <input type="checkbox" name="eager_ids[]" value="<%= a.id %>"<%== " checked" if skill_eager_for?(a, @selected) %>>
105
+ <span class="mono">always on</span>
106
+ </label>
107
+ </div>
76
108
  <% end %>
77
109
  </div>
78
110
  <div class="actions"><button type="submit" class="primary" data-turbo-submits-with="Applying…">Apply to agents</button></div>
79
111
  <% end %>
80
112
  </form>
81
113
  </div>
114
+
115
+ <%# Specializing is what keeps a shared skill shared: without it the only way
116
+ to give one agent a different version was to fork it under a new name and
117
+ edit the allowlist, which throws the sharing away. %>
118
+ <% others = @agents.reject { |a| Array((@specialized || {})[@selected]).include?(a.id) } %>
119
+ <% if others.any? %>
120
+ <div class="drill-section">
121
+ <div class="drill-section-head">
122
+ <h3>Specialize</h3>
123
+ <span class="muted">give ONE agent its own version of this skill, seeded from the body above — same name, no fork</span>
124
+ </div>
125
+ <form method="post" action="/studio/skills/<%= Rack::Utils.escape(@selected) %>/specialize">
126
+ <%== csrf_tag %>
127
+ <label>agent
128
+ <select name="agent_id" required>
129
+ <% others.each do |a| %><option value="<%= a.id %>"><%= a.id %></option><% end %>
130
+ </select>
131
+ </label>
132
+ <div class="actions"><button type="submit" data-turbo-submits-with="Creating…">Specialize for this agent</button></div>
133
+ </form>
134
+ </div>
135
+ <% end %>
82
136
  <% end %>
83
137
  </div>
84
138
  <% end %>
@@ -5,7 +5,7 @@
5
5
  </div>
6
6
  </header>
7
7
 
8
- <p class="muted matrix-hint">GLOBAL prompts/rules: the runtime injects these files <strong>before</strong> each BIA's own identity, every turn. Store-backed and versioned (like per-agent prompts). Empty = no global injection (the prompt stays exactly as before).</p>
8
+ <p class="muted matrix-hint">GLOBAL prompts/rules: the runtime injects these files <strong>before</strong> each agent's own identity, every turn. Store-backed and versioned (like per-agent prompts). Empty = no global injection (the prompt stays exactly as before).</p>
9
9
 
10
10
  <% if @system_files.empty? %>
11
11
  <div class="empty">No system files. Create one below (e.g. <code>HOUSE_RULES.md</code>) for a rule that applies to every agent.</div>
@@ -88,6 +88,19 @@
88
88
  <% end %>
89
89
  </section>
90
90
 
91
+ <% if @task.timing && !@task.timing.empty? %>
92
+ <section class="section">
93
+ <div class="section-head"><h2>Timing <span class="muted">· the turn's latency windows</span></h2></div>
94
+ <div class="card">
95
+ <ul class="kv">
96
+ <% @task.timing.each do |key, value| %>
97
+ <li><span class="k mono"><%= key %></span><span class="v mono"><%= value %></span></li>
98
+ <% end %>
99
+ </ul>
100
+ </div>
101
+ </section>
102
+ <% end %>
103
+
91
104
  <section class="section">
92
105
  <div class="section-head"><h2>Latest checkpoint</h2></div>
93
106
  <% if @checkpoint.nil? %>
@@ -3,7 +3,10 @@
3
3
  <nav class="crumbs"><span>tasks</span></nav>
4
4
  <h1>Tasks<span class="sub"><%= @tasks.size %> task(s)</span></h1>
5
5
  </div>
6
- <a class="btn" href="/studio/approvals">Approvals →</a>
6
+ <div class="head-actions">
7
+ <%== agent_filter_form("/studio/tasks", @agent) %>
8
+ <a class="btn" href="/studio/approvals">Approvals →</a>
9
+ </div>
7
10
  </header>
8
11
 
9
12
  <% if @tasks.empty? %>
@@ -47,7 +47,6 @@
47
47
  <% a = @sel_agent %>
48
48
  <div class="drill-pane-head">
49
49
  <div class="title">
50
- <nav class="crumbs"><a href="/studio/tools">tools</a> / <span><%= a.id %></span></nav>
51
50
  <strong class="mono"><%= a.id %></strong>
52
51
  </div>
53
52
  <button type="submit" form="tools-form" class="btn primary" data-turbo-submits-with="Saving…">Save tools</button>
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- # Definition-time integrity of the subagent delegation graph (RFC-0010 §4.4,
5
- # item 21). A pure function over `{id => [child_ids]}`: detects CYCLES and
4
+ # Definition-time integrity of the subagent delegation graph (
5
+ # A pure function over `{id => [child_ids]}`: detects CYCLES and
6
6
  # computes the max delegation DEPTH, raising a typed SubagentError so the
7
7
  # authoring Command (CreateAgent/UpdateAgent) fails cleanly and boot refuses a
8
8
  # bad static set. Cycle + bounded depth are exactly Flue's definition-time
@@ -21,7 +21,7 @@ module Insika
21
21
 
22
22
  # Max children a single `spawn_subagents` fan-out may run — also the concurrency
23
23
  # bound (N concurrent LLM calls hit the provider rate limit + the per-agent token
24
- # ceiling of item 33). Override with INSIKA_SUBAGENT_FANOUT_CAP.
24
+ # ceiling of). Override with INSIKA_SUBAGENT_FANOUT_CAP.
25
25
  DEFAULT_FANOUT_CAP = 8
26
26
 
27
27
  module_function
@@ -9,7 +9,7 @@ module Insika
9
9
  # `await` as the cooperative SUSPENSION primitive. Cancellation/suspension only
10
10
  # at stage boundaries — never in the middle of an operation.
11
11
  class TaskActor
12
- # `user_message` is posted by `Executor#steer_into_running` (RFC-0015 §5.1) and
12
+ # `user_message` is posted by `Executor#steer_into_running` and
13
13
  # consumed by `SteerInjector` at a tool-batch boundary. `pause`/`resume` (operator),
14
14
  # `approval` (human-in-the-loop), `timeout`/`heartbeat`
15
15
  # (watchdog/liveness, observation).
@@ -61,14 +61,14 @@ module Insika
61
61
  # the flag).
62
62
  def pause_requested? = @pause_requested
63
63
 
64
- # RFC-0015 §5.2 — takes the steered messages and clears the buffer, WITHOUT
64
+ # takes the steered messages and clears the buffer, WITHOUT
65
65
  # observing anything else in the mailbox: whatever is not a `:user_message` is put
66
66
  # back, in the order it arrived.
67
67
  #
68
68
  # Why not `drain!`: this runs INSIDE RubyLLM's tool loop (a `after_message`
69
69
  # callback), and `drain!` raises on `:cancel`. Cancellation is only ever observed
70
70
  # at the Executor's own stage boundaries — that is what keeps a tool batch one
71
- # unit of work (RFC-0015 §6.4, and D7 for the same rule under `turn_timeout`).
71
+ # unit of work (and for the same rule under `turn_timeout`).
72
72
  # Injecting a message must not quietly become a new place a turn can die.
73
73
  def take_user_messages!
74
74
  @mailbox.size.times do
@@ -33,7 +33,7 @@ module Insika
33
33
  }.freeze
34
34
 
35
35
  Task = Data.define(:id, :status, :command, :session_id, :executions,
36
- :mailbox_state, :created_at, :updated_at)
36
+ :mailbox_state, :timing, :created_at, :updated_at)
37
37
  Execution = Data.define(:attempt, :started_at, :finished_at, :outcome, :error)
38
38
 
39
39
  def initialize(store:)
@@ -131,7 +131,19 @@ module Insika
131
131
  to_task(record)
132
132
  end
133
133
 
134
- # RFC-0015 §5.3 (`collect`): appends a fragment to a task's message while it is
134
+ # writes the turn's timing breakdown onto the task record, once,
135
+ # when the turn completes. Best-effort by the caller's contract — a failure
136
+ # here must never re-fail an already-committed turn. `timing` is DATA (string
137
+ # keys, JSON types); deep_stringify makes a symbol-keyed to_h durable.
138
+ def record_timing(id, timing)
139
+ record = fetch!(id)
140
+ record["timing"] = deep_stringify(timing)
141
+ record["updated_at"] = timestamp
142
+ @store.set(SCOPE, key_for(id), record)
143
+ to_task(record)
144
+ end
145
+
146
+ # (`collect`): appends a fragment to a task's message while it is
135
147
  # still waiting at the door. -> Task.
136
148
  #
137
149
  # ONLY on :queued, and that guard is the whole safety of the feature: once a
@@ -185,6 +197,13 @@ module Insika
185
197
  end
186
198
  end
187
199
 
200
+ # -> true | false (did it exist?). For Retention (WS8) — a purged session's
201
+ # tasks go with it, terminal tasks only (a live :running task must never
202
+ # be deleted under its own fiber).
203
+ def delete(id)
204
+ @store.delete(SCOPE, key_for(id))
205
+ end
206
+
188
207
  private
189
208
 
190
209
  def key_for(id)
@@ -228,6 +247,7 @@ module Insika
228
247
  session_id: record["session_id"],
229
248
  executions: record["executions"].map { |e| to_execution(e) },
230
249
  mailbox_state: record["mailbox_state"],
250
+ timing: record["timing"],
231
251
  created_at: record["created_at"],
232
252
  updated_at: record["updated_at"]
233
253
  )
@@ -4,7 +4,7 @@ require "json"
4
4
 
5
5
  module Insika
6
6
  module Telemetry
7
- # Estimated cost of a turn, from a rates table given as DATA (item 16 / P4).
7
+ # Estimated cost of a turn, from a rates table given as DATA.
8
8
  #
9
9
  # Insika never ships prices: they change weekly, differ per contract and per
10
10
  # region, and a stale table in the engine would be worse than no number at all.
@@ -13,11 +13,11 @@ module Insika
13
13
  #
14
14
  # Rates are **USD per million tokens** (the unit every provider publishes):
15
15
  #
16
- # { "deepseek-chat" => { "input" => 0.27, "output" => 1.10,
16
+ # { "deepseek-v4-flash" => { "input" => 0.27, "output" => 1.10,
17
17
  # "cached_input" => 0.07, "cache_write" => 0.34 } }
18
18
  #
19
19
  # A key matches the model id the provider reports, with or without the
20
- # `provider/` prefix (`deepseek/deepseek-chat` and `deepseek-chat` both hit the
20
+ # `provider/` prefix (`deepseek/deepseek-v4-flash` and `deepseek-v4-flash` both hit the
21
21
  # entry above). An UNKNOWN model -> nil: a missing price is not a zero cost, so
22
22
  # nothing is emitted and the dashboard shows a gap instead of a lie.
23
23
  #
@@ -12,7 +12,7 @@ module Insika
12
12
  # insika.data_tool), correlated by task_id. Latency is the span duration; the
13
13
  # agent/tenant/model/tokens/cost ride as ATTRIBUTES.
14
14
  #
15
- # METRICS (item 16 / P4): the SAME events also feed counters and histograms, so
15
+ # METRICS: the SAME events also feed counters and histograms, so
16
16
  # volume/latency/tokens/cost are chartable WITHOUT span aggregation (which not
17
17
  # every backend does, and none does cheaply at retention). Metric attributes are
18
18
  # a deliberate LOW-CARDINALITY subset of the span attributes — never task_id or
@@ -5,7 +5,7 @@ require_relative "telemetry/pricing"
5
5
  require_relative "telemetry/recorder"
6
6
 
7
7
  module Insika
8
- # OPT-IN observability (Phase 6): OTEL mounted at the edge, core untouched. Rides
8
+ # OPT-IN observability: OTEL mounted at the edge, core untouched. Rides
9
9
  # the Event Stream — the Recorder consumes the events and emits spans and metrics.
10
10
  # Off (the default) -> `setup` returns nil and nothing is loaded or instrumented
11
11
  # (parity, zero overhead). The OTEL gems are only REQUIRED lazily in `setup`
@@ -17,7 +17,7 @@ module Insika
17
17
  # OTEL_* keys are the OpenTelemetry SDK's own env and stay verbatim; only our opt-in
18
18
  # toggle is renamed (INSIKA_OTEL, with the HARNESS_OTEL alias still read).
19
19
  #
20
- # Metrics (item 16 / P4) ride the same switch and the SAME standard env: the SDK
20
+ # Metrics ride the same switch and the SAME standard env: the SDK
21
21
  # registers a periodic metric reader when the OTLP metrics exporter is loadable,
22
22
  # and `OTEL_METRICS_EXPORTER=none` turns metrics off while traces stay on. No
23
23
  # Insika-specific toggle is invented for it.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Contract suite for Insika::Store (doc 01 §7), exported for third-party
4
- # backends (RFC-0018 A4) — a gem like `insika-pg` writes its spec against THIS
3
+ # Contract suite for Insika::Store, exported for third-party
4
+ # backends — a gem like `insika-pg` writes its spec against THIS
5
5
  # file, not against a read of stores/sqlite.rb:
6
6
  #
7
7
  # require "insika/testing/store_contract"
@@ -11,7 +11,7 @@
11
11
  # it_behaves_like "an Insika store"
12
12
  # end
13
13
  #
14
- # Two groups, on purpose (RFC-0018 §4 item 4):
14
+ # Two groups, on purpose:
15
15
  #
16
16
  # - "an Insika store" — universal; every backend passes EXACTLY it (L2: the
17
17
  # suite is honest — a test that passes on Memory passes on SQLite). The
@@ -27,89 +27,89 @@
27
27
  # races) — those belong to the backend's own spec.
28
28
  RSpec.shared_examples "an Insika store" do
29
29
  describe "#get / #set (round-trip)" do
30
- it "C1 preserves Hash with string keys" do # C1
30
+ it " preserves Hash with string keys" do #
31
31
  store.set("s", "k", { "a" => 1, "b" => [1, 2] })
32
32
  expect(store.get("s", "k")).to eq({ "a" => 1, "b" => [1, 2] })
33
33
  end
34
34
 
35
- it "C2 preserves Array" do # C2
35
+ it " preserves Array" do #
36
36
  store.set("s", "k", [1, "x", true, nil])
37
37
  expect(store.get("s", "k")).to eq([1, "x", true, nil])
38
38
  end
39
39
 
40
- it "C3 preserves String" do # C3
40
+ it " preserves String" do #
41
41
  store.set("s", "k", "texto")
42
42
  expect(store.get("s", "k")).to eq("texto")
43
43
  end
44
44
 
45
- it "C4 preserves Integer" do # C4
45
+ it " preserves Integer" do #
46
46
  store.set("s", "k", 42)
47
47
  value = store.get("s", "k")
48
48
  expect(value).to eq(42)
49
49
  expect(value).to be_a(Integer)
50
50
  end
51
51
 
52
- it "C5 preserves Float" do # C5
52
+ it " preserves Float" do #
53
53
  store.set("s", "k", 3.14)
54
54
  value = store.get("s", "k")
55
55
  expect(value).to eq(3.14)
56
56
  expect(value).to be_a(Float)
57
57
  end
58
58
 
59
- it "C6 preserves booleans" do # C6
59
+ it " preserves booleans" do #
60
60
  store.set("s", "t", true)
61
61
  store.set("s", "f", false)
62
62
  expect(store.get("s", "t")).to be(true)
63
63
  expect(store.get("s", "f")).to be(false)
64
64
  end
65
65
 
66
- it "C7 preserves nil written without exception" do # C7
66
+ it " preserves nil written without exception" do #
67
67
  store.set("s", "k", nil)
68
68
  expect(store.get("s", "k")).to be_nil
69
69
  end
70
70
 
71
- it "C8 converts Symbols (keys and values) to Strings" do # C8
72
- store.set("s", "k", { chave: :valor })
73
- expect(store.get("s", "k")).to eq({ "chave" => "valor" })
71
+ it " converts Symbols (keys and values) to Strings" do #
72
+ store.set("s", "k", { key: :value })
73
+ expect(store.get("s", "k")).to eq({ "key" => "value" })
74
74
  end
75
75
 
76
- it "C9 returns nil for an absent key, never an exception" do # C9
77
- expect(store.get("s", "nao-existe")).to be_nil
76
+ it " returns nil for an absent key, never an exception" do #
77
+ expect(store.get("s", "missing-key")).to be_nil
78
78
  end
79
79
 
80
- it "C10 overwrites silently (last-write-wins)" do # C10
81
- store.set("s", "k", "primeiro")
82
- store.set("s", "k", "segundo")
83
- expect(store.get("s", "k")).to eq("segundo")
80
+ it " overwrites silently (last-write-wins)" do #
81
+ store.set("s", "k", "first")
82
+ store.set("s", "k", "second")
83
+ expect(store.get("s", "k")).to eq("second")
84
84
  end
85
85
 
86
- it "C11 set returns the same object passed in (not the round-trip)" do # C11
86
+ it " set returns the same object passed in (not the round-trip)" do #
87
87
  obj = { "a" => 1 }
88
88
  expect(store.set("s", "k", obj)).to equal(obj)
89
89
  end
90
90
  end
91
91
 
92
92
  describe "#delete" do
93
- it "C12 removes existing and returns true" do # C12
93
+ it " removes existing and returns true" do #
94
94
  store.set("s", "k", 1)
95
95
  expect(store.delete("s", "k")).to be(true)
96
96
  expect(store.get("s", "k")).to be_nil
97
97
  end
98
98
 
99
- it "C13 returns false for a nonexistent key" do # C13
99
+ it " returns false for a nonexistent key" do #
100
100
  expect(store.delete("s", "k")).to be(false)
101
101
  end
102
102
  end
103
103
 
104
104
  describe "#list" do
105
- it "C14 returns scope keys sorted lexicographically" do # C14
105
+ it " returns scope keys sorted lexicographically" do #
106
106
  store.set("s", "b", 1)
107
107
  store.set("s", "a", 1)
108
108
  store.set("s", "c", 1)
109
109
  expect(store.list("s")).to eq(%w[a b c])
110
110
  end
111
111
 
112
- it "C15 filters by prefix with start_with? (not include?)" do # C15
112
+ it " filters by prefix with start_with? (not include?)" do #
113
113
  store.set("s", "task:1", 1)
114
114
  store.set("s", "task:2", 1)
115
115
  store.set("s", "checkpoint:1", 1)
@@ -117,7 +117,7 @@ RSpec.shared_examples "an Insika store" do
117
117
  expect(store.list("s", "task:")).to eq(%w[task:1 task:2])
118
118
  end
119
119
 
120
- it "C16 returns [] for an empty scope" do # C16
120
+ it " returns [] for an empty scope" do #
121
121
  expect(store.list("s")).to eq([])
122
122
  end
123
123
 
@@ -128,8 +128,29 @@ RSpec.shared_examples "an Insika store" do
128
128
  end
129
129
  end
130
130
 
131
+ describe "#scopes" do
132
+ it " returns scope names sorted lexicographically" do #
133
+ store.set("zeta", "k", 1)
134
+ store.set("alpha", "k", 1)
135
+ store.set("alpha:child", "k", 1)
136
+ expect(store.scopes).to eq(["alpha", "alpha:child", "zeta"])
137
+ end
138
+
139
+ it " filters by prefix with start_with? (not include?)" do #
140
+ store.set("s", "k", 1)
141
+ store.set("s:child", "k", 1)
142
+ store.set("s2", "k", 1) # trap: contains "s" but does not start with "s:"
143
+ expect(store.scopes("s:")).to eq(["s:child"])
144
+ end
145
+
146
+ it " returns [] when nothing matches (or the store is empty)" do #
147
+ expect(store.scopes("nope")).to eq([])
148
+ expect(store.scopes).to eq([])
149
+ end
150
+ end
151
+
131
152
  describe "scope isolation" do
132
- it "C17 keeps scopes independent in get/list/delete" do # C17
153
+ it " keeps scopes independent in get/list/delete" do #
133
154
  store.set("s1", "k", 1)
134
155
  store.set("s2", "k", 2)
135
156
 
@@ -143,16 +164,16 @@ RSpec.shared_examples "an Insika store" do
143
164
  end
144
165
 
145
166
  describe "#transaction" do
146
- it "C18 returns the block's value" do # C18
167
+ it " returns the block's value" do #
147
168
  expect(store.transaction { 42 }).to eq(42)
148
169
  end
149
170
 
150
- it "C19 commits the block's writes" do # C19
171
+ it " commits the block's writes" do #
151
172
  store.transaction { store.set("s", "k", "commitado") }
152
173
  expect(store.get("s", "k")).to eq("commitado")
153
174
  end
154
175
 
155
- it "C20 does a real rollback of set AND delete when the block raises" do # C20
176
+ it " does a real rollback of set AND delete when the block raises" do #
156
177
  store.set("s", "manter", "antigo")
157
178
  store.set("s", "apagar", "existe")
158
179
 
@@ -169,7 +190,7 @@ RSpec.shared_examples "an Insika store" do
169
190
  expect(store.get("s", "apagar")).to eq("existe")
170
191
  end
171
192
 
172
- it "C21 reuses the outer transaction when nested" do # C21
193
+ it " reuses the outer transaction when nested" do #
173
194
  store.set("s", "k", "antigo")
174
195
 
175
196
  expect do
@@ -185,7 +206,7 @@ RSpec.shared_examples "an Insika store" do
185
206
  end
186
207
 
187
208
  describe "serialization errors" do
188
- it "C22 raises StoreError and does not write a non-serializable value" do # C22
209
+ it " raises StoreError and does not write a non-serializable value" do #
189
210
  expect do
190
211
  store.set("s", "k", Object.new)
191
212
  end.to raise_error(Insika::StoreError)
@@ -194,7 +215,7 @@ RSpec.shared_examples "an Insika store" do
194
215
  end
195
216
  end
196
217
 
197
- # Multi-worker safety (RFC-0018 A3). Every RFC-0016 A1 claim (outbox, delegation
218
+ # Multi-worker safety. Every claim (outbox, delegation
198
219
  # sweep, recovery) is a read-check-write inside `transaction` — on SQLite that is
199
220
  # atomic because the backend opens BEGIN IMMEDIATE; a backend whose transaction
200
221
  # only yields passes all 22 cases above and still double-claims under two
@@ -204,7 +225,7 @@ end
204
225
  # The `sleep` inside each transaction is deliberate: it holds the read state
205
226
  # open long enough that a backend without isolation ALWAYS lets a second
206
227
  # connection through, while a correct backend serializes the writers. Without
207
- # it a wrong backend could pass by scheduling luck — the failure E2 relies on.
228
+ # it a wrong backend could pass by scheduling luck — the failure relies on.
208
229
  RSpec.shared_examples "an Insika store safe for N workers" do
209
230
  # Starts N threads, each holding its OWN connection to the same backend,
210
231
  # releases them together, and returns each block's value. Connections are