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.
- {thinkstack_core-4.1.0/thinkstack_core.egg-info → thinkstack_core-4.2.0}/PKG-INFO +22 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/README.md +20 -2
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/pyproject.toml +2 -2
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_hidden_profile_benchmark.py +56 -0
- thinkstack_core-4.2.0/tests/test_hooks_learning_capture.py +263 -0
- thinkstack_core-4.2.0/tests/test_learning_capture_config.py +19 -0
- thinkstack_core-4.2.0/tests/test_learning_stats_counts.py +103 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_mcp_client.py +51 -0
- thinkstack_core-4.2.0/tests/test_no_stale_branding.py +106 -0
- thinkstack_core-4.2.0/tests/test_pii_redaction.py +56 -0
- thinkstack_core-4.2.0/tests/test_regression_learning_extraction.py +145 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_wrapper_capture.py +30 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_sync_bundle.py +66 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/__init__.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/__init__.py +5 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/base.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/config.py +7 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/dispatcher.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/jira.py +2 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/linear.py +2 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/pagerduty.py +2 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/slack.py +2 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/alerts/teams.py +2 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/broadcast/__init__.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/mcp_client.py +31 -5
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync_bundle.py +32 -9
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/thinkstack-mcp-bridge.js +52 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/__init__.py +2 -0
- thinkstack_core-4.2.0/thinkstack_core/consolidation/__init__.py +9 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/consolidation/workflow.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/dashboard_api.py +357 -1
- thinkstack_core-4.2.0/thinkstack_core/divergence/__init__.py +9 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/divergence/detector.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/server.py +23 -2
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gcc.py +11 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/pat.py +5 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hitl/__init__.py +2 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hitl/channels.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hitl/orchestrator.py +5 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/runner.py +97 -11
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/identity/__init__.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/mcp/auth.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/__init__.py +2 -0
- thinkstack_core-4.2.0/thinkstack_core/proxy/routes/__init__.py +1 -0
- thinkstack_core-4.2.0/thinkstack_core/proxy/routes/_shared.py +45 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/anthropic.py +29 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/azure_openai.py +29 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/gemini.py +29 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/groq.py +29 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/ollama.py +29 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/routes/openai.py +29 -3
- thinkstack_core-4.2.0/thinkstack_core/reasoning/__init__.py +10 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning/entry.py +2 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning/store.py +5 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/config.py +6 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/pii.py +51 -1
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/models.py +4 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/storage.py +34 -7
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/base.py +4 -1
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0/thinkstack_core.egg-info}/PKG-INFO +22 -3
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/SOURCES.txt +7 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/requires.txt +1 -0
- thinkstack_core-4.1.0/thinkstack_core/consolidation/__init__.py +0 -3
- thinkstack_core-4.1.0/thinkstack_core/divergence/__init__.py +0 -3
- thinkstack_core-4.1.0/thinkstack_core/proxy/routes/__init__.py +0 -1
- thinkstack_core-4.1.0/thinkstack_core/reasoning/__init__.py +0 -4
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/MANIFEST.in +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/setup.cfg +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_backend_factory.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_broadcast.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_claude_config_locations.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cli_sync_all.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cli_topics_concepts.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_auth.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_bridge.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_integration.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_sse.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_storage.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_cloud_sync.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_concept_catalog.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_doctor_ci.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_getting_started.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_github_app.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_github_pat.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_hook_entrypoint.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_rep_merge.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s0_gcc.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s10_jetbrains.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s10_vscode.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s11_codex.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s11_hooks_installer.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s12_github.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s12_gitlab.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s13_dashboard_api.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s13_forensic.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s14_audit_export.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s14_ciso_api.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s15_rep_network.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s16_alerts.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s17a_metrics.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s17b_observability.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s17b_sso.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s18_context_extraction.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s18_credibility.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s18_identity.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s19_learning_evolution.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s19_projects.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s19_reasoning_query.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s1_branch_merge.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_divergence.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_metrics.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_pii_guardrails.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s20_token_budget.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s21_cross_project.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s21_hitl.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s22_swe_bench.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s22_swe_bench_dry_run.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s22_sync.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s23_reasoning_plus.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s23_sync_remaining.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s24_reasoning_plus_learning.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_delivery_time.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_serve.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_setup_detection.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s25_templates.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_daemon.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_git_hooks.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_proxy.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s26_watcher.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_cross_project.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_drift.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_query.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s27_templates.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_cli.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_disagreement.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_planner.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_roles.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_session.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s28_simulator.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_ci.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_cli.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_dashboard.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s29_github.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s2_context.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s30_audit.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s31_cloud.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s3_sensitivity.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s4_invariants.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s4_variance_lock.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s5_privacy_disclosure.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s5_rep_sis.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s6_debug.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_mcp_hooks.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_ollama_wrapper.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_parser.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_proxy.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7_wrappers.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7ent_gateway.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s7ent_helm.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s8_aggphi.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s8_theta_synthesis.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_s9_gaps.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_setup_command.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_setup_targets.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_signing.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_storage_azure.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_sync_conflicts.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_sync_state.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_integration.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_io.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_serialize.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_toon_token.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/tests/test_topics.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/aggphi_textual.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/exporter.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/privacy.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/scrubber.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/service.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/audit/signing.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/broadcast/broadcaster.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/broadcast/watcher.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/capability.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_config.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.claude-stdio.json +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.cursor-mcp.json +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.opencode-bridge.json +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.opencode.json +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/client_configs/.vscode-mcp.json +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/setup.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync_conflicts.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/sync_state.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/cloud/team_sync.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/__main__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/capture.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/codex/proxy.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/compat.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/concept_catalog.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/consolidation/synthesizer.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/daemon/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/daemon/supervisor.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/daemon/watcher.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/deltaf.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/disclosure.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/key_manager.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/metrics_webhook.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/policy.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gateway/sso.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/app.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/comment_builder.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/pr_parser.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/github/pr_reporter.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/gitlab/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/claude_code.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/git_capture.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/git_commit.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/installer.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/hooks/pre_commit.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/identity/agent.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/identity/providers.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/invariants.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/mcp/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/mcp/server.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/aggregate.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/calibrate.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/calibration.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/credibility.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/delivery_time.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/dhs.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/mcs.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/roi.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/session_writer.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/shadow_ai.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/metrics/sprint_writer.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/datadog.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/formatter.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/report.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/servicenow.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/splunk.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/observability/webhook.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/blocks.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/inference.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/parser/thinking.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/projects.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/prompt_artifact.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/proxy/server.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/grep.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/hybrid.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/query/semantic.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rdp.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/augmenter.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/capture.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/context.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/analytics.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/api.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/chain.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/composer.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/conflicts.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/context_collector.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/cross_project.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/deny_list.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/embeddings.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/evolution.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/extractor.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/filter_five_layer.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/models.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/org_store.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/promotion.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/provenance.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/recorder.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/relevance.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/state.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/store.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/reasoning_plus/prompt.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/merge.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/node.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/server.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/rep_network/sync.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/sensitivity.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/io.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/reporting.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/token_counter.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serialization/toon.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/serve.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/disagreement.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/orchestrator.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/planner.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/session/simulator.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/signing.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/sis.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/skills/pr-reviewer/SKILL.md +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/skills/thinkstack-session-start/SKILL.md +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/engine.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/go.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/infra.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/api_design.md +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/bug_fix.md +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/decision_record.md +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/engine.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/library/security_review.md +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/python.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/react.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/templates/typescript.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/theta.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/theta_synthesis.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/topics.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/variance.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/__init__.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/anthropic.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/bedrock.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/gemini.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/ollama.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core/wrapper/openai.py +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/dependency_links.txt +0 -0
- {thinkstack_core-4.1.0 → thinkstack_core-4.2.0}/thinkstack_core.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|