tokenjam 0.6.2__tar.gz → 0.6.4__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.
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/workflows/ci.yml +19 -6
- {tokenjam-0.6.2 → tokenjam-0.6.4}/CLAUDE.md +46 -6
- {tokenjam-0.6.2 → tokenjam-0.6.4}/PKG-INFO +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/getting-started.md +4 -4
- {tokenjam-0.6.2 → tokenjam-0.6.4}/npm-wrapper/package.json +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/pyproject.toml +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/package.json +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cli.py +7 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cost_proposals_api.py +74 -5
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_db.py +67 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_full_pipeline.py +5 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_inbox_headline_total_api.py +15 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_relearn_api.py +32 -9
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_relearn_window_api.py +26 -3
- tokenjam-0.6.4/tests/integration/test_rules_surface.py +155 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_storage_backend_parity.py +44 -6
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_summarize_cli.py +64 -0
- tokenjam-0.6.4/tests/integration/test_trace_detail_payload.py +200 -0
- tokenjam-0.6.4/tests/unit/test_analysis_span_retention.py +584 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_analyzer_scope_isolation.py +10 -7
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_backfill_progress.py +4 -4
- tokenjam-0.6.4/tests/unit/test_cli_help_descriptions.py +55 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cli_palette.py +15 -5
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cost_proposal_verbs.py +8 -6
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cost_proposals.py +116 -20
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cost_proposals_model.py +44 -17
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_deadweight_apply.py +1 -1
- tokenjam-0.6.4/tests/unit/test_delivery_kinds.py +308 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_doctor_first_signal.py +2 -2
- tokenjam-0.6.4/tests/unit/test_doctor_spans_indexes.py +118 -0
- tokenjam-0.6.4/tests/unit/test_fix_actionability.py +271 -0
- tokenjam-0.6.4/tests/unit/test_fix_catalog.py +458 -0
- tokenjam-0.6.4/tests/unit/test_fix_grounding.py +194 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_framing.py +9 -9
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_inbox_contribution.py +53 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_ingest_otlp.py +21 -2
- tokenjam-0.6.4/tests/unit/test_ingested_ids_endpoint.py +94 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_lens_dashboard_states.py +13 -12
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_lens_select_all_behaviour.py +5 -5
- tokenjam-0.6.4/tests/unit/test_lens_ui_regression.py +217 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_model_apply.py +4 -2
- tokenjam-0.6.4/tests/unit/test_no_fix_prose_outside_the_catalog.py +233 -0
- tokenjam-0.6.4/tests/unit/test_no_sentinel_timestamps.py +367 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_backfill_scope.py +24 -17
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_capture_default.py +11 -8
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_first_run.py +66 -12
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_path_branch.py +9 -7
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_tool_inputs_default.py +10 -7
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_verify.py +3 -3
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_verify_cli.py +2 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_resend_render.py +16 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_summarize.py +256 -7
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_past_overspend.py +13 -5
- tokenjam-0.6.4/tests/unit/test_quickstart.py +1449 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_relearn.py +19 -10
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_relearn_apply.py +91 -50
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_relearn_archive_and_cost.py +28 -16
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_relearn_cli.py +9 -5
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_relearn_families_and_boundary.py +7 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_relearn_window.py +36 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_report_roundtrip.py +69 -0
- tokenjam-0.6.4/tests/unit/test_report_window.py +630 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_rollup_subagent_downsize_dedup.py +63 -0
- tokenjam-0.6.4/tests/unit/test_rule_placement.py +415 -0
- tokenjam-0.6.4/tests/unit/test_rule_presence.py +353 -0
- tokenjam-0.6.4/tests/unit/test_rulewrite_applied_detection.py +229 -0
- tokenjam-0.6.4/tests/unit/test_rulewrite_dismissal.py +192 -0
- tokenjam-0.6.4/tests/unit/test_rulewrite_legacy.py +176 -0
- tokenjam-0.6.4/tests/unit/test_rulewrite_lifecycle.py +325 -0
- tokenjam-0.6.4/tests/unit/test_scan_bar_truth.py +286 -0
- tokenjam-0.6.4/tests/unit/test_scan_cycle_provenance.py +446 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_spans_stats_repair.py +109 -0
- tokenjam-0.6.4/tests/unit/test_subagent_rubric_contradiction.py +97 -0
- tokenjam-0.6.4/tests/unit/test_summarize_calibrate.py +199 -0
- tokenjam-0.6.4/tests/unit/test_summarize_classify.py +309 -0
- tokenjam-0.6.4/tests/unit/test_summarize_estimate.py +251 -0
- tokenjam-0.6.4/tests/unit/test_summarize_numbering.py +79 -0
- tokenjam-0.6.4/tests/unit/test_summarize_relocate.py +307 -0
- tokenjam-0.6.4/tests/unit/test_summarize_route.py +195 -0
- tokenjam-0.6.4/tests/unit/test_summarize_sections.py +90 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_session.py +131 -2
- tokenjam-0.6.4/tests/unit/test_transcript_cache.py +352 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_transcript_sync.py +154 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_ui_offline.py +47 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_write_budget.py +84 -32
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/app.py +4 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/logs.py +54 -14
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/optimize.py +111 -5
- tokenjam-0.6.4/tokenjam/api/routes/persona.py +128 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/relearn.py +87 -34
- tokenjam-0.6.4/tokenjam/api/routes/rules.py +202 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/sessions.py +70 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/traces.py +100 -12
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/backfill_progress.py +48 -3
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/banner.py +20 -8
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_backfill.py +19 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_budget.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_context.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_cost.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_demo.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_doctor.py +222 -3
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_drift.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_export.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_loop.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_mcp.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_onboard.py +300 -152
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_optimize.py +27 -5
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_otel.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_ping.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_policy.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_pricing.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_proxy.py +1 -1
- tokenjam-0.6.4/tokenjam/cli/cmd_quickstart.py +716 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_quota_audit.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_relearn.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_report.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_reset.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_resume_brief.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_route.py +1 -1
- tokenjam-0.6.4/tokenjam/cli/cmd_rules.py +414 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_serve.py +33 -64
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_session_end.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_session_story.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_statusline.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_stop.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_summarize.py +198 -5
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_tokenmaxx.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_uninstall.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_upgrade.py +1 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cost_proposal_verbs.py +11 -12
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/home.py +19 -11
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/main.py +7 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/relearn_write_verbs.py +11 -4
- tokenjam-0.6.4/tokenjam/core/analysis_span.py +207 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/api_backend.py +42 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/backfill.py +30 -7
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/config.py +17 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/data_span.py +32 -3
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/db.py +354 -18
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/distill.py +19 -0
- tokenjam-0.6.4/tokenjam/core/fixes/__init__.py +54 -0
- tokenjam-0.6.4/tokenjam/core/fixes/catalog.py +250 -0
- tokenjam-0.6.4/tokenjam/core/fixes/grounding.py +156 -0
- tokenjam-0.6.4/tokenjam/core/fixes/lint.py +406 -0
- tokenjam-0.6.4/tokenjam/core/fixes/registry.py +806 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/framing.py +10 -4
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/ingest_adapters/codex.py +22 -3
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/models.py +5 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/onboard_verify.py +2 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/context_resend.py +52 -34
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/deadweight.py +10 -7
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/model_downgrade.py +6 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/output_verbosity.py +3 -7
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/relearn.py +166 -146
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/summarize.py +237 -18
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/workflow_restructure.py +1 -1
- tokenjam-0.6.4/tokenjam/core/optimize/build_stamp.py +42 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/cost_apply.py +120 -4
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/cost_proposals.py +764 -178
- tokenjam-0.6.4/tokenjam/core/optimize/dismissals.py +149 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/inbox_contribution.py +112 -15
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/model_apply.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/relearn_apply.py +211 -99
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/relearn_proposals.py +2 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/relearn_store.py +79 -11
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/relearn_window.py +21 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/report_store.py +57 -10
- tokenjam-0.6.4/tokenjam/core/optimize/report_window.py +115 -0
- tokenjam-0.6.4/tokenjam/core/optimize/rule_placement.py +490 -0
- tokenjam-0.6.4/tokenjam/core/optimize/rule_scope.py +95 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/runner.py +50 -1
- tokenjam-0.6.4/tokenjam/core/optimize/scan_cycle.py +280 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/types.py +26 -2
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/write_budget.py +252 -44
- tokenjam-0.6.4/tokenjam/core/retention.py +91 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/__init__.py +58 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/apply.py +308 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/delivery.py +404 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/kinds.py +91 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/legacy.py +151 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/plan.py +391 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/presence.py +277 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/store.py +271 -0
- tokenjam-0.6.4/tokenjam/core/rulewrite/types.py +274 -0
- tokenjam-0.6.4/tokenjam/core/summarize/calibrate.py +299 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/candidates.py +59 -3
- tokenjam-0.6.4/tokenjam/core/summarize/classify.py +480 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/delivery.py +1 -1
- tokenjam-0.6.4/tokenjam/core/summarize/detect.py +284 -0
- tokenjam-0.6.4/tokenjam/core/summarize/estimate.py +248 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/load_semantics.py +81 -6
- tokenjam-0.6.4/tokenjam/core/summarize/numbering.py +102 -0
- tokenjam-0.6.4/tokenjam/core/summarize/relocate.py +375 -0
- tokenjam-0.6.4/tokenjam/core/summarize/route.py +284 -0
- tokenjam-0.6.4/tokenjam/core/summarize/sections.py +142 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/session.py +188 -13
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/wrap.py +12 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/transcript.py +61 -1
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/transcript_cache.py +87 -12
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/transcript_sync.py +36 -6
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/otel/otlp_parsing.py +15 -4
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/ui/index.html +1373 -301
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/utils/theme.py +10 -3
- tokenjam-0.6.2/tests/unit/test_lens_ui_regression.py +0 -5451
- tokenjam-0.6.2/tests/unit/test_quickstart.py +0 -1100
- tokenjam-0.6.2/tests/unit/test_summarize_estimate.py +0 -101
- tokenjam-0.6.2/tests/unit/test_transcript_cache.py +0 -162
- tokenjam-0.6.2/tokenjam/cli/cmd_quickstart.py +0 -839
- tokenjam-0.6.2/tokenjam/core/retention.py +0 -21
- tokenjam-0.6.2/tokenjam/core/summarize/detect.py +0 -111
- tokenjam-0.6.2/tokenjam/core/summarize/estimate.py +0 -124
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.dockerignore +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/CODEOWNERS +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/actions/docker-publish/action.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/pull_request_template.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/scripts/archive_traffic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/scripts/contributor_label_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/workflows/contributor-label-guard.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/workflows/docker-publish.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/workflows/publish-npm.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/workflows/publish-pypi.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.github/workflows/traffic-archive.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/.gitignore +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/AGENTS.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/CODE_OF_CONDUCT.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/CONTRIBUTING.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/Dockerfile +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/LICENSE +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/Makefile +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/SECURITY.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/agent-capability-matrix.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/alerts.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/architecture.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/assets/tj-quickstart-hero.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/assets/tokenjam-flow-band.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/assets/tokenjam-token-flow.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/assets/tokenjam-waste-grid.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/backfill/codex.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/backfill/helicone.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/backfill/langfuse.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/backfill/otlp.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/backfill/overview.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/tokenjam-banner.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/tokenjam-banner.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/tokenjam-icon.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/tokenjam-icon.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/tokenjam-repo-header.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/tokenjam-wordmark.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/brand/tokenjam-wordmark.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/claude-code-integration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/cli-reference.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/configuration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/export.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/first-hour.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/framework-support.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/harness-integration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/installation.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/internal/close-the-loop.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/internal/lens-vendor-versions.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/internal/release-smoke-checklist.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/internal/repeat-task-codification-measurement.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/internal/specs/.gitkeep +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/internal/specs/map-board-direction.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/nemoclaw-integration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/openclaw.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/optimize/cache.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/optimize/downsize.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/optimize/reuse.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/optimize/script.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/optimize/subagent.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/optimize/trim.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/optimize/verbosity.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/policy/overview.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/proxy/overview.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/python-sdk.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/screenshots/tj-budget.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/screenshots/tj-cost.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/screenshots/tj-dashboard-leaderboard.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/screenshots/tj-dashboard-tools.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/screenshots/tj-dashboard.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/screenshots/tj-status.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/screenshots/tj-traces.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/docs/typescript-sdk.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/alerts_and_drift/_shared.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/alerts_and_drift/drift_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/alerts_and_drift/record_outcome_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/multi/rag_pipeline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/multi/research_team.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/multi/router_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/multi/sample_docs/agent_patterns.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/multi/sample_docs/cost_management.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/multi/sample_docs/observability.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/multi/sample_docs/safety.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/openclaw/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_framework/autogen_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_framework/crewai_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_framework/langchain_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_framework/langgraph_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_framework/llamaindex_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_provider/anthropic_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_provider/bedrock_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_provider/gemini_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_provider/litellm_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_provider/openai_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/growth/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/hallucination-drift/BLOG.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/hallucination-drift/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/hallucination-drift/scenario.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/retry-loop/BLOG.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/retry-loop/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/retry-loop/scenario.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/surprise-cost/BLOG.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/surprise-cost/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/incidents/surprise-cost/scenario.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/npm-wrapper/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/npm-wrapper/bin/tj.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/scripts/release.sh +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/examples/record-outcome.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/package-lock.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/client.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/client.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/index.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/record-outcome.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/semconv.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/semconv.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/span-builder.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/span-builder.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/src/types.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/sdk-ts/tsconfig.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agent-post-release-runner.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agent-post-release-v0.4.0.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agent-pre-release-runner.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agent-pre-release-v0.4.0.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agent-pre-release-v0.4.1.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/email_agent_budget_breach.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/email_agent_drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/email_agent_loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/email_agent_normal.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/mock_llm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/test_mock_scenarios.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/agents/test_record_outcome.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/conftest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/e2e/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/e2e/conftest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/e2e/test_real_llm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/factories.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/fixtures/helicone_real_response.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/fixtures/langfuse_real_response.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/fixtures/otlp_sample.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_agents_and_tenants_dual_units.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_analytics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_analyzers_off_the_request_path.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cli_api_framing_parity.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cli_uninstall.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cmd_proxy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cmd_route.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_context_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cost_attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cost_charts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cost_compare_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_cost_framing_window.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_data_access_parity.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_demo_live.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_demos.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_first_run_roundtrip_239.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_global_json_flag.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_litellm_enrichment.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_logs_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_policy_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_proxy_app.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_proxy_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_quota_audit_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_sdk_config_discovery.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_summarize_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_summarize_run_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/integration/test_tokenmaxx_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/manual-new-release-tests.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/manual-pre-release-testing.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/rate_bands.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/results/agent-post-release-v0.4.0-20260619T221412Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/results/agent-pre-release-v0.4.0-20260619T202233Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/results/agent-pre-release-v0.4.0-20260619T205108Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/results/agent-pre-release-v0.4.1-20260620T002546Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/test_alert_rules.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/test_async_hooks_concurrency.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/test_cost_tracking.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/test_drift_detection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/test_ingest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/test_request_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/synthetic/test_schema_validation.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/token_aggregate_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/toy_agent/toy_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_agent_kind.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_analytics_metric_expr.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_analyzer_scope.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_api_backend_timeout.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_async_hooks.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_attribution_cache.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_backfill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_backfill_codex.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_backfill_interrupted_flush.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_batch_placement.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_budget_cap_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_budget_group_cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_budget_route.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_bulk_insert_spans.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cache_efficacy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cache_recommend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cache_root_cause.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cache_root_cause_proposals.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cache_root_cause_render.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cli_entrypoint.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_mcp_config_discovery.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_resume_brief.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_serve_port_precheck.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_session_story.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_status_teaser.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_stop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_stop_scoping.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_tokenmaxx.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cmd_upgrade.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_compare.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_config.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_config_discovery_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_config_secret_divergence.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_config_write_target.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_context_diagnostic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_context_resend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_contributor_label_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cost_accounting_guards.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cost_api_recoverable_presentation.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cost_compare_framing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_cycle.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_data_access.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_db_close_backfill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_db_helpers.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_deadweight.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_demo_env.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_demo_live.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_demo_packaging.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_demo_scenarios.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_distill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_doctor_capture_prompts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_doctor_paths.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_doctor_statusline_wiring.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_doctor_transcript_gap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_downsize_agent_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_downsize_applied_ledger_migration.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_downsize_driver_role.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_export_claude_code.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_export_route_generators.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_formatting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_harness_setup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_http_exporter_auth.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_humanize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_ingest_accounting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_ingest_helicone.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_ingest_langfuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_litellm_client.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_litellm_integration.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_logs_converter.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_loop_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_loop_transcript_path.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_mcp_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_mcp_server.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_mcp_setup_project_tj_config.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_method_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_method_spine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_model_tiers.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_models.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_mythos_premium_gates.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_no_tracked_dev_secrets.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_no_tracker_ids_in_printed_strings.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_add_project.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_budget_gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_codex.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_daemon.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_ephemeral_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_hygiene.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_path_resolution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_plan.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_relearn_activation.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_resume_hook.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_stack_detection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_statusline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_onboard_zshrc_otel.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_openclaw_ingest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_analyzers_route.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_clustering.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_plan_multiplier.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_recoverable.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_reuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_savings_ci.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_subagent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_threshold_rendering.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_trim_provenance.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_validate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_optimize_verbosity.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_opus_quota_audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_persona_analyzer_gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_phases.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_ping.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_plan_tier_apply.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_policy_engine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_pricing_axes.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_pricing_coverage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_pricing_lookup_forms.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_pricing_override.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_prompt_bloat.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_provider_attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_provider_attribution_194.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_provider_content_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_provider_run_id.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_proxy_audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_proxy_gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_rate_profile.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_recommendations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_relearn_otel.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_repeat_task.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_reset.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_resume_brief.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_reuse_skeleton.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_rollup_covers_every_rendered_row.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_runlink.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_schema_self_heal.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_sdk_agent_session.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_sdk_attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_serve_transcript_catchup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_server_state.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_session_cost_drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_session_label.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_session_status_lifecycle.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_session_subagents.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_sessionmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_span_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_status_sdk.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_statusline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_stream_usage_analyzer.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_stream_usage_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_apply.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_backup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_candidates.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_catalog.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_delivery.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_invocations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_load_semantics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_repo_roots.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_summarize_wrap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_time_parse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_transcript.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_transport_401.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_uninstall_persistent_install.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_uninstall_runner_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_uninstall_wrapper.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_workflow_restructure.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tests/unit/test_workmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/deps.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/middleware.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/_body.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/agents.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/analytics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/budget.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/context.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/cost_compare.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/metrics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/otlp.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/quota_audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/recommendations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/reuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/runs.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/spans.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/status.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/summarize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/summarize_run.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/tools.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/api/routes/version.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_status.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_tools.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/cmd_traces.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/data_access.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/entrypoint.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/json_option.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/cli/onboard_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/agent_kind.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/atomic_write.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/attribution_cache.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/context_diagnostic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/cycle.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/export/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/export/ccr.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/export/claude_code.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/export/common.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/export/litellm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/export/reuse_report.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/export/reuse_skeleton.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/harness_setup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/ingest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/ingest_adapters/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/ingest_adapters/helicone.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/ingest_adapters/otlp.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/method_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/method_spine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/model_tiers.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/accounting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/batch_placement.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/downsize_agents.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/plan_reuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/resend_tail.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/stream_usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/analyzers/subagent_rightsizing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/clustering.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/projection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/rate_profile.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/registry.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/relearn_otel.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/repeat_task.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/scope.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/span_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/stats.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/optimize/validate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/phases.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/pricing_coverage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/recommendations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/resume_brief.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/runlink.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/savings_log.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/schema_validator.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/server_state.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/session_timeline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/sessionmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/agent_files.toml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/apply.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/backup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/catalog.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/invocations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/summarize/repo_roots.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/core/workmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/demo/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/demo/env.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/demo/live.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/mcp/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/mcp/server.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/otel/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/otel/provider.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/otel/semconv.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/pricing/models.toml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/app.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/engine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/observer.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/recommend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/server.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/stream_usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/proxy/wiring.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/py.typed +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/bootstrap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/client.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/http_exporter.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/_request_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/anthropic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/autogen.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/base.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/bedrock.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/crewai.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/gemini.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/langchain.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/langgraph.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/litellm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/llamaindex.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/openai.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/sdk/transport.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/ui/vendor/htm.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/ui/vendor/preact-hooks.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/ui/vendor/preact.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/ui/vendor/uplot.css +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/ui/vendor/uplot.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/utils/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/utils/formatting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/utils/humanize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/utils/ids.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/utils/signatures.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.4}/tokenjam/utils/time_parse.py +0 -0
|
@@ -23,6 +23,25 @@ jobs:
|
|
|
23
23
|
exit 1
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
+
lint:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- name: Set up Python 3.12
|
|
32
|
+
uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.12"
|
|
35
|
+
|
|
36
|
+
- name: Install dependencies
|
|
37
|
+
run: pip install -e ".[dev]"
|
|
38
|
+
|
|
39
|
+
- name: Lint
|
|
40
|
+
run: ruff check tokenjam/
|
|
41
|
+
|
|
42
|
+
- name: Type check
|
|
43
|
+
run: mypy tokenjam/
|
|
44
|
+
|
|
26
45
|
test:
|
|
27
46
|
runs-on: ubuntu-latest
|
|
28
47
|
strategy:
|
|
@@ -40,12 +59,6 @@ jobs:
|
|
|
40
59
|
- name: Install dependencies
|
|
41
60
|
run: pip install -e ".[dev,mcp]"
|
|
42
61
|
|
|
43
|
-
- name: Lint
|
|
44
|
-
run: ruff check tokenjam/
|
|
45
|
-
|
|
46
|
-
- name: Type check
|
|
47
|
-
run: mypy tokenjam/
|
|
48
|
-
|
|
49
62
|
- name: Run tests
|
|
50
63
|
run: pytest tests/unit/ tests/synthetic/ tests/agents/ tests/integration/
|
|
51
64
|
|
|
@@ -203,6 +203,34 @@ Post-ingest hooks run after each span is written to DB:
|
|
|
203
203
|
|
|
204
204
|
**A headline figure is the CEILING, not the expectation — and a ceiling still has to be reachable.** Where the arithmetic legitimately supports a range, the number a card leads with is the top of it: the largest figure the derivation honestly bears, not a discounted guess at what a user would actually realize. This is a founder decision and it is deliberate, so do not "correct" a headline downward by applying a realization factor, an adoption rate, or any other haircut invented at the render layer. It is not, however, a licence to publish an unreachable number: a ceiling has to be attainable in *some* real world, so a slice the fix **structurally cannot touch** — spans the remedy does not reach, sessions the analyzer never examined — does not belong inside it, and shaving those out is a correctness fix rather than a haircut. One carve-out: where a card offers a fix the user is about to apply, lead with the realized figure instead, because at that point the honest question has changed from "how large could this be" to "what will this specific action do". This is a convention about which of two defensible numbers to lead with; the numbers themselves belong on the cards and in `estimate_basis`, never in this file.
|
|
205
205
|
|
|
206
|
+
**WHERE a permanent rule lands is an INPUT to whether it is offered, not a presentation of the decision.** Four analyzers (`downsize`, `resend`, `subagent`, `relearn`) share one fix shape — a block appended to a `CLAUDE.md` — and each used to offer exactly one destination, the workspace/root file. That is wrong in both directions: a root-level rule is re-sent in every session of every project, including the ones that never exhibited the problem, and it dilutes the file whose dilution is what makes rules start being ignored. Every session records its working directory (`core/transcript.first_recorded_cwd` is the one extractor; `core/summarize/repo_roots.resolve_roots` turns those into boundary-safe roots), so `core/optimize/rule_placement` can resolve which projects incurred a finding and attribute each session's share to a destination. Because the standing cost is `per-session tokens x the sessions that load the file`, confining a rule to the projects that exhibited the behaviour can flip a net-negative verdict to net-positive with no change to the finding at all. Three things fall out and are easy to get wrong. **(a) Two costs, not one.** STANDING cost follows the sessions (what the rule costs to keep, and the only figure the saving is netted against); FOOTPRINT cost follows the FILES (`per-session x destinations written`, and the only figure the write budget is spent in). A three-project split can halve the standing cost while tripling the budget spend; charging either for the other's question makes the placement decision stop being arithmetic. **(b) The growth allowance is per FILE.** `measured_agent_file_tokens` aggregates the `summarize` scan, which is right for "may this window grow the files at all" and wrong for "may this rule grow THIS file" — 10% of an aggregate dominated by a large root file can exceed a small project `CLAUDE.md` entirely, so `measured_agent_file_tokens_by_path` + `WriteBudget.destination_budget` size each destination against its own measured size. **(c) Unifying the FIX surface must not merge the CLAIMS** (Critical Rule 27): the per-session weight maps (`DowngradeFinding.driver_session_tokens`, `ResendFinding.session_weights`) are BREAKDOWNS of each analyzer's own `past_overspend_tokens`, split by the same weights on both the token and the dollar side so every destination's implied rate equals the finding's own (Critical Rule 28). A recorded cwd that no longer exists is counted and surfaced in a `coverage_note` saying the gap is what was **not analysed** (Critical Rule 30), never reconstructed and never redistributed onto the destinations that did resolve.
|
|
207
|
+
|
|
208
|
+
**A rule a card writes must ERASE the number the card found.** `past_overspend_usd` is the maximum its analyzer knows: applying the fix should remove the behaviour that produced it. If applying it would leave the number roughly where it was, that is a correctness bug, not a UX gap. The live example: `SUBAGENT_RUBRIC_INTRO` used to end with "a subagent that does little tool work and returns a short result rarely needs the premium tier" — exactly the `output_tokens`/`tool_calls` gate deleted from `analyzers/subagent_rightsizing.py` for inverting on the expensive dispatches (Critical Rule 29), so the rule told the agent to keep doing the thing the card was billing it for. `context_resend.RIGHTSIZE_FIX_TEMPLATE` carried the same sentence in a different costume. Two durable halves: **the shape-based default-down is the lever and stays**, and **a premium escape hatch must be checkable by an outside reader rather than self-assessed** — "unless the subtask genuinely needs deep reasoning" asks the dispatching agent to rate its own task's difficulty, and an agent asked that question answers yes, so the exception swallows the rule. State the exception as conditions that are either written into the dispatch or not.
|
|
209
|
+
|
|
210
|
+
**HOW a rule reaches the agent is a DELIVERY MECHANISM, and "rung 3 is free" is FALSE for a context-injecting one.** Appending markdown to a `CLAUDE.md` is one way to put guidance in front of an agent; a Claude Code hook is another, and for several analyzers a better one, because it delivers at the moment of the decision rather than at the top of a long context. `core/rulewrite/delivery.py` is the seam: a `DeliveryKind` owns its own renderer AND its own pricer, and adding a mechanism is a registration plus two functions — never an edit to the staging, diff, apply, undo or budget machinery, none of which may name a CLAUDE.md. The pricing half is the part that bites. `write_budget`'s rung ladder charges rung 3+ ZERO because a settings file is *executed*, never sent as prompt text; that is earned by a formatter or a guard hook, and it is **wrong for a `UserPromptSubmit` re-injection or a `PreToolUse` additionalContext nudge**, which are prompt text on a different SCHEDULE. Their cost is worse-behaved than a rule's, not better: an injected block lands in the conversation and is re-sent every subsequent turn, so it is not even bounded by the number of injections the way a session-start read is bounded by the session count. The ladder grades intervention STRENGTH, not delivery, so the question "does this put tokens in front of the model" is asked of the mechanism (`DeliveryKind.carries_prompt_text`) and never inferred from the rung. Caught in this repo's own first draft of the seam: a rung-3 kind flagged as carrying prompt text still priced to zero, because its pricer routed through the ladder. An unknown mechanism is REFUSED rather than falling back to markdown — a wrong write to a real file is the one failure the lifecycle exists to prevent.
|
|
211
|
+
|
|
212
|
+
**A fix the user already dealt with must lose its OFFER and keep its FIGURE.** The rule-write surface consults the apply ledgers (`cost_applied.json` / `applied_fixes.json`, via `cost_apply.applied_signatures` / `relearn_apply.applied_signatures`, matched by `cost_apply.signature_is_applied`) and withdraws the offer for a signature already applied. `past_overspend_usd`/`_tokens` are untouched, and that is Critical Rule 32 in its sharpest form: the waste genuinely happened inside the analyzed window, and the user having since fixed it does not un-spend the money. An action-availability gate establishes that WE have no remedy left to offer, never that the spend was avoidable or free — so it may never reach back and edit a past figure, and a zero there would be the exact "no action for this" / "this was free" conflation. Pin it by asserting the whole record is byte-identical with and without the ledger entry EXCEPT the offer fields; an equal-value check passes a figure that got re-derived through a zero. Two further rules that cost real correctness here: **the row stays listed** carrying an applied state rather than vanishing (a user who applies something and then sees nothing cannot tell "done" from "broken"), and **an unreadable ledger reads as "nothing applied"** — that direction can only waste attention, while the opposite hides fixes the user never made. Reuse `signature_is_applied` rather than writing a second matcher: it already resolves a legacy agent-only mark as covering the later model-qualified signatures for that agent, and a second matcher silently reopens every card it settles.
|
|
213
|
+
|
|
214
|
+
**A fix must name an action that EXISTS, for THIS user, through a key the harness actually reads.** Four shipped defects, one framing failure — applying the fix has to be able to erase the number its analyzer found. (a) The right-sizing snippet emitted `reasoning_effort:`, which Claude Code does not read; the field is `effort` (`low`/`medium`/`high`/`xhigh`/`max`). **A silently-ignored key is worse than no line**: it converts an unfixed problem into one the user believes is fixed. It was also hardcoded to `low` for a population that is by construction large and tool-heavy, so the guess recommended making the expensive work worse — a value that cannot be derived from the observation is OMITTED, never guessed, because a plausible line in a frontmatter block is indistinguishable from a measured one. (b) The subagent lane required an agent definition file to already EXIST, and every Claude Code dispatch is a built-in (`general-purpose`, `Explore`, `Plan`, `fork`) which has none — so the branch was unreachable on the dominant corpus and the claim fell through to prose. **A user or project subagent of the same name overrides the built-in and keeps its own `model`, so no-file is not no-fix: offer the CREATE.** The origin belongs in the evidence too — `model` defaults to `inherit`, so an Opus-driven session hands every dispatch an Opus worker unless pinned; that is an unset default, not a decision, which is why it goes unnoticed. Watch the shape checks around this: a lowercase-only slug predicate silently rejected `Explore` and `Plan`, the exact names the fix targets. (c) The per-agent downsize lane told Claude Code users to "redeploy or restart the agent", but there `agent_id` is `claude-code-<cwd-basename>` — a PROJECT DIRECTORY with ephemeral sessions, no process and no model id written down. Gate that with a **persona check at the point of production** (Critical Rule 26(c)), not a dollar threshold, and withhold only the offer. (d) `relearn`'s `edit_before_read` shipped a card whose own fix text says the harness already handles it and no rule is needed. `is_placeholder_fix` cannot see this — the text is a real, specific sentence that happens to say there is nothing to do — so the family carries an explicit `advisory_only` flag and `REASON_ADVISORY_ONLY`, distinct from the placeholder reason because "no template matched" and "none is needed" are different statements. In all four the observation stands and `past_overspend_*` is untouched (Critical Rule 32).
|
|
215
|
+
|
|
216
|
+
**Fix text lives in ONE catalog with a lint, because scattered constants are why the above could happen.** `core/fixes/` holds one `FixRecord` per fix type — text, delivery kind, personas, lever, the observation it answers, and the shapes it may not excuse — and every analyzer reads its text from there instead of defining a constant. The proof this is structural, not tidiness: the identical sizing contradiction shipped in BOTH the subagent rubric and the resend right-sizing template in different words, so correcting the reported one left the other live. Two copies of a policy is two policies. `core/fixes/lint.py` checks the properties mechanically — under the published line ceiling, long enough to be an instruction, no escape hatch the agent grades itself against, advisory-only text never offered as a write, fixed-point behaviour escalated to a hook — and above all: **a fix may not re-license the behaviour its own analyzer bills for**, which is the whole D-class and is now caught by a substring check against the record's own `must_not_relicense` set rather than by review. Two dimensions the catalog carries that a bare string cannot: **persona** (a Claude Code user edits instruction files, agent files and hooks; an SDK service edits routing code and redeploys — handing either the other's fix names an action they cannot take) and **lever**, where `model` and `effort` are separate and not substitutes: a worker that did not know enough needs a bigger model, one that did not try hard enough needs more effort.
|
|
217
|
+
|
|
218
|
+
**One instruction, one record — and the duplicate check must use CONTAINMENT, not Jaccard.** Three analyzers (`resend`, `downsize`, `relearn`'s `context_overflow`) each authored their own wording of "delegate context-heavy work to a subagent", so three near-identical blocks could land in one `CLAUDE.md`. The write budget's one-block-per-family rule cannot see this: they are three families from three analyzers. The harm is not untidiness — **length and redundancy reduce adherence**, so writing a rule three times makes it LESS likely to be followed than writing it once, and each analyzer's copy actively defeats the others. Their CLAIMS stay disjoint (Critical Rule 27 is untouched — they price different span populations); only the instruction is shared, with per-analyzer framing living in the card's advise text rather than in a second copy of the rule. The lint's metric is load-bearing and the obvious choice is wrong: **Jaccard divides by the union, so it collapses when two texts differ in LENGTH, which is exactly this defect's shape** — measured against the real wordings, the two duplicates scored 24% and 17% Jaccard against the rule they duplicated and would have been certified clean. Containment (`|a ∩ b| / min(|a|,|b|)`) scores the same pairs at 67% and 72%. A check that cannot catch what it was written for is worse than no check, because it reports the problem as absent.
|
|
219
|
+
|
|
220
|
+
**Two surfaces rendering one finding must lead with the SAME fix, through the same helper.** The Review inbox card led with the durable offload rule while `tj optimize`'s CLI branch led with `/compact`, for the identical finding — and the CLI led with the weaker one, which `COMPACTION_FIX`'s own text disclaims ("never fixes the pattern going forward; treat it as immediate relief, not the durable fix"). Both now render through `cost_proposals.compound_offload_fix`, so they cannot drift into different wordings again. Related: **`/compact` is never listed as an actionable lever.** It is transient, persists nothing past the session, and users who feel a session filling already do it unprompted — it sits below the actionable ceiling recorded in `.claude/product-state/ANALYZER-PERSONA-MATRIX.md`. Listing it beside durable levers implies it is the same kind of thing, and pads a list whose length is itself a cost to adherence. It survives only in an explicitly-labelled "immediate relief in an already-full session" position.
|
|
221
|
+
|
|
222
|
+
**A path-scoped rule is not a cheaper way to say the same thing — it changes WHICH fixes are offerable at all.** A `.claude/rules/*.md` carrying a `paths:` glob loads only when Claude reads a matching file, so only its frontmatter is resident (measured on a real record: ~13% of the same words in a `CLAUDE.md`). Because standing cost is an INPUT to the offer decision, that near-zero rent flips fixes from net-negative to net-positive: the same observation that cannot carry a `CLAUDE.md` rule can carry a scoped one. Three things are load-bearing. **(a) The frontmatter is the entire reason it is cheap** — a rule in the same directory WITHOUT `paths:` loads at launch with `CLAUDE.md` priority, so `load_semantics.classify` takes the file TEXT and decides on `has_paths_scope`, never on the directory; a path-scoped kind that emits no globs has silently become an always-resident rule while still being priced as if it were not, and the renderer refuses rather than degrading because the failure is invisible (the file is written, the rule works, only the cost is wrong). Accept both YAML forms — a block list's inline value is empty, so an inline-only check reads a perfectly good scoped rule as unscoped. **(b) Selection is DERIVED from the LEVER, never preferred.** A rule about the shape of the next action (model, effort, offload, routing) decides something *before any file is read*; scoped to globs it would load only after the decision it governs was made — present, well-formed and useless, which is worse than paying the rent and is invisible. Only file-shaped (awareness) fixes may scope, and only when the record names its own globs; inferring a glob from prose is guessing which files an instruction is about. **(c) Brace expansion has a budget** — past it a pattern is used UNEXPANDED and matches nothing, so the rule reads as applied while never firing; refuse such a glob rather than emitting a silent no-op.
|
|
223
|
+
|
|
224
|
+
**Hooks are already written; what was wrong was their PRICE.** `relearn_apply` has shipped hook generation for some time (guard + reactive renderers, the settings patch, the enforcement wiring), so "nothing writes a hook" is not the gap — three of the four rung-3 families that ship are `PostToolUseFailure` reactive hooks that inject `hookSpecificOutput.additionalContext`, and the rung ladder priced all of them at ZERO. For an executing guard that zero is earned; for an injecting hook it is false, and its cost is worse-behaved than a rule's, because the injected block lands in the conversation and is re-sent on every subsequent turn rather than being bounded by session count. Hence two delivery kinds, not one, each with its own pricer. Three rails the generated hook must carry, and they are not interchangeable with a rule's: **`additionalContext` MUST be nested inside `hookSpecificOutput`** (at top level it is silently ignored — the hook runs, exits 0, injects nothing, and looks like it worked); **the per-session cap lives in the script**, keyed on the stdin `session_id`, because only `SessionStart` is structurally once-per-session and the `once` field is honored solely in skill frontmatter, never in a settings file; and **injection is advisory, fail-open, never blocking by default** — a misfiring blocking hook degrades every session until removed, an asymmetry that justifies the rails on its own. Hook entries MERGE across settings layers rather than overriding, so a write cannot clobber the user's hooks but also must not assume it is the only writer.
|
|
225
|
+
|
|
226
|
+
**Ground a behavioural rule in the evidence it came from — by REPLACING vague words, never by appending them.** The behavioural fixes (`downsize`'s driver-role card, `resend`'s offload rule, `relearn`'s rung-1 notes) described a HABIT generically while the analyzer that produced them knew which directories the user worked in, which tools drove the tail and which models ran it. That is the shape of instruction the published guidance says gets ignored ("concrete enough to verify": `API handlers live in src/api/handlers/` beats `keep files organized`). Four rules. **(a) Substitution, never generation** — a template carrying "you did X in Y" beats both a generic sentence and a model call: no cost, no latency, no new failure mode, and it cannot hallucinate a directory the user does not have. **(b) SPECIFIC MUST NOT MEAN LONGER.** The guidance is specific *and concise*, and a longer instruction file reduces adherence — the very thing grounding is for — so a substitution REPLACES a vague span and a grounded rendering that came out materially longer is DISCARDED for the generic text (`grounding.MAX_GROUNDED_GROWTH`). On the reference corpus the grounded offload rule is 94% the length of the generic one it replaces, naming three real repos in place of a parenthesised enumeration; the failure mode to guard against is the generic sentence plus an evidence paragraph, which looks like an improvement and is a regression. **(c) Grounding lives in RENDERING, never in the record set** — one catalog record per fix, always; minting a record per user or per finding makes the catalog and its duplicate lint meaningless overnight, so the record declares which spans may be replaced and the renderer fills them. **(d) Degrade per-substitution, and never invent specificity** — an unobserved slot skips its own substitution and keeps that generic span, so a rule can be grounded in its directory even when the tool mix was not observed; an empty field means not observed and is never filled with a plausible-looking value. Mechanical fixes (`deadweight`'s "remove server X", `subagent`'s "set `model:` here") are already concrete by construction and are deliberately left alone — this is specific to rules that describe a habit rather than an edit.
|
|
227
|
+
|
|
228
|
+
**A user's DISMISSAL is a decision and belongs server-side — durable, reversible, and figure-safe.** It used to live in the browser (`localStorage`), which means it was not really recorded: a cleared profile, a second browser or a different machine and every dismissed card came back. It now lands in `dismissed.json` beside the DB, the same lock-free sink the applied/reverted ledgers use — and **not** a DuckDB table, for a structural reason rather than a historical one: `tj rules` is in `no_db_commands` and must answer while `tj serve` holds the write lock, and DuckDB permits one writer OR many readers with no concurrent read-only escape, so a dismissal stored in DuckDB could not be READ by the command that has to honour it whenever the daemon is up. Three properties, each a test. **(a) It suppresses the OFFER only** (Critical Rule 32) — the behaviour happened and cost what it cost, and "not this one" is a statement about our recommendation, not about the user's bill; the record is byte-identical with and without the dismissal except the offer fields. **(b) It must be REVERSIBLE, and durability is exactly what makes that non-negotiable** — a dismissal that cannot be undone trades a card that came back on every browser for one that never comes back anywhere, so the row stays listed carrying its state and `undismiss` restores it. **(c) It resolves through the ONE matcher** (`cost_apply.signature_is_applied`) and states its exclusion filter once, like its two sibling ledgers — a dismissal that did not honour the legacy-signature equivalence would let a card dismissed under one signature reappear under its refinement. Browser-local dismissals are not migrated: `localStorage` is unreadable server-side, so they simply lapse, and nothing in the UI pretends otherwise.
|
|
229
|
+
|
|
230
|
+
**A flag set UPSTREAM of a pass that rewrites the same field is not a flag, it is a suggestion.** `relearn`'s advisory families set `write_offered=False` at proposal construction, and `_apply_write_budget` then assigned `write_offered=decision.offered` from an allocator that knows nothing about advisory — so the withdrawal held in the unit test (which exercised the constructor) and was silently undone on the live daemon. **Unit tests cannot see this class**: they call the stage that sets the field, not the stage that overwrites it. The fix is to keep such a record out of the later pass entirely rather than to set the field twice. Two general lessons. (a) When a value is written at two stages, the LATER one wins and the earlier one is decoration — either remove the earlier write or exclude the record from the later stage. (b) **A defect that survives a green suite is usually a stage-boundary defect**, so verify through the composed path (here `_apply_write_budget` directly) rather than through the constructor, and boot the daemon against a real corpus before believing a behavioural fix shipped. The same live pass found placement's per-destination session counts computed and never serialized — the payload carried only paths, so every consumer reported `sessions: 0` for destinations the resolver had counted correctly, which is the one figure that justifies placing a rule at all.
|
|
231
|
+
|
|
232
|
+
**A gate the user cannot pass is not a gate, it is a dead end — and a ceiling over an AGGREGATE stops being meaningful once the thing it gates can be placed elsewhere.** The write budget's standing ceiling withheld every rule whenever the user's agent files were collectively too large, and told them to compress. Measured on seven real instruction files across six repos, compression returns a ~0.952 prose ratio (much of it whitespace reflow), so the two largest cards this product produces were gated behind a remedy that returns almost nothing. Two fixes, and both generalise. **Name a remedy that works**: these files are long by ACCUMULATION (numbered incident lists, append-only logs), where what frees space is pruning what no longer applies, expiring what was time-bound, and moving file-specific guidance into a path-scoped rule — compression works on prose, and these files are not long because their prose is verbose. **Scope the gate to what it is actually about**: the ceiling now asks whether THIS destination is too large, not whether the corpus is, because one pathological root file was blocking writes into nearly-empty project files with ample headroom. An aggregate gate was correct when there was one destination; placement made it answer a question nobody asked.
|
|
233
|
+
|
|
206
234
|
**Net-of-standing-cost is part of the contract, not an extra.** A fix that WRITES a permanent artifact into the user's agent files is not free: a rung-1 `CLAUDE.md` rule is re-sent on every future session, forever. Any analyzer whose fix is such a write MUST report `past_overspend_*` **net** of that standing cost and park the pre-net figure on a `gross_*` field; a proposal may never claim a saving larger than its net value. The arithmetic lives in exactly one place, `core/optimize/write_budget.py`, which also owns the write budget (ranked, capped, one block per family, no generic-placeholder rule) and reads the agent-file footprint off the `summarize` analyzer so the product can never recommend adding rules to a file the same report says to compress. Two rules that fall out of it and are easy to get wrong: **rung matters** (rung 2 charges only the always-loaded skill frontmatter, rung 3+ hooks/config charge ZERO because they are executed, never sent as prompt text), and **the budget is spent in PER-SESSION tokens** while the saving is netted against the WINDOW total — charging the window total against the budget would shrink it as a user's session count grows, which is backwards. The 30-day session pace both sides are expressed on is defined once in `core/optimize/projection.py`; never re-derive a projection factor locally.
|
|
207
235
|
|
|
208
236
|
**The same load semantics govern READING a file, not just writing one.** "Rung matters" is a statement about how Claude Code delivers a file, so it holds identically when an analyzer PRICES an existing file. `core/summarize/load_semantics.py` is the single source of truth both sides classify through: a `CLAUDE.md` / `.claude/rules/*.md` is resident in full on every call of every loading session, while a `.claude/skills/*/SKILL.md`, `.claude/commands/*.md` or `.claude/agents/*.md` surfaces only its **frontmatter** that way and delivers its **body** on invocation. So the window figure is two separately-observed terms — `always-resident reduction x sessions x reads per session` **+** `on-demand reduction x OBSERVED invocations` — never one term applied to the whole file. Getting this wrong in either direction is a real defect: pricing a whole skill body as always-resident made an uninvoked 160 KB skill library the single most expensive prompt file a user owns and pushed the cross-analyzer token rollup to 115% of the window's billed tokens; pricing an invoked body at zero would deny that a long skill that really runs costs anything. The invocation multiplier is OBSERVED from Claude Code transcripts (`core/summarize/invocations.py`: `Skill` tool calls, typed `<command-name>` slash commands, `Task`/`Agent` `subagent_type`), never assumed — and "invoked zero times" (a measurement, prices the body at zero) is a different statement from "no transcript corpus" (not a measurement; Critical Rule 28 corollary (a) degrades BOTH fields).
|
|
@@ -228,6 +256,7 @@ Post-ingest hooks run after each span is written to DB:
|
|
|
228
256
|
- **`tokenjam/api/deps.py`**: `require_api_key` — FastAPI dependency for optional API key auth on GET endpoints. Only enforced when `api.auth.enabled = true` in config.
|
|
229
257
|
- **`tokenjam/api/routes/`**: One file per resource — `spans.py` (OTLP JSON ingest), `traces.py`, `cost.py`, `cost_compare.py`, `tools.py`, `alerts.py`, `drift.py`, `optimize.py`, `reuse.py` (`GET /api/v1/reuse/clusters` — the Reuse finding plus skeleton-rendering extras `planning_texts` + `pricing_mode`; a dedicated endpoint (not bolted onto `/optimize`) so the per-cluster planner text, which can be many KB, isn't paid for on every Overview poll — #154), `budget.py`, `status.py`, `agents.py`, `metrics.py` (Prometheus text format from DB queries), `version.py` (unauthenticated `GET /health` → `{"status":"ok","version":...}` mounted with no prefix, plus `GET /api/v1/version`; the version is derived at runtime via `importlib.metadata.version("tokenjam")` — no hardcoded literal). **The dollar-bearing read routes (`/cost`, `/cost/compare`, `/optimize`, `/budget`) each return a `framing` block** (see `core/framing.py`) so the web UI renders plan-tier-aware figures without re-deriving the rules in JS. `/optimize` takes `?fast=true` to skip the expensive Trim analyzer (returns `skipped_analyzers`) for the polling Overview; `/cost` returns a window-bucketed `series` for the chart (see Web UI below). **Concurrency:** the sync (`def`) read routes (`/optimize`, `/cost/compare`) run in Starlette's threadpool, so concurrent requests reach the DB from multiple threads. `DuckDBBackend.conn` is a **per-thread DuckDB cursor** (`threading.local`) over one shared database — cursors are independent connections safe for concurrent use, so fan-out callers (the Overview) can fetch in parallel. Fixed in #124 (was a single shared connection that aborted under concurrent access); do not collapse `conn` back to one shared connection object.
|
|
230
258
|
- **`tokenjam/core/summarize/`**: Structure-aware prompt summarization (advisory; new feature). `detect.py` classifies prose vs. structure (fenced/inline code, tags, templates, tables); `candidates.py` (+ `catalog.py`/`estimate.py`) powers the `tj summarize list` scan; `wrap.py` is the pure protect→restore algorithm (wrap each structured span behind an id'd `<tj-keep>` marker, restore verbatim by id — structure is a hard guarantee); `session.py` is the no-scratch `prepare`/`check` lifecycle + staging (re-derives the wrap from the live file + a content hash; persists nothing but the staged result); `apply.py` writes a staged rewrite back to the file (default dry-run; `--go` writes) behind an owner + content-hash + symlink guard, with a gzip backup and `undo`; `backup.py` stores the gzipped original + metadata under `~/.tj/summary/backups/`; `delivery.py` is the CLI's automated rewrite step — `claude -p` (subprocess, timeout-guarded) or the Anthropic API (lazy `httpx` + the user's own `TJ_ANTHROPIC_API_KEY`) — plus the "pays for itself" amortization. Pure domain logic — no `tokenjam.cli`/`tokenjam.api` imports (delivery's API path lazily imports `httpx`, its lone outbound dependency).
|
|
259
|
+
- **`tokenjam/core/rulewrite/`**: The ONE rule-write lifecycle every rule-writing analyzer shares (`plan.py` list, `apply.py` stage/check/apply/undo, `store.py` staging + gzip backups, `types.py` shapes). Pure domain — no `tokenjam.cli`/`tokenjam.api` imports. Reuses `core/summarize/apply`'s guard model and `relearn_apply`'s block renderer rather than reimplementing either; the store persists rendered OUTPUT, never a recipe, so what a reviewer approved in the diff is byte-for-byte what apply writes. Surfaced by `tj rules` and `/api/v1/rules/*`. `core/optimize/rule_placement.py` is the WHERE half — named that, not `placement`, because `placement` is already a registered analyzer name for an unrelated question (Batch API lane, `analyzers/batch_placement.py`; Critical Rule 19).
|
|
231
260
|
- **`tokenjam/mcp/server.py`**: FastMCP stdio server exposing observability data to Claude Code (plus the summarize tools — `list_summarize_candidates`, `summarize_prep`, `summarize_check`, `summarize_apply`, `summarize_undo`; see `core/summarize/`). Uses either a read-only DuckDB connection or HTTP proxy to `tj serve`. Initialized via `init()` from `cmd_mcp.py`.
|
|
232
261
|
- **`tokenjam/cli/main.py`**: Root Click group with global options (`--config`, `--json`, `--no-color`, `--db`, `--agent`, `-v`). Registers all subcommands.
|
|
233
262
|
|
|
@@ -248,13 +277,15 @@ Post-ingest hooks run after each span is written to DB:
|
|
|
248
277
|
|
|
249
278
|
- **`tj summarize`** (`cmd_summarize.py`) — structure-aware prompt summarization (advisory; new feature). `list` scans for prompt files worth summarizing (catalog-default; any scope-widening flag opens it to `*.md`) and estimates the per-call token saving — read-only. `prep`/`check` are the mechanism: `prep` wraps a prompt's structure behind id'd `<tj-keep>` markers and emits it for rewriting (CLI manual/copy, or the MCP `summarize_prep`/`summarize_check` tools for in-session rewrites); `check` re-reads + hash-guards the file, restores every block verbatim by id, and stages the result **only if structure survives** (a hard gate). No scratch state — the file on disk is the source of truth; `summarize` is in `no_db_commands` (config-only). `apply` writes a staged rewrite back to the file — default **dry-run** (it prints the unified diff), `--go` writes — guarded by an owner + content-hash + symlink check and a gzip backup; `undo` restores from that backup (refusing on drift). `prep --via claude-p` (your local `claude -p`, no key) or `prep --via api` (Anthropic with your `TJ_ANTHROPIC_API_KEY` + the required `[summarize] api_model`, reporting a "pays for itself" amortization) runs the rewrite for you in one shot — both still pass the `check` gate before staging.
|
|
250
279
|
|
|
280
|
+
- **`tj rules`** (`cmd_rules.py`, `core/rulewrite/`) — the permanent CLAUDE.md rules the analyzers propose, and WHERE each one goes. `list`/`show` read the proposal cache the optimize pass already wrote (`rules` is in `no_db_commands` — config only, no analyzer sweep, works while `tj serve` holds the DuckDB lock); `stage` renders one diff PER DESTINATION; `check` re-verifies each against the file as it stands now; `apply` writes (default **dry-run**, `--go` writes, gzip backup first); `undo` reverts per destination; `applied` lists what can still be undone. The safety model is `core/summarize/apply`'s, reused rather than reimplemented (owner + content-hash + symlink guards, atomic write, refuse-on-drift undo), and the block rendering is `relearn_apply.render_note_content`'s, so a re-apply replaces its own marked block instead of duplicating it and the existing Revert path can still find it. **Partial outcomes are first-class:** three project files written and a fourth skipped because it drifted is the reported result, never rounded up to "applied". Mirrored at `GET/POST /api/v1/rules/*` and on the Lens **Optimize ▸ Rules** screen (`#/optimize/rules`), which exists because a Review-inbox card can express one write and cannot express "this rule, into these 4 of your 11 projects, here is each diff, apply selectively".
|
|
281
|
+
|
|
251
282
|
- **`tj pricing list`** (`cmd_pricing.py`) — read-only inspection of the resolved model pricing table. Prints one row per `(provider, model)` with `input` / `output` / `cache_read` / `cache_write` rates and a `source` column (`override` / `packaged`) showing which layer the rate resolved from. Supports `--json` and `--model <substring>` (case-insensitive filter). `pricing` is in `no_db_commands` (config-only). The source derivation lives in `core/pricing.load_pricing_sources()` (public) — the CLI reads it rather than re-deriving precedence; a listed row is always `packaged` or `override` (the `DEFAULT_INPUT_PER_MTOK` / `DEFAULT_OUTPUT_PER_MTOK` fallback in `core/pricing.py` applies only to models absent from the table, which never appear here). The `set` half of a future `tj pricing set|list` is not built yet (#282 shipped `list` only).
|
|
252
283
|
|
|
253
284
|
All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist.
|
|
254
285
|
|
|
255
286
|
**CLI testing pattern:** Tests use `click.testing.CliRunner` with `unittest.mock.patch` on `tokenjam.cli.main.load_config` and `tokenjam.cli.main.open_db` to inject an `InMemoryBackend` and test config. See `tests/integration/test_cli.py`. Note: `cmd_doctor` opens its own DuckDB connection via `config.storage.path` to verify writability — in tests you must set this to a real temp path (e.g. `tmp_path / "test.duckdb"`).
|
|
256
287
|
|
|
257
|
-
**`no_db_commands` in `cli/main.py`:** Commands that don't open the DB at startup —
|
|
288
|
+
**`no_db_commands` in `cli/main.py`:** Commands that don't open the DB at startup — read the set in `cli/main.py` rather than trusting an enumeration here. New commands that read only from config (or do their own DB connection later) should be added to this set so they work when `tj serve` holds the write lock. Tests for these commands can patch `open_db` with `side_effect=AssertionError(...)` to verify they never touch the DB.
|
|
258
289
|
|
|
259
290
|
**Test factories:** `tests/factories.py` provides `make_llm_span(billing_account="anthropic", ...)` and `make_session(plan_tier="api", ...)` with safe defaults that preserve existing test behavior. Tests exercising subscription / local / unknown plan-tier rendering paths should pass the field explicitly.
|
|
260
291
|
|
|
@@ -288,7 +319,7 @@ That routing lives behind **one seam, not per-command duck-typing**: `resolve_da
|
|
|
288
319
|
|
|
289
320
|
### Web UI ("TokenJam Lens")
|
|
290
321
|
|
|
291
|
-
`tokenjam/ui/index.html` is the served dashboard — a **single-file Preact + htm SPA** (no build step, no TypeScript, no client-side router). "TokenJam Lens" is the **brand only**: it appears in `<title>`, the sidebar wordmark, and the OpenAPI title, but never in module names, route paths, or config keys. Screens: **Overview** (the default landing route — a triage front door), Status, Traces, Cost, Alerts, Drift, Optimize, Budget.
|
|
322
|
+
`tokenjam/ui/index.html` is the served dashboard — a **single-file Preact + htm SPA** (no build step, no TypeScript, no client-side router). "TokenJam Lens" is the **brand only**: it appears in `<title>`, the sidebar wordmark, and the OpenAPI title, but never in module names, route paths, or config keys. Screens: **Overview** (the default landing route — a triage front door), Status, Traces, Cost, Alerts, Drift, Optimize (with **Summarize** and **Rules** sub-views), Budget.
|
|
292
323
|
|
|
293
324
|
- **Offline-first (Critical Rule 18):** every JS/CSS dep is vendored under `tokenjam/ui/vendor/` — Preact + hooks + htm (ESM via `<script type="importmap">`) and **uPlot** (vendored IIFE global `uPlot` + CSS, pinned in `docs/internal/lens-vendor-versions.md`). No render-time external HTTP. `tests/unit/test_ui_offline.py` enforces this; clickable `<a href>` links are the only allowed external URLs.
|
|
294
325
|
- **Single compute path:** the UI reads everything from the REST API and **never re-implements analysis, aggregation, or plan-tier framing in JS** — it consumes the `framing` block (see `core/framing.py`). If the UI needs a number, extend the endpoint; don't compute it client-side. This applies to gating/filter sets too, not just numbers: if a Python-side map decides which analyzers/findings apply to a persona (e.g. `core/optimize/runner.py`'s `PERSONA_DISABLED_ANALYZERS`), the API already publishes the resolved set (`persona_disabled_analyzers` on `/optimize`) — read that field everywhere the gate is needed, never re-declare the map as a second JS literal. Two call sites deriving the same set from two different sources will silently desync the moment either side edits its copy.
|
|
@@ -297,7 +328,9 @@ That routing lives behind **one seam, not per-command duck-typing**: `resolve_da
|
|
|
297
328
|
- **Charts:** `SpendChart` wraps uPlot, reads CSS custom properties (`--chart-1..5`) so it re-themes, and has a cursor tooltip. The spend chart spans the **full selected window** with zero-fill: `/api/v1/cost` returns a window-bucketed `series` (hourly buckets for ≤2-day windows, daily otherwise; epoch-second `bucket` keys) plus `series_bucket` + `window_start`/`window_end`, and the UI builds a continuous grid + pins the x-scale to the window (issues #133/#136).
|
|
298
329
|
- **Run-rate** is a single linear figure projected to the end of the current calendar cycle (`daily_rate × days-remaining`), captioned "not a forecast". The forecasting boundary is deliberate: linear run-rate only — no EWMA, seasonality, or anomaly detection.
|
|
299
330
|
- **Polling:** the Overview auto-refreshes every 30s only while the tab is visible (`document.visibilityState`) and **fetches its endpoints in parallel** via `Promise.all` (the daemon DB layer is concurrency-safe since #124 — per-thread cursors). The error handling is deliberately asymmetric: `/cost` is load-bearing (no `.catch` — its failure surfaces the error state), while every other panel carries a `.catch` fallback so one failing panel renders empty rather than blanking the Overview. Don't unify them. Detail screens refresh on user action.
|
|
300
|
-
- **
|
|
331
|
+
- **Spacing comes from the `:root` tokens, never from a hand-picked literal.** Every surface here is one of four shapes — a card/section body, a row inside one, a head strip, a table cell — and they share ONE horizontal inset (`--inset-x`) so a table lines up with the panel head above it; the vertical value varies by density (`--pad-card-y` / `--pad-row-y` / `--pad-head-y`), and `--stack-gap` separates stacked containers. Reach for the shared classes before writing a style attribute: `.panel-body` and `.panel-row` inset a panel's children (`.panel` is deliberately padding-free so it can hold a full-bleed table, which is exactly why its children kept being written with an ad-hoc padding, or none at all), `.opt-tile` is an `.opt-section` used as a flex column, and `.empty` + `.compact`/`.err` and `td.empty-cell` cover every not-here state. The failure mode is not ugliness, it is drift: an inline box and the loading skeleton that stands in for it are edited months apart and stop matching, and a re-declared `border`/`border-radius` in a style attribute hides that the class already owned it.
|
|
332
|
+
- **A cell with `overflow-wrap:anywhere` + `min-width:0` has a min-content width of ONE CHARACTER,** so under auto table-layout its column is the first thing the browser sacrifices and it collapses into a vertical stack of letters rather than wrapping. Any such column needs an explicit `min-width` floor. The mirror image is a `td` left at the default `white-space:nowrap` holding long text: that one column then dictates the whole table's width, and sibling tables on the same page come out visibly different widths with only one of them overflowing. Cap it and let it wrap; `.table-wrap` absorbs the remainder. Both bite on small, unrelated changes (a few px of extra cell padding was enough), so re-check wide tables after any spacing edit.
|
|
333
|
+
- **Testing the UI (no JS runner in CI):** the Python `test` job can't run JS. Guard UI work with tests that **execute** something — `test_lens_dashboard_states.py` and `test_lens_select_all_behaviour.py` extract a pure JS helper out of `index.html` and run it under `node`, and `test_ui_offline.py` asserts file-level invariants (no external hosts, no NUL bytes, and that the module script actually parses). **Do not add static-grep tests that assert on literal substrings of `index.html`.** A 344-test file of those was deleted: it pinned wording and markup structure rather than behaviour, so ordinary visual iteration broke assertions that guarded nothing, and one broke merely because an identifier was added to a `useEffect` dependency array it used as a slice anchor. If a UI behaviour is worth guarding, extract the logic into a pure function and execute it. When iterating locally, verify visually by running `tj serve` (or a seeded `create_app` + uvicorn on an alt port) and screenshotting with headless Chrome — there is intentionally no Playwright/Cypress.
|
|
301
334
|
|
|
302
335
|
### Session Continuity
|
|
303
336
|
|
|
@@ -332,7 +365,7 @@ When a span has a `conversation_id` matching an existing session, it's attribute
|
|
|
332
365
|
23. **When removing something that should never have been user-visible, grep the TESTS for it FIRST — a green suite may be ENFORCING the defect, not protecting against it.** An internal tracker id sat in the `tj doctor` MCP-wiring warning, a string the product prints, and `tests/integration/test_cli.py` asserted that the id was PRESENT in `mcp_checks[0]["message"]` — so the suite actively required the leak to be present, and anyone who did the right thing was told by CI that they had broken something. Most people believe CI, which is why it survived so long. A stale comment is entropy; a test asserting the bad state is an ACTIVE DEFENCE of it, and it is invisible until you touch the thing. **The repair is to INVERT the assertion, not to delete it:** the test now asserts the durable content is present (`"+36% measured"`) AND that no bare `#\d+` remains, so the same pin now defends the correct state. Generalise both halves: before deleting any user-visible string, `grep -rn "<the thing>" tests/`; and when a guard has to go, replace it with its inverse rather than removing coverage. `tests/unit/test_no_tracker_ids_in_printed_strings.py` is the standing version of that inverse for this whole class — it walks every non-docstring string literal under `tokenjam/` and fails on an UNQUALIFIED tracker reference, separating real references from CSS colours and ordinals structurally (markup-shaped literals are skipped; a reference needs two or more digits) rather than by an allowlist, because a guard with exceptions just teaches people to add exceptions. **The sharper half of this lesson, learned by getting it wrong:** the first version of that guard banned EVERY reference, and it immediately pushed a scrub into deleting a correct public issue link out of the spans-column-statistics warning — the reference named the real upstream bug the user had just hit, and removing it destroyed true, useful information. Number-sign references are not the problem. **BARE ones are ambiguous** (they resolve against whatever repo the reader is looking at, and our internal and the public number spaces genuinely collide), and **internal ones must never be public**. Those are two different faults with two different fixes: qualify the first (`Metabuilder-Labs/tokenjam#56`, or a full `https://github.com/...` URL, which is what a printed string should carry since a terminal linkifies nothing and a URL survives being pasted anywhere), and describe-and-delete the second. **Decide which fault you have by EVIDENCE, not by the shape of the token:** open the number and ask whether that artifact actually explains the sentence it sits in. It did for the column-statistics warning, whose public issue is exactly about corrupt spans column statistics, so that one was restored qualified; it did not for the MCP-overhead claim or the schema-self-heal warning, whose numbers name our own queue, so those stayed deleted. A guard that cannot tell those two faults apart will cheerfully destroy good information while looking green.
|
|
333
366
|
24. **A surface is reachable only if a USER has a path to it. "The component mounts", "the route resolves", "the code is invoked" and "it is deliberate" each prove nothing on their own.** Static dead-code analysis cannot find this class: ruff and mypy stayed green through every instance below. Four checks, and a surface must pass all of them: **(a) the inverse direction** — not "does this nav item resolve to a view" but "does anything link TO this view". `case 'sessions'` and `case 'runs'` resolved correctly and had no sidebar entry, so `SessionDetailView` and its Map / Approach / Timeline tabs were reachable only by hand-typed URL. An audit that only checked nav→view reported the UI clean. **(b) does the path carry data** — every click-path into a session came from Status rows, and `/api/v1/status` returned empty because its hand-written SQL knew `'active'` and `'closed'` but not the `'completed'` that `core/backfill.py` stamps on every backfilled session, so EVERY session in the DB was filtered out of both zones and there was nothing on screen to click. One literal missing from two WHERE clauses made every surface downstream of it unreachable at once. **(c) does the destination resolve** — the Review inbox rendered example-session links built from transcript filenames (`analyzers/relearn.py`, `session_id = path.stem`), and only the minority that had actually been ingested had a page; the rest 404'd. Links now render only when the session resolves, checked at READ time so already-cached findings self-correct as ingestion catches up. **(d) does a capability have a name a user can type** — `placement` runs, produces a cost card and has a verify branch, but is absent from `tj optimize`'s Click choices, so it is invisible as a capability and only executes as a side effect of `downsize`. Deliberate design and documentation are NOT a defense against invisibility; the `sessions` view was deliberate too. **The inverse trap, equally real:** absence of a `@register` decorator is not evidence of death. `batch_placement` and `downsize_agents` are invoked by `model_downgrade.py` and documented there, and an auditor applying "no decorator means dead" called a live analyzer dead. **The question that resolves both directions is the same one: what can a user actually do?** Not what the code does. Ask it in both directions before deleting anything or declaring anything fine.
|
|
334
367
|
25. **Removing a feature strands its helper modules — grep every function in the modules it used, not just the files that named it.** When the output-cap hook, its `tj savings` CLI, `core/output_cap.py`, and their tests were deleted, `core/savings_log.py` was left behind untouched: 8 of its 9 functions had zero callers anywhere in the repo, including tests, and its docstring still described `tj hook cap-output`, `tj savings`, and an A/B harness that no longer existed. Only `hooks_dir` was still consumed (by `core/recommendations.py`). The durable rule: after deleting a feature, grep every function in its supporting modules for callers before deciding what happens to the module, and treat a docstring that still describes deleted commands as a live defect, not a stale comment — it actively misleads the next reader. A helper module can legitimately outlive the feature it was built for, but only if it's trimmed to what's still consumed and re-documented for its remaining consumer.
|
|
335
|
-
26. **An analyzer that has no fix for the window's dominant persona must never RUN — see `PERSONA_DISABLED_ANALYZERS` in `core/optimize/runner.py`.** If you are wondering why some analyzer is missing from a report on an interactive coding-agent window, this is why, and it is deliberate — the
|
|
368
|
+
26. **An analyzer that has no fix for the window's dominant persona must never RUN — see `PERSONA_DISABLED_ANALYZERS` in `core/optimize/runner.py`.** If you are wondering why some analyzer is missing from a report on an interactive coding-agent window, this is why, and it is deliberate — the gated sets live in `PERSONA_DISABLED_ANALYZERS` in `runner.py` (keyed by persona), where every entry carries an inline comment naming the gate it fails. Read the map; do NOT maintain a second copy of that list in this file, because a doc list drifts from the map silently and then reads as a claim about which analyzers run. Two different "disabled" mechanisms exist in this tree and they must not be conflated: a **fake skip** still runs, still queries, and returns an `enabled: False` finding (what `trim` and `cache-recommend` do when their prerequisites are off), whereas a **true skip** drops the analyzer from the selection before dispatch so it is never invoked at all (what `fast=true` already did for expensive analyzers). The persona gate is a TRUE skip: `build_report` resolves the persona at the top, subtracts `disabled_analyzers_for_persona(persona)` from the selected set right after validation, and the dispatch loop below never sees the name. Three consequences to respect. **(a) The bar is three gates, not one.** Keeping an analyzer requires all of: its output ends in a concrete edit to a file or setting this persona actually controls; the user is better off NET of the fix's own standing cost (a rule written into an always-loaded instruction file is re-sent on every future session, forever, so a 200-token rule that prevents a 1,500-token mistake twice a month is net negative); and the saving does not come from making the agent terser or dumber. "A fix could be described in a text field" is not a bridge to a real edit, and it is the mistake that put every currently-gated analyzer on screen in the first place. **(b) Mirror every gate into `COST_ANALYZERS` (`core/optimize/cost_proposals.py`), which is an INDEPENDENT second analyzer-selection surface** feeding the Review inbox: use `cost_analyzers_for_persona`, and note that `cost_proposals_from_report` gates its adapter table off the same map so a report built elsewhere still can't produce an unapplyable card. Skipping the mirror is how a "disabled" analyzer keeps surfacing findings in Review while looking disabled everywhere else. **(c) A sub-check attached by another analyzer needs its own gate** — `placement` is produced inside `downsize`, not registered under its own name, so the selection gate cannot reach it and `model_downgrade.run` consults the same map itself. Adding a persona-disabled name is therefore three edits, not one, and each entry carries an inline comment stating WHICH gate it fails. Only a window we can positively classify may lose an analyzer: a persona with no key in the map disables nothing. Do not assume which personas have a key — read the map, since more than one does and this sentence is exactly where a stale census would sit. Finally, a disabled analyzer VANISHES; it never renders a placeholder row. The one exception is a name the user typed explicitly, which gets a single "not run, and here is why" line, because silence in response to a typed command reads as a bug.
|
|
336
369
|
27. **Two analyzers that both claim `past_overspend_*` must draw from DISJOINT spans — the rollup cannot save you.** `past_overspend_rollup` (`core/optimize/cost_proposals.py`) dedupes by EXACT signature string, so `cost:downsize:<agent>` and `cost:subagent:<name>` are structurally distinct and both survive even when they price the identical tokens. That is precisely how `downsize` (aggregated per `session_id`) and `subagent` (per `(session_id, sub_agent_id)`) came to double-count a Task dispatch: Claude Code stores a subagent's turns under its PARENT session id, so an unfiltered `GROUP BY session_id` silently absorbs them — and `MODE(model)` could even name a subagent's model as the session's dominant one. The rule for the next analyzer: **before adding a recoverable figure, name the exact span population it claims and prove no shipped analyzer already claims those rows.** Two remedies exist, both in-tree; prefer the first. **(a) Make it disjoint at the source** — `analyze_model_downgrade` filters `sub_agent_id IS NULL`, so main-thread and subagent tokens partition the session. Use this when the overlap is a column filter away; it fixes the derived shape (`MODE(model)`, per-turn tool counts) too, not just the sum. **(b) Subtract what the more specific card already claimed** — `_per_agent_cache_recoverable_by_model` does this for the `cache` family, where the generic per-`(provider, model)` row must stay window-wide by definition and cannot be narrowed. Two traps: **only the CLAIM narrows, never the DENOMINATOR** — `percent_of_sessions` / `percent_of_tokens` are shares of the user's whole window, so scoping their denominator to the same subset would silently inflate both (hence the separate unfiltered totals query); and **any share computed against a narrowed figure has to narrow too** — `thinking_tokens_by_session(..., main_thread_only=True)` exists because a whole-session thinking numerator over a main-thread-only output denominator can exceed 1.0. Pin it with an end-to-end test that runs `build_report` → `cost_proposals_from_report` → `past_overspend_rollup` and asserts the token sum equals the seeded session's real total (`tests/unit/test_rollup_subagent_downsize_dedup.py`); a per-analyzer unit test cannot see an overlap that only exists between two analyzers. The stakes are asymmetric: the product's stance is to show the LARGEST number the derivation legitimately supports, which makes a double-count the one failure that turns a big number into an indefensible one. **A third remedy joined these two when `downsize` grew its driver-role case: (c) partition the session POPULATION behind one shared predicate.** `resend` and `downsize` price the identical mechanism (material re-read by every later main-thread turn), so no column filter separates them — what separates them is *which sessions* each may claim. `analyzers/resend_tail.premium_driver_role` is that partition, imported by both, and the shared tail arithmetic lives in the same module so neither side can reimplement it slightly differently. Use this when two analyzers legitimately measure the same thing about different populations: a single predicate that both call cannot drift, whereas two independently-tuned threshold sets silently will.
|
|
337
370
|
28. **A finding's `past_overspend_tokens` and `past_overspend_usd` must count the SAME events — divide one by the other and the implied per-token rate has to land inside a real price band.** Rule 27 covers two analyzers claiming the same spans; this is the same failure one level down, INSIDE a single finding, and it is the harder one to see because both fields are individually defensible. `summarize` priced its dollars per-call-multiplied (reduction x loading sessions x reads per session, all documented in `estimate_basis`) while leaving its token field on the un-multiplied one-time file reduction — so the card carried a small token count beside a large dollar figure, and dividing them implied a per-token rate orders of magnitude above any rate any provider charges. Nothing was wrong with either number in isolation; they just answered different questions, and the cross-analyzer rollup sums the token fields, so one analyzer's basis leak silently corrupts the product's token floor. The deadweight per-session-vs-per-call bug was this same class. **The check is mechanical, so write it as the test:** assert `usd / tokens` equals the blend the basis string advertises, and assert it stays between the cache-read rate and the input rate. A hardcoded-number assertion passes happily while both fields drift; a rate assertion cannot, because a basis mismatch always throws the implied rate orders of magnitude out of band. Two corollaries. **(a) The "no evidence" degrade has to be symmetric** — if the load count could not be observed, BOTH fields go `None`, never a zero on one side and a number on the other; a candidate contributes to both sums or neither. **(b) If a second basis is genuinely wanted (the curate/diff UI needs the one-time per-file reduction), give it its OWN named field** — `file_reduction_tokens` — and say so in `estimate_basis`. Never overload the aggregate field that cross-analyzer rollups read. Also re-check every derived percentage after a basis change: `reduction_pct` is saved ÷ source tokens, so it must keep the one-time numerator or it reads as >100% the moment sessions and calls multiply in.
|
|
338
371
|
29. **Before "fixing" an analyzer that looks inert, measure whether its gate is unreachable or merely selects worthless sessions — the two have opposite fixes.** `downsize` was contributing a trivial slice of the Review-inbox rollup, and the ticket's hypothesis was that its small-session gate (the `SMALL_INPUT_TOKENS` / `SMALL_OUTPUT_TOKENS` / `SMALL_TOOL_CALLS` ceilings in `model_downgrade.py`) was structurally unreachable for a Claude Code session. Measured on a real corpus, it was not: **a meaningful minority of CC sessions cleared the gate** — they just carried a negligible share of main-thread spend, and only part of even that had a cheaper same-family target at all. The gate fires fine; small sessions are just cheap. Loosening it (the reflex fix for an "unreachable" gate) would have produced MORE near-worthless cards, against the standing don't-fill-the-inbox constraint, while changing the dollars by nothing. The real gap was a question nobody asked: a premium model acting as the DRIVER of a long undelegated session, where the waste GROWS with session size and so is structurally invisible to any summed-per-session gate — *the worse the waste, the less eligible the session became.* Two general lessons. **(a) An inert-analyzer ticket's first deliverable is the gate-clearance measurement, not a rewrite** — sessions-cleared and share-of-spend are different numbers and only the second one tells you whether to touch the threshold. **(b) When a metric is summed per session, waste that compounds with session length inverts the gate.** Check for that inversion before assuming a threshold is miscalibrated.
|
|
@@ -356,6 +389,12 @@ When a span has a `conversation_id` matching an existing session, it's attribute
|
|
|
356
389
|
|
|
357
390
|
38. **Do not assume a telemetry source carries an id for the thing it describes — check, then derive the key from stored COLUMNS when it does not.** Cross-observer accounting needs a name for the underlying API call, and the obvious design is to stamp the provider's own response id on both sides. That works wherever the observer sees the provider's response — `sdk/integrations/anthropic.py` stamps `gen_ai.response.id`, `core/backfill.py` stamps `tj.call_id` off the assistant message key. It does not work for the pairing that matters most on this product's dominant corpus: **Claude Code's own OTel log exporter emits no per-call identifier at all.** Read `ClaudeCodeEvents` in `otel/semconv.py` for what its `api_request` event actually carries, and confirm there before designing around an id — the absence is the kind of premise that silently invalidates a whole approach. A stamp only ONE side of a pair can carry is worse than no stamp: it yields two different keys for one call, so the pairing never fires and nothing looks broken. **Derive the cross-observer key from columns the store already holds rather than from an attribute an ingest path promises to write** — that also makes it readable on rows written before any stamping existed, which is what lets `tj doctor --repair` clean a database an older build already doubled. Keep stamping the ids that DO exist: they are exact where a derived key is only strong, and they are what a future exporter revision would let you key on directly. If you are about to rely on the absence, re-verify the exporter's attribute set rather than trusting this line.
|
|
358
391
|
|
|
392
|
+
39. **A flag set UPSTREAM of a pass that rewrites the same field is not a flag, it is a suggestion — and a unit test on the constructor cannot see it.** An analyzer correctly set `advisory_only` on a family and cleared its write offer at construction; a later pass, `_apply_write_budget`, then assigned `write_offered` from an allocator that knew nothing about the flag and quietly restored the offer. Every value on the field was individually correct at the moment it was written, and the last writer won. The unit test passed the whole time because it exercised the CONSTRUCTOR path directly while the daemon ran the COMPOSED path — the test was structurally incapable of observing the defect, so a green suite was evidence of nothing. **The fix shape is not a second guard in the later pass** (that is a race between two writers, which the next pass re-opens); it is to **remove the item from the INPUT the later pass consumes**, so no decision exists to overwrite. Here the advisory family never becomes a `WriteCandidate` at all. Two rules follow. *(a) When you set a field that a downstream pass also writes, either withdraw the row from that pass's input or move the decision into it — never both write it.* Grep for other assignments to the field before assuming your assignment is the final one. *(b) A test that constructs the object under test proves the constructor works and says nothing about the pipeline.* Assert through the composed path a user actually reaches (build the report, run the budget pass, then read the field), which is the same lesson Critical Rule 36 draws for repair passes.
|
|
393
|
+
|
|
394
|
+
40. **A fixture that is a real domain object can stop being a valid vehicle for what it tests while every assertion still passes — for the wrong reason.** Eight fixtures seeded one particular relearn failure family in order to exercise write-budget behaviour. That family later became advisory-only, so it can no longer demonstrate ANY budget behaviour at all: it is never offered, never priced, never ranked. The tests stayed green, because the assertions they made happened to hold vacuously on a family that had left the population under test. This happened twice on the same branch, with two different families. The hazard is specific to fixtures that are real domain objects rather than stubs: **changing a domain object's semantics silently invalidates every test that used it as scaffolding, and the suite reports success either way.** Three habits. *(a) When you change what a family, analyzer or kind MEANS, grep the tests for its name and re-read each use — asking not "does this still pass" but "can this fixture still exhibit the behaviour the test claims to check".* *(b) Prefer a fixture that would FAIL loudly if it stopped qualifying* — assert the precondition explicitly (`assert cluster.write_offered`) before asserting the behaviour, so the vehicle's own validity is pinned rather than assumed. *(c) When you do have to swap the fixture, say in the docstring which object it replaced and why the old one stopped qualifying* — that note is what stops the next reader from "restoring" it.
|
|
395
|
+
41. **A `list[Any]` field loses its TYPE across a round trip while its VALUE survives intact — and a broad `except` downstream turns that into a whole analyzer silently missing from a published total.** `hydrate_dataclass` dispatches on the annotation, so a field declared `list[Any]` to keep `core/optimize/types.py` free of an analyzer import gives it nothing to work with: the rows come back as plain dicts, correctly shaped and correctly numbered, with the class gone. Nothing fails at the seam. The failure surfaces far away, as `'dict' object has no attribute ...` inside whichever consumer reads a row attribute — and if that consumer sits under a per-item `except Exception: continue`, the item is dropped and the total it fed just gets smaller. That is how one analyzer's entire contribution disappeared from the Review inbox while the Dashboard tile, reading the finding directly and never round-tripping, went on publishing it: two surfaces, one analyzer, no error anywhere. Three rules. **(a) Declare the element type as DATA when you cannot import it**: `field(metadata={"hydrate": "package.module:ClassName"})`, resolved lazily at hydration time (`runner._hydrate_target`), so the layering stays intact and the round trip is lossless in type as well as value. A test asserts every `list[Any]` in `types.py` carries it. **(b) A round-trip test that compares VALUES cannot see this** — it passes on dicts, because the numbers really did survive. Assert `isinstance` of the rehydrated row, not just its fields. **(c) A broad `except` around a per-item transform must record WHAT it skipped**, and the caller must publish that as a known-unknown rather than absorbing it: a skipped item is a hole in every figure summed from the result, and a null-figure disclosure through the `excluded` channel is the difference between a total that says "this analyzer is missing" and one that quietly reads as complete. Continuing on error is right; continuing without a trace is what makes the arithmetic lie.
|
|
396
|
+
|
|
397
|
+
|
|
359
398
|
## Config
|
|
360
399
|
|
|
361
400
|
Config is TOML, discovered at: `tj.toml` -> `.tj/config.toml` -> `~/.config/tj/config.toml`. Override with `--config` or `TJ_CONFIG` env var. Full config hierarchy is in `tokenjam/core/config.py` (`TjConfig` dataclass).
|
|
@@ -437,8 +476,9 @@ The packaged table stays the zero-config default — the override is a *layer*,
|
|
|
437
476
|
## CI
|
|
438
477
|
|
|
439
478
|
GitHub Actions workflow at `.github/workflows/ci.yml` runs on push/PR to `main`:
|
|
440
|
-
- **`
|
|
441
|
-
- **`test
|
|
479
|
+
- **`lint`** job: Python 3.12 — `ruff check tokenjam/` and `mypy tokenjam/`
|
|
480
|
+
- **`test`** job: Python 3.10/3.11/3.12 matrix — `pytest tests/unit/ tests/synthetic/ tests/agents/ tests/integration/`
|
|
481
|
+
- **`test-ts`** job: Node 22 — `npm install && npm test` in `sdk-ts/`
|
|
442
482
|
|
|
443
483
|
All steps are blocking — lint, typecheck, and tests must pass for CI to go green.
|
|
444
484
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenjam
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: TokenJam: a local-first, OTel-native cost-saving utility for AI agents
|
|
5
5
|
Project-URL: Homepage, https://tokenjam.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/Metabuilder-Labs/tokenjam
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
The fastest way onto TokenJam is to install it and run the onboarding wizard for your agent; that
|
|
4
4
|
wires up everything (statusline, backfill, dashboard) in one step. This page walks every entry path
|
|
5
|
-
by persona, most-common first; each
|
|
5
|
+
by persona, most-common first; each step ends with a way to verify it actually worked before you move
|
|
6
6
|
on.
|
|
7
7
|
|
|
8
8
|
> **Just looking first?** `npx tokenjam` prints a 15-second read-only report over the Claude Code logs
|
|
9
9
|
> you already have on disk (`~/.claude/projects/*.jsonl`); nothing installed, nothing written to disk,
|
|
10
|
-
> no daemon. You should see
|
|
11
|
-
> Code logs found," which just means you haven't run Claude Code on this machine yet). See
|
|
10
|
+
> no daemon. You should see one line telling you how much of your recent sessions was avoidable (or
|
|
11
|
+
> "No Claude Code logs found," which just means you haven't run Claude Code on this machine yet). See
|
|
12
12
|
> [docs/installation.md](installation.md) for the `uv`/`pipx` runner requirements. When you're ready to
|
|
13
|
-
> keep it, start at
|
|
13
|
+
> keep it, start at step 1 below.
|
|
14
14
|
|
|
15
15
|
## 1. Claude Code / Codex onboarding wizards
|
|
16
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenjam",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Zero-install launcher for TokenJam (tj): npx tokenjam runs the Python CLI via uvx/pipx and reports the recurring mistakes your AI agent keeps repeating, no setup required.",
|
|
5
5
|
"keywords": ["claude-code", "ccusage", "tokens", "cost-optimization", "llm", "agents", "observability", "tokenjam", "anthropic", "claude", "token-usage", "agent-behavior", "cli", "statusline", "opentelemetry", "agent-observability"],
|
|
6
6
|
"homepage": "https://tokenjam.dev",
|
|
@@ -12,7 +12,7 @@ from click.testing import CliRunner
|
|
|
12
12
|
|
|
13
13
|
from tokenjam.cli.main import cli
|
|
14
14
|
from tokenjam.core.config import AgentConfig, BudgetConfig, TjConfig
|
|
15
|
-
from tokenjam.core.db import InMemoryBackend
|
|
15
|
+
from tokenjam.core.db import SPANS_INDEX_SQL, InMemoryBackend
|
|
16
16
|
from tokenjam.core.models import (
|
|
17
17
|
AgentRecord,
|
|
18
18
|
Alert,
|
|
@@ -473,6 +473,12 @@ def _duckdb_with_dropped_migration_7(tmp_path):
|
|
|
473
473
|
backend.conn.execute(f"DROP INDEX IF EXISTS {idx}")
|
|
474
474
|
backend.conn.execute("ALTER TABLE spans DROP COLUMN request_params")
|
|
475
475
|
backend.conn.execute("ALTER TABLE spans DROP COLUMN request_tools")
|
|
476
|
+
# DuckDB refuses to drop a column while the table carries ART indexes, so
|
|
477
|
+
# the indexes above come off first — and have to go straight back on. Left
|
|
478
|
+
# off, this fixture would additionally reproduce the SEPARATE fault doctor's
|
|
479
|
+
# index-integrity check exists for, and these tests would be asserting an
|
|
480
|
+
# exit code that has nothing to do with the missing column they are about.
|
|
481
|
+
backend.conn.execute(SPANS_INDEX_SQL)
|
|
476
482
|
return backend
|
|
477
483
|
|
|
478
484
|
|
|
@@ -11,6 +11,8 @@ from datetime import timedelta
|
|
|
11
11
|
import httpx
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
|
+
from tokenjam.core.rulewrite.kinds import DELIVERY_SKILL
|
|
15
|
+
|
|
14
16
|
from tokenjam.api.app import create_app
|
|
15
17
|
from tokenjam.core.config import ApiAuthConfig, ApiConfig, StorageConfig, TjConfig
|
|
16
18
|
from tokenjam.core.db import InMemoryBackend
|
|
@@ -128,6 +130,73 @@ async def test_mark_cost_applied_round_trip(app, client):
|
|
|
128
130
|
assert rev.json()["state"] == "reverted"
|
|
129
131
|
|
|
130
132
|
|
|
133
|
+
# --- an applied proposal loses its OFFER and keeps its FIGURE ---------------- #
|
|
134
|
+
|
|
135
|
+
async def test_an_applied_proposal_is_withdrawn_on_the_PAYLOAD(app, client):
|
|
136
|
+
"""Verified WITHOUT consulting a second endpoint — that is the whole point.
|
|
137
|
+
|
|
138
|
+
THE DEFECT. This route computed a filtered ``open_proposals`` for its rollup
|
|
139
|
+
and then returned the UNFILTERED ``proposals`` list, and the rows carried no
|
|
140
|
+
``applied``/``applied_at`` field at all. So an already-applied proposal went
|
|
141
|
+
out advertising ``apply_capable: true`` with nothing on it to say otherwise.
|
|
142
|
+
Only the browser was safe, and only because it independently re-fetches
|
|
143
|
+
``/relearn/cost-applied`` and filters client-side — meaning the CLI,
|
|
144
|
+
``--json``, an export and every future surface saw an offer to re-apply
|
|
145
|
+
something already done, and had to know to cross-reference a second endpoint
|
|
146
|
+
to avoid it. Measured on a real corpus: the ONE apply-capable row in the
|
|
147
|
+
whole inbox was a fix that had already been applied.
|
|
148
|
+
|
|
149
|
+
Critical Rule 32: the offer is withdrawn, the figure is kept, and the row
|
|
150
|
+
stays listed CARRYING an applied state.
|
|
151
|
+
"""
|
|
152
|
+
token = app.state.relearn_write_token
|
|
153
|
+
hdr = {"X-TJ-Local-Token": token}
|
|
154
|
+
await client.post("/api/v1/relearn/cost-proposals/refresh", headers=hdr)
|
|
155
|
+
before = (await client.get("/api/v1/relearn/cost-proposals")).json()["proposals"]
|
|
156
|
+
prop = next(p for p in before if p["analyzer"] == "cache")
|
|
157
|
+
assert prop["applied"] is False, "every row carries the field, open ones included"
|
|
158
|
+
assert prop["applied_at"] is None
|
|
159
|
+
|
|
160
|
+
applied = await client.post(
|
|
161
|
+
"/api/v1/relearn/cost-proposals/apply",
|
|
162
|
+
json={"proposal_id": prop["proposal_id"]}, headers=hdr,
|
|
163
|
+
)
|
|
164
|
+
assert applied.status_code == 200
|
|
165
|
+
|
|
166
|
+
after = (await client.get("/api/v1/relearn/cost-proposals")).json()["proposals"]
|
|
167
|
+
row = next(p for p in after if p["signature"] == prop["signature"])
|
|
168
|
+
assert row["applied"] is True
|
|
169
|
+
assert row["applied_at"], "an applied row must carry WHEN"
|
|
170
|
+
assert row.get("apply_capable") is not True, (
|
|
171
|
+
"an applied fix is still being offered for application"
|
|
172
|
+
)
|
|
173
|
+
# The figure is what the behaviour ALREADY cost. Applying the fix afterwards
|
|
174
|
+
# does not un-spend the money, and a gate that edits a past figure is the
|
|
175
|
+
# "this was free" conflation Critical Rule 32 exists to stop.
|
|
176
|
+
assert row["past_overspend_usd"] == prop["past_overspend_usd"]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def test_a_reverted_proposal_is_offered_again_on_the_payload(app, client):
|
|
180
|
+
"""A revert is the user saying the fix is no longer in place."""
|
|
181
|
+
token = app.state.relearn_write_token
|
|
182
|
+
hdr = {"X-TJ-Local-Token": token}
|
|
183
|
+
await client.post("/api/v1/relearn/cost-proposals/refresh", headers=hdr)
|
|
184
|
+
proposals = (await client.get("/api/v1/relearn/cost-proposals")).json()["proposals"]
|
|
185
|
+
prop = next(p for p in proposals if p["analyzer"] == "cache")
|
|
186
|
+
rec = (await client.post(
|
|
187
|
+
"/api/v1/relearn/cost-proposals/apply",
|
|
188
|
+
json={"proposal_id": prop["proposal_id"]}, headers=hdr,
|
|
189
|
+
)).json()
|
|
190
|
+
await client.post(
|
|
191
|
+
f"/api/v1/relearn/cost-applied/{rec['id']}/revert", headers=hdr,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
after = (await client.get("/api/v1/relearn/cost-proposals")).json()["proposals"]
|
|
195
|
+
row = next(p for p in after if p["signature"] == prop["signature"])
|
|
196
|
+
assert row["applied"] is False
|
|
197
|
+
assert row["applied_at"] is None
|
|
198
|
+
|
|
199
|
+
|
|
131
200
|
# --- the marker's numbers come from the STORE, never from the caller -------- #
|
|
132
201
|
|
|
133
202
|
async def test_mark_applied_refuses_a_caller_supplied_estimate(app, client, config):
|
|
@@ -198,7 +267,7 @@ async def test_apply_workspace_refuses_a_caller_supplied_proposed_fix(
|
|
|
198
267
|
|
|
199
268
|
|
|
200
269
|
async def test_cost_apply_workspace_writes_note_and_records_marker(app, client, db, monkeypatch, tmp_path):
|
|
201
|
-
"""A CC-origin subagent proposal routes a reversible
|
|
270
|
+
"""A CC-origin subagent proposal routes a reversible CLAUDE.md rule through the
|
|
202
271
|
existing relearn apply path, then records the cost marker for delta-verify."""
|
|
203
272
|
from tokenjam.core.optimize import relearn_apply as pa
|
|
204
273
|
|
|
@@ -253,17 +322,17 @@ async def test_cost_apply_workspace_writes_note_and_records_marker(app, client,
|
|
|
253
322
|
assert any(r["analyzer"] == "subagent" for r in applied["applied"])
|
|
254
323
|
|
|
255
324
|
|
|
256
|
-
async def
|
|
325
|
+
async def test_cost_apply_workspace_writes_skill_for_a_skill_proposal_and_reverts(
|
|
257
326
|
app, client, config, db, monkeypatch, tmp_path,
|
|
258
327
|
):
|
|
259
328
|
"""`apply-workspace` is generic across analyzers, not special-cased to
|
|
260
|
-
`subagent`: a
|
|
329
|
+
`subagent`: a skill-note proposal routes through the SAME path,
|
|
261
330
|
writes, and reverts cleanly.
|
|
262
331
|
|
|
263
332
|
Two facts are pinned here. First, a deterministic tool-call cluster on a
|
|
264
333
|
claude-code window produces NO `script` card any more: the analyzer has no
|
|
265
334
|
fix that survives for that persona and is skipped before it runs. Second,
|
|
266
|
-
the generic
|
|
335
|
+
the generic skill write/revert route still works, exercised by seeding the
|
|
267
336
|
proposal the adapter builds directly into the store."""
|
|
268
337
|
from tokenjam.core.optimize import relearn_apply as pa, relearn_store
|
|
269
338
|
from tokenjam.core.optimize.analyzers.workflow_restructure import (
|
|
@@ -322,7 +391,7 @@ async def test_cost_apply_workspace_writes_skill_for_a_rung2_proposal_and_revert
|
|
|
322
391
|
prop = script_props[0]
|
|
323
392
|
assert prop["apply_capable"] is True
|
|
324
393
|
assert prop["advise_only"] is False
|
|
325
|
-
assert prop["
|
|
394
|
+
assert prop["delivery"] == DELIVERY_SKILL
|
|
326
395
|
|
|
327
396
|
body = {"proposal_id": prop["proposal_id"], "target_path": str(target)}
|
|
328
397
|
|
|
@@ -212,11 +212,76 @@ def test_delete_spans_before_cutoff(db):
|
|
|
212
212
|
db.insert_span(span_new)
|
|
213
213
|
|
|
214
214
|
cutoff = now - timedelta(days=90)
|
|
215
|
-
|
|
216
|
-
assert
|
|
215
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(cutoff)
|
|
216
|
+
assert spans_deleted == 1
|
|
217
217
|
|
|
218
218
|
remaining = db.conn.execute("SELECT COUNT(*) FROM spans").fetchone()
|
|
219
219
|
assert remaining[0] == 1
|
|
220
|
+
# The session straddles the cutoff and still has a live span, so it stays:
|
|
221
|
+
# a session is orphaned by the delete only once it has NO spans left.
|
|
222
|
+
assert sessions_deleted == 0
|
|
223
|
+
assert db.conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] == 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_delete_spans_before_cutoff_takes_the_sessions_it_orphans(db):
|
|
227
|
+
"""Deleting only spans left the parent sessions asserting a day had data.
|
|
228
|
+
|
|
229
|
+
`core/data_span` unions `sessions.started_at` into the day set it measures
|
|
230
|
+
the available span from, so every orphan went on claiming a day carried data
|
|
231
|
+
after the data for that day was destroyed — the deletion skewed the measure
|
|
232
|
+
of what survived it.
|
|
233
|
+
"""
|
|
234
|
+
_insert_agent(db)
|
|
235
|
+
now = utcnow()
|
|
236
|
+
old = now - timedelta(days=100)
|
|
237
|
+
|
|
238
|
+
aged_out = make_session(started_at=old, ended_at=old + timedelta(minutes=1))
|
|
239
|
+
db.upsert_session(aged_out)
|
|
240
|
+
db.insert_span(make_llm_span(session_id=aged_out.session_id, start_time=old))
|
|
241
|
+
|
|
242
|
+
live = make_session()
|
|
243
|
+
db.upsert_session(live)
|
|
244
|
+
db.insert_span(make_llm_span(
|
|
245
|
+
session_id=live.session_id, start_time=now - timedelta(days=1),
|
|
246
|
+
))
|
|
247
|
+
|
|
248
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(now - timedelta(days=90))
|
|
249
|
+
assert (spans_deleted, sessions_deleted) == (1, 1)
|
|
250
|
+
|
|
251
|
+
surviving = [
|
|
252
|
+
r[0] for r in db.conn.execute("SELECT session_id FROM sessions").fetchall()
|
|
253
|
+
]
|
|
254
|
+
assert surviving == [live.session_id]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_delete_spans_before_cutoff_takes_an_aged_out_session_with_no_spans(db):
|
|
258
|
+
"""A pre-cutoff session with no spans is aged-out history like any other.
|
|
259
|
+
|
|
260
|
+
Left behind it would go on asserting, to `core/data_span`, that a day
|
|
261
|
+
beyond the retention horizon carried data.
|
|
262
|
+
"""
|
|
263
|
+
_insert_agent(db)
|
|
264
|
+
now = utcnow()
|
|
265
|
+
empty = make_session(
|
|
266
|
+
started_at=now - timedelta(days=100),
|
|
267
|
+
ended_at=now - timedelta(days=100) + timedelta(minutes=1),
|
|
268
|
+
)
|
|
269
|
+
db.upsert_session(empty)
|
|
270
|
+
|
|
271
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(now - timedelta(days=90))
|
|
272
|
+
assert (spans_deleted, sessions_deleted) == (0, 1)
|
|
273
|
+
assert db.conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] == 0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_delete_spans_before_cutoff_keeps_a_live_session_with_no_spans_yet(db):
|
|
277
|
+
"""An open session that has not written a span is not aged out."""
|
|
278
|
+
_insert_agent(db)
|
|
279
|
+
now = utcnow()
|
|
280
|
+
db.upsert_session(make_session(started_at=now, ended_at=None))
|
|
281
|
+
|
|
282
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(now - timedelta(days=90))
|
|
283
|
+
assert (spans_deleted, sessions_deleted) == (0, 0)
|
|
284
|
+
assert db.conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] == 1
|
|
220
285
|
|
|
221
286
|
|
|
222
287
|
# -- Traces --
|
|
@@ -134,8 +134,12 @@ def full_stack():
|
|
|
134
134
|
ocw_exporter = TjSpanExporter(pipeline)
|
|
135
135
|
|
|
136
136
|
# Create a local TracerProvider (not global) and bind the SDK tracer to it
|
|
137
|
+
# for the duration of this test. Restored in teardown below so a stale
|
|
138
|
+
# tracer bound to this test's (soon-to-be-shutdown) provider and closed DB
|
|
139
|
+
# doesn't leak into tests that run later in the same session (#615).
|
|
137
140
|
provider = TracerProvider()
|
|
138
141
|
provider.add_span_processor(SimpleSpanProcessor(ocw_exporter))
|
|
142
|
+
original_tracer = agent_mod._tracer
|
|
139
143
|
agent_mod._tracer = provider.get_tracer("tokenjam.sdk")
|
|
140
144
|
|
|
141
145
|
# Seed agent records
|
|
@@ -155,6 +159,7 @@ def full_stack():
|
|
|
155
159
|
|
|
156
160
|
yield stack
|
|
157
161
|
|
|
162
|
+
agent_mod._tracer = original_tracer
|
|
158
163
|
provider.shutdown()
|
|
159
164
|
db.close()
|
|
160
165
|
|