thinkstack-core 4.1.0__tar.gz → 4.2.0__tar.gz

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 (336) hide show
  1. {thinkstack_core-4.1.0/thinkstack_core.egg-info → thinkstack_core-4.2.0}/PKG-INFO +22 -3
  2. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/README.md +20 -2
  3. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/pyproject.toml +2 -2
  4. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_hidden_profile_benchmark.py +56 -0
  5. thinkstack_core-4.2.0/tests/test_hooks_learning_capture.py +263 -0
  6. thinkstack_core-4.2.0/tests/test_learning_capture_config.py +19 -0
  7. thinkstack_core-4.2.0/tests/test_learning_stats_counts.py +103 -0
  8. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_mcp_client.py +51 -0
  9. thinkstack_core-4.2.0/tests/test_no_stale_branding.py +106 -0
  10. thinkstack_core-4.2.0/tests/test_pii_redaction.py +56 -0
  11. thinkstack_core-4.2.0/tests/test_regression_learning_extraction.py +145 -0
  12. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_wrapper_capture.py +30 -0
  13. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_sync_bundle.py +66 -0
  14. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/__init__.py +6 -0
  15. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/__init__.py +5 -0
  16. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/base.py +6 -0
  17. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/config.py +7 -0
  18. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/dispatcher.py +6 -0
  19. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/jira.py +2 -0
  20. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/linear.py +2 -0
  21. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/pagerduty.py +2 -0
  22. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/slack.py +2 -0
  23. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/teams.py +2 -0
  24. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/broadcast/__init__.py +6 -0
  25. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/mcp_client.py +31 -5
  26. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync_bundle.py +32 -9
  27. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/thinkstack-mcp-bridge.js +52 -3
  28. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/__init__.py +2 -0
  29. thinkstack_core-4.2.0/thinkstack_core/consolidation/__init__.py +9 -0
  30. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/consolidation/workflow.py +6 -0
  31. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/dashboard_api.py +357 -1
  32. thinkstack_core-4.2.0/thinkstack_core/divergence/__init__.py +9 -0
  33. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/divergence/detector.py +6 -0
  34. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/server.py +23 -2
  35. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gcc.py +11 -0
  36. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/pat.py +5 -0
  37. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hitl/__init__.py +2 -0
  38. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hitl/channels.py +6 -0
  39. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hitl/orchestrator.py +5 -0
  40. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/runner.py +97 -11
  41. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/identity/__init__.py +6 -0
  42. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/mcp/auth.py +6 -0
  43. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/__init__.py +2 -0
  44. thinkstack_core-4.2.0/thinkstack_core/proxy/routes/__init__.py +1 -0
  45. thinkstack_core-4.2.0/thinkstack_core/proxy/routes/_shared.py +45 -0
  46. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/anthropic.py +29 -3
  47. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/azure_openai.py +29 -3
  48. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/gemini.py +29 -3
  49. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/groq.py +29 -3
  50. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/ollama.py +29 -3
  51. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/openai.py +29 -3
  52. thinkstack_core-4.2.0/thinkstack_core/reasoning/__init__.py +10 -0
  53. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning/entry.py +2 -0
  54. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning/store.py +5 -0
  55. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/config.py +6 -0
  56. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/pii.py +51 -1
  57. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/models.py +4 -0
  58. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/storage.py +34 -7
  59. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/base.py +4 -1
  60. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0/thinkstack_core.egg-info}/PKG-INFO +22 -3
  61. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/SOURCES.txt +7 -0
  62. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/requires.txt +1 -0
  63. thinkstack_core-4.1.0/thinkstack_core/consolidation/__init__.py +0 -3
  64. thinkstack_core-4.1.0/thinkstack_core/divergence/__init__.py +0 -3
  65. thinkstack_core-4.1.0/thinkstack_core/proxy/routes/__init__.py +0 -1
  66. thinkstack_core-4.1.0/thinkstack_core/reasoning/__init__.py +0 -4
  67. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/MANIFEST.in +0 -0
  68. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/setup.cfg +0 -0
  69. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_backend_factory.py +0 -0
  70. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_broadcast.py +0 -0
  71. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_claude_config_locations.py +0 -0
  72. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cli_sync_all.py +0 -0
  73. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cli_topics_concepts.py +0 -0
  74. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_auth.py +0 -0
  75. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_bridge.py +0 -0
  76. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_integration.py +0 -0
  77. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_sse.py +0 -0
  78. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_storage.py +0 -0
  79. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_sync.py +0 -0
  80. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_concept_catalog.py +0 -0
  81. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_doctor_ci.py +0 -0
  82. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_getting_started.py +0 -0
  83. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_github_app.py +0 -0
  84. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_github_pat.py +0 -0
  85. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_hook_entrypoint.py +0 -0
  86. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_rep_merge.py +0 -0
  87. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s0_gcc.py +0 -0
  88. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s10_jetbrains.py +0 -0
  89. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s10_vscode.py +0 -0
  90. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s11_codex.py +0 -0
  91. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s11_hooks_installer.py +0 -0
  92. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s12_github.py +0 -0
  93. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s12_gitlab.py +0 -0
  94. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s13_dashboard_api.py +0 -0
  95. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s13_forensic.py +0 -0
  96. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s14_audit_export.py +0 -0
  97. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s14_ciso_api.py +0 -0
  98. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s15_rep_network.py +0 -0
  99. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s16_alerts.py +0 -0
  100. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s17a_metrics.py +0 -0
  101. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s17b_observability.py +0 -0
  102. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s17b_sso.py +0 -0
  103. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s18_context_extraction.py +0 -0
  104. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s18_credibility.py +0 -0
  105. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s18_identity.py +0 -0
  106. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s19_learning_evolution.py +0 -0
  107. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s19_projects.py +0 -0
  108. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s19_reasoning_query.py +0 -0
  109. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s1_branch_merge.py +0 -0
  110. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_divergence.py +0 -0
  111. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_metrics.py +0 -0
  112. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_pii_guardrails.py +0 -0
  113. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_token_budget.py +0 -0
  114. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s21_cross_project.py +0 -0
  115. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s21_hitl.py +0 -0
  116. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s22_swe_bench.py +0 -0
  117. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s22_swe_bench_dry_run.py +0 -0
  118. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s22_sync.py +0 -0
  119. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s23_reasoning_plus.py +0 -0
  120. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s23_sync_remaining.py +0 -0
  121. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s24_reasoning_plus_learning.py +0 -0
  122. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_delivery_time.py +0 -0
  123. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_serve.py +0 -0
  124. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_setup_detection.py +0 -0
  125. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_templates.py +0 -0
  126. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_daemon.py +0 -0
  127. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_git_hooks.py +0 -0
  128. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_proxy.py +0 -0
  129. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_watcher.py +0 -0
  130. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_cross_project.py +0 -0
  131. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_drift.py +0 -0
  132. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_query.py +0 -0
  133. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_templates.py +0 -0
  134. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_cli.py +0 -0
  135. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_disagreement.py +0 -0
  136. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_planner.py +0 -0
  137. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_roles.py +0 -0
  138. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_session.py +0 -0
  139. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_simulator.py +0 -0
  140. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_ci.py +0 -0
  141. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_cli.py +0 -0
  142. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_dashboard.py +0 -0
  143. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_github.py +0 -0
  144. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s2_context.py +0 -0
  145. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s30_audit.py +0 -0
  146. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s31_cloud.py +0 -0
  147. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s3_sensitivity.py +0 -0
  148. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s4_invariants.py +0 -0
  149. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s4_variance_lock.py +0 -0
  150. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s5_privacy_disclosure.py +0 -0
  151. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s5_rep_sis.py +0 -0
  152. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s6_debug.py +0 -0
  153. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_mcp_hooks.py +0 -0
  154. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_ollama_wrapper.py +0 -0
  155. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_parser.py +0 -0
  156. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_proxy.py +0 -0
  157. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_wrappers.py +0 -0
  158. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7ent_gateway.py +0 -0
  159. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7ent_helm.py +0 -0
  160. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s8_aggphi.py +0 -0
  161. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s8_theta_synthesis.py +0 -0
  162. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s9_gaps.py +0 -0
  163. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_setup_command.py +0 -0
  164. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_setup_targets.py +0 -0
  165. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_signing.py +0 -0
  166. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_storage_azure.py +0 -0
  167. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_sync_conflicts.py +0 -0
  168. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_sync_state.py +0 -0
  169. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_integration.py +0 -0
  170. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_io.py +0 -0
  171. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_serialize.py +0 -0
  172. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_token.py +0 -0
  173. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_topics.py +0 -0
  174. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/aggphi_textual.py +0 -0
  175. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/__init__.py +0 -0
  176. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/exporter.py +0 -0
  177. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/privacy.py +0 -0
  178. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/scrubber.py +0 -0
  179. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/service.py +0 -0
  180. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/signing.py +0 -0
  181. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/broadcast/broadcaster.py +0 -0
  182. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/broadcast/watcher.py +0 -0
  183. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/capability.py +0 -0
  184. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/__init__.py +0 -0
  185. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_config.py +0 -0
  186. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +0 -0
  187. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.claude-stdio.json +0 -0
  188. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.cursor-mcp.json +0 -0
  189. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.opencode-bridge.json +0 -0
  190. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.opencode.json +0 -0
  191. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.vscode-mcp.json +0 -0
  192. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/setup.py +0 -0
  193. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync.py +0 -0
  194. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync_conflicts.py +0 -0
  195. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync_state.py +0 -0
  196. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/team_sync.py +0 -0
  197. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/__main__.py +0 -0
  198. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/capture.py +0 -0
  199. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/proxy.py +0 -0
  200. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/compat.py +0 -0
  201. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/concept_catalog.py +0 -0
  202. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/consolidation/synthesizer.py +0 -0
  203. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/daemon/__init__.py +0 -0
  204. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/daemon/supervisor.py +0 -0
  205. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/daemon/watcher.py +0 -0
  206. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/deltaf.py +0 -0
  207. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/disclosure.py +0 -0
  208. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/__init__.py +0 -0
  209. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/key_manager.py +0 -0
  210. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/metrics_webhook.py +0 -0
  211. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/policy.py +0 -0
  212. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/sso.py +0 -0
  213. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/__init__.py +0 -0
  214. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/app.py +0 -0
  215. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/comment_builder.py +0 -0
  216. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/pr_parser.py +0 -0
  217. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/pr_reporter.py +0 -0
  218. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gitlab/__init__.py +0 -0
  219. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/__init__.py +0 -0
  220. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/claude_code.py +0 -0
  221. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/git_capture.py +0 -0
  222. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/git_commit.py +0 -0
  223. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/installer.py +0 -0
  224. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/pre_commit.py +0 -0
  225. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/identity/agent.py +0 -0
  226. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/identity/providers.py +0 -0
  227. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/invariants.py +0 -0
  228. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/mcp/__init__.py +0 -0
  229. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/mcp/server.py +0 -0
  230. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/__init__.py +0 -0
  231. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/aggregate.py +0 -0
  232. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/calibrate.py +0 -0
  233. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/calibration.py +0 -0
  234. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/credibility.py +0 -0
  235. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/delivery_time.py +0 -0
  236. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/dhs.py +0 -0
  237. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/mcs.py +0 -0
  238. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/roi.py +0 -0
  239. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/session_writer.py +0 -0
  240. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/shadow_ai.py +0 -0
  241. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/sprint_writer.py +0 -0
  242. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/__init__.py +0 -0
  243. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/datadog.py +0 -0
  244. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/formatter.py +0 -0
  245. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/report.py +0 -0
  246. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/servicenow.py +0 -0
  247. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/splunk.py +0 -0
  248. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/webhook.py +0 -0
  249. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/__init__.py +0 -0
  250. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/blocks.py +0 -0
  251. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/inference.py +0 -0
  252. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/thinking.py +0 -0
  253. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/projects.py +0 -0
  254. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/prompt_artifact.py +0 -0
  255. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/server.py +0 -0
  256. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/__init__.py +0 -0
  257. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/grep.py +0 -0
  258. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/hybrid.py +0 -0
  259. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/semantic.py +0 -0
  260. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rdp.py +0 -0
  261. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/__init__.py +0 -0
  262. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/augmenter.py +0 -0
  263. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/capture.py +0 -0
  264. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/context.py +0 -0
  265. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/__init__.py +0 -0
  266. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/analytics.py +0 -0
  267. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/api.py +0 -0
  268. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/chain.py +0 -0
  269. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/composer.py +0 -0
  270. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/conflicts.py +0 -0
  271. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/context_collector.py +0 -0
  272. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/cross_project.py +0 -0
  273. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/deny_list.py +0 -0
  274. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/embeddings.py +0 -0
  275. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/evolution.py +0 -0
  276. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/extractor.py +0 -0
  277. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/filter_five_layer.py +0 -0
  278. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/models.py +0 -0
  279. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/org_store.py +0 -0
  280. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/promotion.py +0 -0
  281. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/provenance.py +0 -0
  282. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/recorder.py +0 -0
  283. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/relevance.py +0 -0
  284. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/state.py +0 -0
  285. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/store.py +0 -0
  286. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +0 -0
  287. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/prompt.py +0 -0
  288. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep.py +0 -0
  289. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/__init__.py +0 -0
  290. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/merge.py +0 -0
  291. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/node.py +0 -0
  292. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/server.py +0 -0
  293. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/sync.py +0 -0
  294. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/sensitivity.py +0 -0
  295. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/__init__.py +0 -0
  296. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/io.py +0 -0
  297. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/reporting.py +0 -0
  298. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/token_counter.py +0 -0
  299. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/toon.py +0 -0
  300. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serve.py +0 -0
  301. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/__init__.py +0 -0
  302. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/disagreement.py +0 -0
  303. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/orchestrator.py +0 -0
  304. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/planner.py +0 -0
  305. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/simulator.py +0 -0
  306. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/signing.py +0 -0
  307. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/sis.py +0 -0
  308. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/skills/pr-reviewer/SKILL.md +0 -0
  309. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +0 -0
  310. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/skills/thinkstack-session-start/SKILL.md +0 -0
  311. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/__init__.py +0 -0
  312. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/engine.py +0 -0
  313. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/go.py +0 -0
  314. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/infra.py +0 -0
  315. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/__init__.py +0 -0
  316. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/api_design.md +0 -0
  317. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/bug_fix.md +0 -0
  318. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/decision_record.md +0 -0
  319. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/engine.py +0 -0
  320. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/security_review.md +0 -0
  321. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/python.py +0 -0
  322. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/react.py +0 -0
  323. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/typescript.py +0 -0
  324. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/theta.py +0 -0
  325. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/theta_synthesis.py +0 -0
  326. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/topics.py +0 -0
  327. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/variance.py +0 -0
  328. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/__init__.py +0 -0
  329. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/anthropic.py +0 -0
  330. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/bedrock.py +0 -0
  331. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/gemini.py +0 -0
  332. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/ollama.py +0 -0
  333. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/openai.py +0 -0
  334. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/dependency_links.txt +0 -0
  335. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/entry_points.txt +0 -0
  336. {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thinkstack-core
3
- Version: 4.1.0
3
+ Version: 4.2.0
4
4
  Summary: ThinkStack — AI reasoning capture, audit trail, and governance for agent-assisted development (core library)
5
5
  Author-email: Hemant Joshi <hemant@flotorch.ai>
6
6
  License: Apache-2.0
@@ -21,6 +21,7 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: pytest>=8.0.0; extra == "dev"
24
+ Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
24
25
  Requires-Dist: boto3>=1.34.0; extra == "dev"
25
26
  Requires-Dist: pyyaml>=6.0; extra == "dev"
26
27
  Requires-Dist: pdfminer.six>=20221105; extra == "dev"
@@ -61,10 +62,10 @@ Requires-Dist: thinkstack-core[audit,cloud,embeddings,mcp,proxy,wrapper]; extra
61
62
 
62
63
  **Local-first AI agent governance.** ThinkStack gives multi-agent systems a shared on-disk audit trail, coordination vector, privacy controls, and LLM integration layer — all without a remote service.
63
64
 
64
- > **Latest release:** `thinkstack-core` and `thinkstack-cli` **4.0.0** — Multi-model reasoning parsers + TOON default serialization + RACP spec paper. Install or upgrade with `pip install -U thinkstack-cli`.
65
+ > **Latest release:** `thinkstack-core` and `thinkstack-cli` **4.2.0** — learning capture on the Claude Code path, redacted call records, JSON sync bundles, a stateless MCP transport, and honest metrics. Install or upgrade with `pip install -U thinkstack-cli`.
65
66
 
66
67
  ```bash
67
- pip install -U thinkstack-cli>=4.0.0
68
+ pip install -U 'thinkstack-cli>=4.2.0'
68
69
  thinkstack init
69
70
  thinkstack setup # connects to Claude Code, Cursor, or Antigravity
70
71
  thinkstack debug timeline
@@ -98,6 +99,21 @@ thinkstack reasoning-plus config --smart-top-n 3 --smart-max-lines 150 --learnin
98
99
 
99
100
  Set `THINKSTACK_REASONING_PLUS=0` to disable it for a single session. See [REASONING_PLUS.md](REASONING_PLUS.md) for details.
100
101
 
102
+ #### Learning capture
103
+
104
+ Every captured call is recorded under `.GCC/reasoning_learnings/` and a learning is extracted from it — what was decided, and why. Secrets and structured PII are redacted *before* anything is written, so a credential or an email in a model response never reaches the store.
105
+
106
+ In Claude Code the final assistant message is captured on the `Stop` hook (Claude Code v2.1.47+), so a session that only reads and reasons still leaves a learning behind.
107
+
108
+ ```bash
109
+ thinkstack learning stats # totals, average confidence, danger zones
110
+ thinkstack learning list # active learnings, filterable by type or scope
111
+ thinkstack learning show <id> # one learning in full
112
+ thinkstack learning promote <id> # share it with your org
113
+ ```
114
+
115
+ "Danger zones" are concepts whose recorded outcomes disagree — the places where re-injecting past reasoning is most likely to mislead. The directory holds `calls/` (what was recorded), `learnings/` (what was extracted), and `provenance/` (which call produced which learning).
116
+
101
117
  Everything lives in `.GCC/` — a local directory in your project. No database, no network service, no telemetry.
102
118
 
103
119
  ---
@@ -547,6 +563,8 @@ No API key required. Fully local — RACP context and captures stay on-device.
547
563
 
548
564
  For teams that want a single shared reasoning backend across all projects and machines, ThinkStack provides a Cloudflare Worker MCP server. It stores `.GCC/` in Cloudflare R2 and exposes an HTTPS SSE endpoint that every IDE can connect to.
549
565
 
566
+ The transport is **stateless**: `GET /{org}/{repo}/sse` answers the MCP handshake and closes, and `POST /{org}/{repo}/messages` returns the JSON-RPC response in the response body. Nothing is held server-side between calls, so the worker runs comfortably inside Cloudflare's free tier. All routes except `/health` require an API key or OAuth/JWT.
567
+
550
568
  **Deploy once, use everywhere:**
551
569
 
552
570
  ```bash
@@ -859,6 +877,7 @@ ThinkStack is explicit about what every number is based on:
859
877
  - **Auditor rate** — BLS SOC 13-2011 (2023): $39–40/hr employed staff. External rates ($75–400/hr) not used as defaults.
860
878
  - **MCS/DHS weights** — Design choices. No external benchmark. Calibrate with your team's data.
861
879
  - **Developer hourly rate** — No default. You must set it: `thinkstack calibrate --set developer_hourly_rate=<value>`
880
+ - **A metric that is not measured is reported as 0, not estimated.** `bundle_tokens` and `latency_speedup_factor`, for instance, stay at 0 until something actually measures a bundle. A plausible-looking derived number is worse than an obvious zero: it reads as evidence, and it survives review. The same principle applies to the learning statistics — an empty store reports zero rather than an inferred figure.
862
881
 
863
882
  See `thinkstack_core/metrics/credibility.py` for the full citation registry.
864
883
 
@@ -2,10 +2,10 @@
2
2
 
3
3
  **Local-first AI agent governance.** ThinkStack gives multi-agent systems a shared on-disk audit trail, coordination vector, privacy controls, and LLM integration layer — all without a remote service.
4
4
 
5
- > **Latest release:** `thinkstack-core` and `thinkstack-cli` **4.0.0** — Multi-model reasoning parsers + TOON default serialization + RACP spec paper. Install or upgrade with `pip install -U thinkstack-cli`.
5
+ > **Latest release:** `thinkstack-core` and `thinkstack-cli` **4.2.0** — learning capture on the Claude Code path, redacted call records, JSON sync bundles, a stateless MCP transport, and honest metrics. Install or upgrade with `pip install -U thinkstack-cli`.
6
6
 
7
7
  ```bash
8
- pip install -U thinkstack-cli>=4.0.0
8
+ pip install -U 'thinkstack-cli>=4.2.0'
9
9
  thinkstack init
10
10
  thinkstack setup # connects to Claude Code, Cursor, or Antigravity
11
11
  thinkstack debug timeline
@@ -39,6 +39,21 @@ thinkstack reasoning-plus config --smart-top-n 3 --smart-max-lines 150 --learnin
39
39
 
40
40
  Set `THINKSTACK_REASONING_PLUS=0` to disable it for a single session. See [REASONING_PLUS.md](REASONING_PLUS.md) for details.
41
41
 
42
+ #### Learning capture
43
+
44
+ Every captured call is recorded under `.GCC/reasoning_learnings/` and a learning is extracted from it — what was decided, and why. Secrets and structured PII are redacted *before* anything is written, so a credential or an email in a model response never reaches the store.
45
+
46
+ In Claude Code the final assistant message is captured on the `Stop` hook (Claude Code v2.1.47+), so a session that only reads and reasons still leaves a learning behind.
47
+
48
+ ```bash
49
+ thinkstack learning stats # totals, average confidence, danger zones
50
+ thinkstack learning list # active learnings, filterable by type or scope
51
+ thinkstack learning show <id> # one learning in full
52
+ thinkstack learning promote <id> # share it with your org
53
+ ```
54
+
55
+ "Danger zones" are concepts whose recorded outcomes disagree — the places where re-injecting past reasoning is most likely to mislead. The directory holds `calls/` (what was recorded), `learnings/` (what was extracted), and `provenance/` (which call produced which learning).
56
+
42
57
  Everything lives in `.GCC/` — a local directory in your project. No database, no network service, no telemetry.
43
58
 
44
59
  ---
@@ -488,6 +503,8 @@ No API key required. Fully local — RACP context and captures stay on-device.
488
503
 
489
504
  For teams that want a single shared reasoning backend across all projects and machines, ThinkStack provides a Cloudflare Worker MCP server. It stores `.GCC/` in Cloudflare R2 and exposes an HTTPS SSE endpoint that every IDE can connect to.
490
505
 
506
+ The transport is **stateless**: `GET /{org}/{repo}/sse` answers the MCP handshake and closes, and `POST /{org}/{repo}/messages` returns the JSON-RPC response in the response body. Nothing is held server-side between calls, so the worker runs comfortably inside Cloudflare's free tier. All routes except `/health` require an API key or OAuth/JWT.
507
+
491
508
  **Deploy once, use everywhere:**
492
509
 
493
510
  ```bash
@@ -800,6 +817,7 @@ ThinkStack is explicit about what every number is based on:
800
817
  - **Auditor rate** — BLS SOC 13-2011 (2023): $39–40/hr employed staff. External rates ($75–400/hr) not used as defaults.
801
818
  - **MCS/DHS weights** — Design choices. No external benchmark. Calibrate with your team's data.
802
819
  - **Developer hourly rate** — No default. You must set it: `thinkstack calibrate --set developer_hourly_rate=<value>`
820
+ - **A metric that is not measured is reported as 0, not estimated.** `bundle_tokens` and `latency_speedup_factor`, for instance, stay at 0 until something actually measures a bundle. A plausible-looking derived number is worse than an obvious zero: it reads as evidence, and it survives review. The same principle applies to the learning statistics — an empty store reports zero rather than an inferred figure.
803
821
 
804
822
  See `thinkstack_core/metrics/credibility.py` for the full citation registry.
805
823
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "thinkstack-core"
3
- version = "4.1.0"
3
+ version = "4.2.0"
4
4
  description = "ThinkStack — AI reasoning capture, audit trail, and governance for agent-assisted development (core library)"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -26,7 +26,7 @@ dependencies = []
26
26
  thinkstack-hook = "thinkstack_core.hooks.runner:cli"
27
27
 
28
28
  [project.optional-dependencies]
29
- dev = ["pytest>=8.0.0", "boto3>=1.34.0", "pyyaml>=6.0", "pdfminer.six>=20221105", "tiktoken>=0.7.0"]
29
+ dev = ["pytest>=8.0.0", "pytest-cov>=7.0.0", "boto3>=1.34.0", "pyyaml>=6.0", "pdfminer.six>=20221105", "tiktoken>=0.7.0"]
30
30
  wrapper = [
31
31
  "anthropic>=0.40.0",
32
32
  "openai>=1.50.0",
@@ -181,3 +181,59 @@ class TestBenchmarkResultsDoc:
181
181
  results_dir = BENCHMARK_DIR / "results" / "thinkstack"
182
182
  assert results_dir.exists()
183
183
  assert any(results_dir.glob("*/*.json"))
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Determinism
188
+ # ---------------------------------------------------------------------------
189
+
190
+
191
+ def _load_run_baseline():
192
+ """Import benchmarks/hidden_profile/run_baseline.py as a module."""
193
+ import importlib.util
194
+
195
+ spec = importlib.util.spec_from_file_location(
196
+ "run_baseline", BENCHMARK_DIR / "run_baseline.py"
197
+ )
198
+ module = importlib.util.module_from_spec(spec)
199
+ spec.loader.exec_module(module)
200
+ return module
201
+
202
+
203
+ class TestDeterminism:
204
+ def test_decision_does_not_depend_on_theta_key_order(self):
205
+ """Benchmark output must not vary with dict/set iteration order.
206
+
207
+ theta reached agent_decide() from a set, so PYTHONHASHSEED reordered
208
+ its keys between processes. Both the rendered "Theta signals read:
209
+ [...]" line and the order of constraints_included varied, so the
210
+ committed results under results/ churned on every regeneration with no
211
+ change in behaviour.
212
+ """
213
+ module = _load_run_baseline()
214
+
215
+ scenario = {
216
+ "required_constraints": ["c2", "c3"],
217
+ "agents": [
218
+ {
219
+ "id": "peer",
220
+ "private_info": [
221
+ {"constraint_id": "c2", "affects": ["alpha"]},
222
+ {"constraint_id": "c3", "affects": ["beta"]},
223
+ ],
224
+ }
225
+ ],
226
+ }
227
+ agent = {
228
+ "id": "decider",
229
+ "role": "dba",
230
+ "private_info": [{"constraint_id": "c1", "description": "d"}],
231
+ }
232
+ theta_ab = {"alpha": {"mean_confidence": 0.9}, "beta": {"mean_confidence": 0.9}}
233
+ theta_ba = {"beta": {"mean_confidence": 0.9}, "alpha": {"mean_confidence": 0.9}}
234
+
235
+ first = module.agent_decide(agent, scenario, shared_theta=theta_ab)
236
+ second = module.agent_decide(agent, scenario, shared_theta=theta_ba)
237
+
238
+ assert first["constraints_included"] == second["constraints_included"]
239
+ assert first["decision_text"] == second["decision_text"]
@@ -0,0 +1,263 @@
1
+ """The Stop hook must record the final response and extract a learning (plan Task 2).
2
+
3
+ Reported symptom: a full session records events and commits but zero learnings,
4
+ because nothing on the Claude Code path ever wrote a ReasoningCall.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from thinkstack_core import GCCRepository
11
+ from thinkstack_core.hooks import runner
12
+ from thinkstack_core.reasoning_plus.config import ReasoningPlusConfig, SettingsStore
13
+ from thinkstack_core.reasoning_plus.learning.store import LearningStore
14
+
15
+ MESSAGE = (
16
+ "Retrieval beat finetuning here because the corpus changed mid-experiment."
17
+ )
18
+
19
+
20
+ def _init_repo(tmp_path: Path) -> Path:
21
+ """A genuinely initialised store — is_initialized() requires VERSION."""
22
+ repo = GCCRepository.at(tmp_path)
23
+ repo.init()
24
+ return repo.gcc_dir
25
+
26
+
27
+ def test_session_end_captures_a_learning(tmp_path, monkeypatch):
28
+ gcc = _init_repo(tmp_path)
29
+ monkeypatch.chdir(tmp_path)
30
+
31
+ runner.handle_session_end({
32
+ "session_id": "s-1",
33
+ "last_assistant_message": MESSAGE,
34
+ })
35
+
36
+ learnings = LearningStore(gcc).list()
37
+ assert len(learnings) == 1
38
+ assert "retrieval" in {c.lower() for c in learnings[0].trigger_concepts}
39
+
40
+
41
+ def test_session_end_without_message_records_no_learning(tmp_path, monkeypatch):
42
+ gcc = _init_repo(tmp_path)
43
+ monkeypatch.chdir(tmp_path)
44
+
45
+ runner.handle_session_end({"session_id": "s-2"})
46
+
47
+ assert LearningStore(gcc).list() == []
48
+
49
+
50
+ def test_session_end_never_raises(tmp_path, monkeypatch):
51
+ gcc = _init_repo(tmp_path)
52
+ monkeypatch.chdir(tmp_path)
53
+
54
+ # A payload that would blow up any naive implementation.
55
+ runner.handle_session_end({
56
+ "last_assistant_message": None,
57
+ "session_id": object(),
58
+ })
59
+
60
+ assert LearningStore(gcc).list() == []
61
+
62
+
63
+ def test_capture_disabled_by_config(tmp_path, monkeypatch):
64
+ gcc = _init_repo(tmp_path)
65
+ monkeypatch.chdir(tmp_path)
66
+ SettingsStore(gcc).save_project(
67
+ ReasoningPlusConfig(learning_capture_on_stop=False)
68
+ )
69
+
70
+ runner.handle_session_end({
71
+ "session_id": "s-3",
72
+ "last_assistant_message": MESSAGE,
73
+ })
74
+
75
+ assert LearningStore(gcc).list() == []
76
+
77
+
78
+ def test_session_end_flushes_pending_extractions(tmp_path, monkeypatch):
79
+ """Queued calls are extracted at session end, not discarded.
80
+
81
+ The docstring promised a final flush of learnings; the body never called
82
+ flush_extractions(), so anything in the batch queue was dropped.
83
+ """
84
+ from unittest.mock import patch
85
+
86
+ from thinkstack_core.reasoning_plus.learning import api as learning_api
87
+
88
+ _init_repo(tmp_path)
89
+ monkeypatch.chdir(tmp_path)
90
+
91
+ called = {"flush": 0}
92
+ real_flush = learning_api.ReasoningPlusLearning.flush_extractions
93
+
94
+ def spy(self):
95
+ called["flush"] += 1
96
+ return real_flush(self)
97
+
98
+ with patch.object(learning_api.ReasoningPlusLearning, "flush_extractions", spy):
99
+ runner.handle_session_end({"session_id": "s-flush"})
100
+
101
+ assert called["flush"] == 1
102
+
103
+
104
+ def test_auto_sync_event_is_honest_without_org_id(tmp_path, monkeypatch):
105
+ """No org configured -> the event must not claim a push happened.
106
+
107
+ The event was appended outside the try with learnings_pushed/pulled
108
+ hard-coded true, so session end logged a successful org push even when
109
+ org_id was None and nothing was sent.
110
+ """
111
+ import json
112
+
113
+ gcc = _init_repo(tmp_path)
114
+ monkeypatch.chdir(tmp_path)
115
+ monkeypatch.delenv("THINKSTACK_ORG_ID", raising=False)
116
+ monkeypatch.delenv("DEVTORCH_ORG_ID", raising=False)
117
+
118
+ runner.handle_session_end({"session_id": "s-org"})
119
+
120
+ events = [
121
+ json.loads(line)
122
+ for line in (gcc / "events.log.jsonl").read_text().splitlines()
123
+ if line.strip()
124
+ ]
125
+ sync_events = [e for e in events if e["event_type"] == "AUTO_SYNC_ON_STOP"]
126
+ assert len(sync_events) == 1
127
+ payload = sync_events[0]["payload"]
128
+ assert payload["org_id"] is None
129
+ assert payload["learnings_pushed"] is False
130
+ assert payload["learnings_pulled"] is False
131
+
132
+
133
+ def test_capture_ignores_non_string_message(tmp_path, monkeypatch):
134
+ gcc = _init_repo(tmp_path)
135
+ monkeypatch.chdir(tmp_path)
136
+
137
+ runner.handle_session_end({"session_id": "s-ns", "last_assistant_message": 12345})
138
+
139
+ assert LearningStore(gcc).list() == []
140
+
141
+
142
+ def test_capture_ignores_whitespace_only_message(tmp_path, monkeypatch):
143
+ gcc = _init_repo(tmp_path)
144
+ monkeypatch.chdir(tmp_path)
145
+
146
+ runner.handle_session_end({
147
+ "session_id": "s-ws",
148
+ "last_assistant_message": " \n ",
149
+ })
150
+
151
+ assert LearningStore(gcc).list() == []
152
+
153
+
154
+ def test_capture_truncates_a_very_long_message(tmp_path, monkeypatch):
155
+ """Review Focus: an unbounded response must not produce an unbounded write."""
156
+ gcc = _init_repo(tmp_path)
157
+ monkeypatch.chdir(tmp_path)
158
+
159
+ runner.handle_session_end({
160
+ "session_id": "s-long",
161
+ "last_assistant_message": "retrieval corpus changed " * 5000,
162
+ })
163
+
164
+ calls = list((gcc / "reasoning_learnings" / "calls").glob("*"))
165
+ assert calls, "capture should still produce a call record"
166
+ assert len(calls[0].read_text()) < 200_000
167
+
168
+
169
+ def test_capture_survives_a_corrupt_settings_file(tmp_path, monkeypatch):
170
+ """Hooks must exit 0 even when settings are unreadable.
171
+
172
+ Note the file is settings.toon, not settings.json — PROJECT_SETTINGS_FILENAME
173
+ is settings.toon, so writing settings.json here would be silently ignored.
174
+ A corrupt file makes the loader fall back to defaults, so capture still runs.
175
+ """
176
+ import json
177
+
178
+ gcc = _init_repo(tmp_path)
179
+ monkeypatch.chdir(tmp_path)
180
+ (gcc / "settings.toon").write_text("{ this is not valid toon or json")
181
+
182
+ runner.handle_session_end({
183
+ "session_id": "s-bad",
184
+ "last_assistant_message": MESSAGE,
185
+ })
186
+
187
+ logged = [
188
+ json.loads(line)
189
+ for line in (gcc / "events.log.jsonl").read_text().splitlines()
190
+ if line.strip()
191
+ ]
192
+ assert any(e["event_type"] == "SESSION_END" for e in logged), "handler must complete"
193
+
194
+
195
+ def test_capture_disabled_when_learning_disabled(tmp_path, monkeypatch):
196
+ """learning_enabled=False is a hard gate at the hook level too."""
197
+ gcc = _init_repo(tmp_path)
198
+ monkeypatch.chdir(tmp_path)
199
+ SettingsStore(gcc).save_project(ReasoningPlusConfig(learning_enabled=False))
200
+
201
+ runner.handle_session_end({
202
+ "session_id": "s-off",
203
+ "last_assistant_message": MESSAGE,
204
+ })
205
+
206
+ assert LearningStore(gcc).list() == []
207
+
208
+
209
+ def test_auto_sync_reports_success_when_org_configured(tmp_path, monkeypatch):
210
+ """With an org set, the event reports the push and pull as done."""
211
+ import json
212
+
213
+ import thinkstack_core.cloud.team_sync as team_sync
214
+
215
+ gcc = _init_repo(tmp_path)
216
+ monkeypatch.chdir(tmp_path)
217
+ monkeypatch.setenv("THINKSTACK_ORG_ID", "test-org")
218
+
219
+ calls = []
220
+ monkeypatch.setattr(team_sync, "push_learnings", lambda org, d: calls.append(("push", org)))
221
+ monkeypatch.setattr(team_sync, "pull_learnings", lambda org, d: calls.append(("pull", org)))
222
+
223
+ runner.handle_session_end({"session_id": "s-org2"})
224
+
225
+ events = [
226
+ json.loads(line)
227
+ for line in (gcc / "events.log.jsonl").read_text().splitlines()
228
+ if line.strip()
229
+ ]
230
+ payloads = [e["payload"] for e in events if e["event_type"] == "AUTO_SYNC_ON_STOP"]
231
+ assert payloads[0]["org_id"] == "test-org"
232
+ assert payloads[0]["learnings_pushed"] is True
233
+ assert payloads[0]["learnings_pulled"] is True
234
+ assert ("push", "test-org") in calls
235
+ assert ("pull", "test-org") in calls
236
+
237
+
238
+ def test_capture_does_not_persist_credentials(tmp_path, monkeypatch):
239
+ """Review Focus: a response containing a secret or PII must not land raw.
240
+
241
+ The extractor quarantines the resulting learning, but the call record stores
242
+ the response text verbatim — so both the credential and the email survived
243
+ in reasoning_learnings/calls/*.toon.
244
+ """
245
+ gcc = _init_repo(tmp_path)
246
+ monkeypatch.chdir(tmp_path)
247
+
248
+ runner.handle_session_end({
249
+ "session_id": "s-secret",
250
+ "last_assistant_message": (
251
+ "We fixed the deploy. The leaked key is AKIAIOSFODNN7EXAMPLE "
252
+ "and the owner is jane.doe@example.com — rotate it."
253
+ ),
254
+ })
255
+
256
+ leaked = []
257
+ for path in (gcc / "reasoning_learnings").rglob("*"):
258
+ if not path.is_file():
259
+ continue
260
+ body = path.read_text(errors="replace")
261
+ if "AKIAIOSFODNN7EXAMPLE" in body or "jane.doe@example.com" in body:
262
+ leaked.append(path.name)
263
+ assert leaked == [], f"secret/PII persisted in {leaked}"
@@ -0,0 +1,19 @@
1
+ """Config for the Stop-hook learning capture path (plan Task 1)."""
2
+ from __future__ import annotations
3
+
4
+ from thinkstack_core.reasoning_plus.config import ReasoningPlusConfig
5
+
6
+
7
+ def test_capture_on_stop_defaults_true():
8
+ assert ReasoningPlusConfig().learning_capture_on_stop is True
9
+
10
+
11
+ def test_capture_on_stop_survives_dict_round_trip():
12
+ cfg = ReasoningPlusConfig(learning_capture_on_stop=False)
13
+ assert cfg.to_dict()["learning_capture_on_stop"] is False
14
+ assert ReasoningPlusConfig.from_dict(cfg.to_dict()).learning_capture_on_stop is False
15
+
16
+
17
+ def test_capture_on_stop_survives_with_values():
18
+ cfg = ReasoningPlusConfig().with_values(learning_capture_on_stop=False)
19
+ assert cfg.learning_capture_on_stop is False
@@ -0,0 +1,103 @@
1
+ """learning stats must report learning count, not concept assignments (plan Task 5/6/8)."""
2
+ from __future__ import annotations
3
+
4
+ from argparse import Namespace
5
+ from pathlib import Path
6
+
7
+ import thinkstack_cli
8
+ from thinkstack_cli.main import cmd_learning_stats
9
+
10
+ from thinkstack_core import GCCRepository
11
+ from thinkstack_core.reasoning_plus.learning.api import ReasoningPlusLearning
12
+
13
+ MESSAGE = (
14
+ "Retrieval beat finetuning here because the corpus changed mid-experiment."
15
+ )
16
+
17
+
18
+ def test_cli_imports_from_repo_source():
19
+ """Guard: CLI tests must exercise thinkstack-cli/, not site-packages."""
20
+ assert "thinkstack-cli" in thinkstack_cli.__file__, thinkstack_cli.__file__
21
+
22
+
23
+ def _repo_with_one_five_concept_learning(tmp_path: Path) -> Path:
24
+ """One learning carrying five trigger concepts."""
25
+ repo = GCCRepository.at(tmp_path)
26
+ repo.init()
27
+ ReasoningPlusLearning(gcc_dir=repo.gcc_dir).record_and_extract(
28
+ call_type="llm",
29
+ reasoning=MESSAGE,
30
+ outcome="success",
31
+ session_id="stats-1",
32
+ )
33
+ return tmp_path
34
+
35
+
36
+ def test_total_counts_learnings_not_concepts(tmp_path, monkeypatch, capsys):
37
+ root = _repo_with_one_five_concept_learning(tmp_path)
38
+ monkeypatch.chdir(root)
39
+
40
+ assert cmd_learning_stats(Namespace(path=str(root), by_concept=False)) == 0
41
+
42
+ out = capsys.readouterr().out
43
+ assert "Total learnings: 1" in out
44
+ assert "Active: 1" in out
45
+
46
+
47
+ def test_single_learning_is_not_a_danger_zone(tmp_path, monkeypatch, capsys):
48
+ """One learning cannot be a danger zone: dangers need min_samples evidence.
49
+
50
+ The inline `success_rate < 0.4` check had no minimum sample, and
51
+ _compute_success_rate returns 0.0 for concepts with no non-unknown
52
+ outcomes, so a single learning reported five danger zones from no data.
53
+ """
54
+ root = _repo_with_one_five_concept_learning(tmp_path)
55
+ monkeypatch.chdir(root)
56
+
57
+ assert cmd_learning_stats(Namespace(path=str(root), by_concept=False)) == 0
58
+
59
+ out = capsys.readouterr().out
60
+ assert "Danger zones: 0" in out
61
+
62
+
63
+ def test_empty_store_states_the_capture_path(tmp_path, monkeypatch, capsys):
64
+ """A project with learning on but no producer must not look merely empty.
65
+
66
+ 'No learnings found' was indistinguishable from 'nothing can be learned
67
+ here', which is what made the reported bug invisible.
68
+ """
69
+ repo = GCCRepository.at(tmp_path)
70
+ repo.init()
71
+ monkeypatch.chdir(tmp_path)
72
+
73
+ assert cmd_learning_stats(Namespace(path=str(tmp_path), by_concept=False)) == 0
74
+
75
+ out = capsys.readouterr().out
76
+ assert "No learnings found." in out
77
+ assert "capture path" in out.lower()
78
+
79
+
80
+ def test_by_concept_branch_still_works(tmp_path, monkeypatch, capsys):
81
+ root = _repo_with_one_five_concept_learning(tmp_path)
82
+ monkeypatch.chdir(root)
83
+
84
+ assert cmd_learning_stats(Namespace(path=str(root), by_concept=True)) == 0
85
+
86
+ out = capsys.readouterr().out
87
+ assert "Total: 1" in out
88
+ assert "Success rate:" in out
89
+
90
+
91
+ def test_stats_reports_zero_capture_when_learning_disabled(tmp_path, monkeypatch, capsys):
92
+ """Writes settings.toon via the store — settings.json is not the filename."""
93
+ from thinkstack_core.reasoning_plus.config import ReasoningPlusConfig, SettingsStore
94
+
95
+ repo = GCCRepository.at(tmp_path)
96
+ repo.init()
97
+ SettingsStore(repo.gcc_dir).save_project(
98
+ ReasoningPlusConfig(learning_enabled=False)
99
+ )
100
+ monkeypatch.chdir(tmp_path)
101
+
102
+ assert cmd_learning_stats(Namespace(path=str(tmp_path), by_concept=False)) == 0
103
+ assert "learning_enabled is false" in capsys.readouterr().out
@@ -131,6 +131,10 @@ class TestMcpClient:
131
131
  post_resp = MagicMock()
132
132
  post_resp.__enter__ = MagicMock(return_value=post_resp)
133
133
  post_resp.__exit__ = MagicMock(return_value=False)
134
+ # An SSE-only server returns 202 with an empty body; the response
135
+ # arrives on the stream. Returning a bare MagicMock here would be a
136
+ # body that is neither str nor bytes.
137
+ post_resp.read = MagicMock(return_value=b"")
134
138
 
135
139
  def urlopen_side_effect(req, **kwargs):
136
140
  if req.get_full_url() == "https://example.com/sse":
@@ -156,6 +160,10 @@ class TestMcpClient:
156
160
  post_resp = MagicMock()
157
161
  post_resp.__enter__ = MagicMock(return_value=post_resp)
158
162
  post_resp.__exit__ = MagicMock(return_value=False)
163
+ # An SSE-only server returns 202 with an empty body; the response
164
+ # arrives on the stream. Returning a bare MagicMock here would be a
165
+ # body that is neither str nor bytes.
166
+ post_resp.read = MagicMock(return_value=b"")
159
167
 
160
168
  def urlopen_side_effect(req, **kwargs):
161
169
  if req.get_full_url() == "https://example.com/sse":
@@ -168,6 +176,49 @@ class TestMcpClient:
168
176
 
169
177
  client.close()
170
178
 
179
+ def test_call_tool_reads_response_from_post_body(self) -> None:
180
+ """A stateless server answers in the POST body.
181
+
182
+ The stream carries only the handshake, so the response can only come
183
+ from the body. Before this was supported the client waited on a stream
184
+ that never delivered and failed with a timeout.
185
+ """
186
+ client = McpClient("https://example.com/sse", "test-key")
187
+ mock_messages_url = "https://example.com/messages?sessionId=test"
188
+
189
+ sse_resp = _SSEMockResponse([
190
+ b"event: endpoint\n",
191
+ f"data: {mock_messages_url}\n".encode("utf-8"),
192
+ b"\n",
193
+ # Deliberately no message event: the answer is only in the body.
194
+ ])
195
+
196
+ body = json.dumps({
197
+ "jsonrpc": "2.0",
198
+ "id": 1,
199
+ "result": {
200
+ "content": [
201
+ {"type": "text", "text": "Topics:\n authentication (5 concepts)"}
202
+ ],
203
+ "isError": False,
204
+ },
205
+ }).encode("utf-8")
206
+ post_resp = MagicMock()
207
+ post_resp.__enter__ = MagicMock(return_value=post_resp)
208
+ post_resp.__exit__ = MagicMock(return_value=False)
209
+ post_resp.read = MagicMock(return_value=body)
210
+
211
+ def urlopen_side_effect(req, **kwargs):
212
+ if req.get_full_url() == "https://example.com/sse":
213
+ return sse_resp
214
+ return post_resp
215
+
216
+ with patch("urllib.request.urlopen", side_effect=urlopen_side_effect):
217
+ result = client.call_tool("thinkstack_topics_list", {"all": True})
218
+ assert "authentication" in result
219
+
220
+ client.close()
221
+
171
222
  def test_is_available_returns_false_on_error(self) -> None:
172
223
  client = McpClient("https://nonexistent.example.com/sse", "bad-key", timeout=0.01)
173
224