bantamkit 0.35.1__tar.gz → 0.35.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {bantamkit-0.35.1 → bantamkit-0.35.3}/.gitignore +1 -0
  2. {bantamkit-0.35.1 → bantamkit-0.35.3}/PKG-INFO +4 -4
  3. {bantamkit-0.35.1 → bantamkit-0.35.3}/README.md +2 -2
  4. bantamkit-0.35.3/_assets/tools/shiftwork_clock_out.json +150 -0
  5. bantamkit-0.35.3/_assets/tools/shiftwork_plan.json +25 -0
  6. {bantamkit-0.35.1 → bantamkit-0.35.3}/pyproject.toml +6 -1
  7. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/__init__.py +1 -1
  8. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/mcpserver.py +32 -0
  9. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/memory/store.py +2 -2
  10. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/selfupdate.py +89 -0
  11. bantamkit-0.35.3/src/bantamkit/updatecheck.py +340 -0
  12. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/served-tool-surface.json +65 -10
  13. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_mcpserver.py +22 -0
  14. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_selfupdate.py +257 -1
  15. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_status_surface.py +109 -1
  16. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_tool_manifest.py +48 -1
  17. bantamkit-0.35.3/tests/test_updatecheck.py +541 -0
  18. bantamkit-0.35.1/_assets/tools/shiftwork_clock_out.json +0 -95
  19. bantamkit-0.35.1/_assets/tools/shiftwork_plan.json +0 -25
  20. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/contracts/default.yaml +0 -0
  21. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/manifest.yaml +0 -0
  22. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/HISTORY.md +0 -0
  23. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/README.md +0 -0
  24. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
  25. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
  26. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
  27. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
  28. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
  29. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
  30. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
  31. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
  32. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
  33. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
  34. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
  35. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
  36. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
  37. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
  38. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
  39. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
  40. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
  41. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
  42. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
  43. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
  44. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
  45. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
  46. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
  47. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
  48. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
  49. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
  50. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
  51. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
  52. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
  53. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
  54. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
  55. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
  56. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/fixtures/.gitkeep +0 -0
  57. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/fixtures/catalog.json +0 -0
  58. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/perturbations/task-completion.yaml +0 -0
  59. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/.gitkeep +0 -0
  60. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/extract-contact.yaml +0 -0
  61. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/extract-invoice.yaml +0 -0
  62. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/extract-order.yaml +0 -0
  63. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/extract-schedule.yaml +0 -0
  64. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/extract-versions.yaml +0 -0
  65. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
  66. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
  67. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
  68. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
  69. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-db-port.yaml +0 -0
  70. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-deploy.yaml +0 -0
  71. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
  72. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
  73. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-oncall.yaml +0 -0
  74. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
  75. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/recall-owner.yaml +0 -0
  76. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
  77. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
  78. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/shop-compare.yaml +0 -0
  79. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
  80. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
  81. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/evals/tasks/shop-total.yaml +0 -0
  82. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/pricing/default.json +0 -0
  83. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/profiles/default.yaml +0 -0
  84. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/profiles/patient.yaml +0 -0
  85. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/rubrics/.gitkeep +0 -0
  86. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/rubrics/code-quality.yaml +0 -0
  87. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/rubrics/grounded-completion.yaml +0 -0
  88. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/rubrics/task-completion.yaml +0 -0
  89. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/schemas/shiftwork-checkpoint.json +0 -0
  90. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/skills/.gitkeep +0 -0
  91. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/skills/file-graph.md +0 -0
  92. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/skills/memory.md +0 -0
  93. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/.gitkeep +0 -0
  94. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/bantamkit_read.json +0 -0
  95. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/bantamkit_status.json +0 -0
  96. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/build_identity.json +0 -0
  97. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/document_list.json +0 -0
  98. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/document_read.json +0 -0
  99. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/file_graph.json +0 -0
  100. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/memory_compact.json +0 -0
  101. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/memory_dream.json +0 -0
  102. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/memory_recall.json +0 -0
  103. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/memory_save.json +0 -0
  104. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/repo_map.json +0 -0
  105. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/shiftwork_clock_in.json +0 -0
  106. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/shiftwork_status.json +0 -0
  107. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/skill_audit.json +0 -0
  108. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/token_ledger.json +0 -0
  109. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/validate_json.json +0 -0
  110. {bantamkit-0.35.1 → bantamkit-0.35.3}/_assets/tools/work_plan.json +0 -0
  111. {bantamkit-0.35.1 → bantamkit-0.35.3}/hatch_build.py +0 -0
  112. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/agent.py +0 -0
  113. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/assets.py +0 -0
  114. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/budget.py +0 -0
  115. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/client.py +0 -0
  116. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/contract.py +0 -0
  117. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/criticreplay.py +0 -0
  118. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/critique.py +0 -0
  119. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/docmanifest.py +0 -0
  120. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/docread.py +0 -0
  121. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/evalrun.py +0 -0
  122. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/eventlog.py +0 -0
  123. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/filegraph.py +0 -0
  124. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/hostinstall.py +0 -0
  125. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/loopguard.py +0 -0
  126. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/mcpreport.py +0 -0
  127. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/memory/__init__.py +0 -0
  128. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/memory/__main__.py +0 -0
  129. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/memory/component.py +0 -0
  130. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/memory/divergence.py +0 -0
  131. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/memory/dream.py +0 -0
  132. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/memory/layers.py +0 -0
  133. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/pdfread.py +0 -0
  134. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/pricing.py +0 -0
  135. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/profile.py +0 -0
  136. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/repomap.py +0 -0
  137. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/shiftwork.py +0 -0
  138. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/skillaudit.py +0 -0
  139. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/statusline.py +0 -0
  140. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/structured.py +0 -0
  141. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/textutil.py +0 -0
  142. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/tokenledger.py +0 -0
  143. {bantamkit-0.35.1 → bantamkit-0.35.3}/src/bantamkit/workplan.py +0 -0
  144. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/cli_exit_status_probe.py +0 -0
  145. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/conftest.py +0 -0
  146. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/bad-crc.docx +0 -0
  147. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/charref-4301-digits.html +0 -0
  148. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/charset-table.json +0 -0
  149. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/compression-method-9.docx +0 -0
  150. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/corrupt-deflate.docx +0 -0
  151. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/encrypted-member.docx +0 -0
  152. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/encrypted-mimetype.odt +0 -0
  153. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
  154. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/internal-dtd-entity.docx +0 -0
  155. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/rfc2231-charset.eml +0 -0
  156. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/rfc822-nested-twice.eml +0 -0
  157. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
  158. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/docread/x-uuencode.eml +0 -0
  159. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/f8404ab-perturbation-baseline.json +0 -0
  160. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/data/platform-assumption-baseline.json +0 -0
  161. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/docread_fixtures.py +0 -0
  162. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/perturbation_baseline_harness.py +0 -0
  163. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/rbp16_effect_probe.py +0 -0
  164. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/rbp18_payload_probe.py +0 -0
  165. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_adapter.py +0 -0
  166. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_agent.py +0 -0
  167. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_amendguard.py +0 -0
  168. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_bantamkit_gitignore.py +0 -0
  169. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_bantamkit_read_tool.py +0 -0
  170. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_budget.py +0 -0
  171. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_build_identity.py +0 -0
  172. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_client.py +0 -0
  173. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_compaction_corpus_survey.py +0 -0
  174. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_conformance.py +0 -0
  175. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_conformance_harness_resilience.py +0 -0
  176. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_conformance_suite_table_gate.py +0 -0
  177. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_contract_fanout.py +0 -0
  178. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_criticreplay.py +0 -0
  179. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_critique.py +0 -0
  180. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_doc_commands_gate.py +0 -0
  181. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_docread.py +0 -0
  182. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_docread_ceilings.py +0 -0
  183. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_document_manifest_parity.py +0 -0
  184. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_document_setup.py +0 -0
  185. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_document_tasks.py +0 -0
  186. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_document_tools.py +0 -0
  187. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_encoding_gate.py +0 -0
  188. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_evalrun.py +0 -0
  189. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_eventlog.py +0 -0
  190. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_field_program_gates.py +0 -0
  191. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_field_programs.py +0 -0
  192. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_filegraph.py +0 -0
  193. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_hostinstall.py +0 -0
  194. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_install_shape.py +0 -0
  195. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_ladder_statistics.py +0 -0
  196. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_launcher_which.py +0 -0
  197. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_layers.py +0 -0
  198. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_loopguard.py +0 -0
  199. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_mcp_endpoint.py +0 -0
  200. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_mcpdrift.py +0 -0
  201. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_mcpreport.py +0 -0
  202. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_memory.py +0 -0
  203. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_memory_compact_tool.py +0 -0
  204. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_memory_component.py +0 -0
  205. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_memory_divergence.py +0 -0
  206. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_memory_dream.py +0 -0
  207. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_memory_layers.py +0 -0
  208. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_memory_store_tripwire.py +0 -0
  209. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_mutmatrix.py +0 -0
  210. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_newline_gate.py +0 -0
  211. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_packaging.py +0 -0
  212. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_pdfread.py +0 -0
  213. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_pinharness_ledger.py +0 -0
  214. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_platform_assumption_gate.py +0 -0
  215. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_pricing.py +0 -0
  216. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_repo_map_tool.py +0 -0
  217. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_repomap.py +0 -0
  218. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_served_tool_count_records.py +0 -0
  219. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_shiftwork.py +0 -0
  220. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_skillaudit.py +0 -0
  221. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_statusline.py +0 -0
  222. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_structured.py +0 -0
  223. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_thread_exception_gate.py +0 -0
  224. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_tokenledger.py +0 -0
  225. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_version_agreement.py +0 -0
  226. {bantamkit-0.35.1 → bantamkit-0.35.3}/tests/test_workplan.py +0 -0
@@ -11,3 +11,4 @@ dist-*/
11
11
  .bantamkit/memory/
12
12
  .shiftwork/
13
13
  .release/
14
+ .claude/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bantamkit
3
- Version: 0.35.1
3
+ Version: 0.35.3
4
4
  Summary: Memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop, plus a Python library that lifts small-model agents. Install once, run offline.
5
5
  Project-URL: Homepage, https://github.com/Ink01101011/bantamkit
6
6
  Project-URL: Repository, https://github.com/Ink01101011/bantamkit
@@ -23,7 +23,7 @@ Requires-Dist: hatchling>=1.24; extra == 'dev'
23
23
  Requires-Dist: pytest>=8.0; extra == 'dev'
24
24
  Requires-Dist: ruff>=0.4; extra == 'dev'
25
25
  Provides-Extra: mcp
26
- Requires-Dist: mcp<3,>=2.0; extra == 'mcp'
26
+ Requires-Dist: mcp<2.1,>=2.0; extra == 'mcp'
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # bantamkit — memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop (Python)
@@ -97,9 +97,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
97
97
  one platform only), copy `wheels/` across, and install from it:
98
98
 
99
99
  ```bash
100
- python -m pip download "bantamkit[mcp]==0.35.1" -d wheels
100
+ python -m pip download "bantamkit[mcp]==0.35.3" -d wheels
101
101
  python -m venv <env>
102
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.1"
102
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.3"
103
103
  <env>/bin/bantamkit-mcp --install cursor
104
104
  ```
105
105
 
@@ -69,9 +69,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
69
69
  one platform only), copy `wheels/` across, and install from it:
70
70
 
71
71
  ```bash
72
- python -m pip download "bantamkit[mcp]==0.35.1" -d wheels
72
+ python -m pip download "bantamkit[mcp]==0.35.3" -d wheels
73
73
  python -m venv <env>
74
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.1"
74
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.3"
75
75
  <env>/bin/bantamkit-mcp --install cursor
76
76
  ```
77
77
 
@@ -0,0 +1,150 @@
1
+ {
2
+ "name": "shiftwork_clock_out",
3
+ "description": "Shift-work clock-out: record a finished unit — set its status, advance plan.cursor, merge handoff_patch, push history_entry onto the 5-entry ring — validating the whole mutated document against the checkpoint schema BEFORE an atomic write (a failure writes nothing and returns result=error). `unit_id` must be `plan.cursor`, the one unit clock_in briefs; any other id is refused (`unit X is not the cursor unit Y`). `handoff_patch` and `history_entry` are both required arguments: `handoff_patch` is shallow-merged into `handoff` and takes only its four keys (pass `{}` to leave it unchanged); `history_entry` needs `unit` and `outcome`; `status` is one of the five unit statuses — a value outside these shapes is refused by the schema check, nothing written. Every success appends one accounting line (unit, role, status, ts, briefed, plus your accounting fields, e.g. tokens/duration_ms/model) to <checkpoint>.log.jsonl, beside the brief lines clock_in writes (a brief line carries `event` and no `status`). `accounting` is optional: null, the default, logs a line with no cost figures at all; a non-null line must fit the accounting shape below. `briefed` is written by the runtime, never taken from you — a self-reported value is overwritten by the measured one: true when a brief was issued for this unit since its LAST clock-out (not ever), so a unit re-run inline after a blocked clock-out reads false. A brief is not a precondition: a unit that was never clocked in still clocks out, with briefed=false. If the checkpoint's job.roles names this unit's role, accounting.model must be one of that role's model identifiers, spelled exactly — a model that is not on the list, or none reported at all, is refused before anything is written; a role job.roles omits, and a checkpoint carrying no job.roles, are unconstrained.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "properties": {
9
+ "checkpoint": {
10
+ "title": "Checkpoint",
11
+ "type": "string"
12
+ },
13
+ "unit_id": {
14
+ "title": "Unit Id",
15
+ "type": "string",
16
+ "description": "The unit being clocked out. Must equal plan.cursor — the single-pointer contract: clock_in briefs only the cursor unit and clock_out accepts only it."
17
+ },
18
+ "status": {
19
+ "title": "Status",
20
+ "type": "string",
21
+ "enum": [
22
+ "todo",
23
+ "in_progress",
24
+ "done",
25
+ "blocked",
26
+ "dropped"
27
+ ],
28
+ "description": "The unit's new status; the same five values the checkpoint schema allows on plan.units[].status."
29
+ },
30
+ "handoff_patch": {
31
+ "title": "Handoff Patch",
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "description": "Shallow-merged into `handoff`: only these four keys exist there, and a key outside them is refused by the schema check. Required; `{}` leaves `handoff` unchanged.",
35
+ "properties": {
36
+ "next_action": {
37
+ "type": "string",
38
+ "minLength": 1,
39
+ "description": "Imperative first move — zero re-derivation."
40
+ },
41
+ "open_questions": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "string",
45
+ "minLength": 1
46
+ },
47
+ "description": "The autonomy switch: empty = driver keeps cycling, non-empty = STOP and ask."
48
+ },
49
+ "do_not": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string",
53
+ "minLength": 1
54
+ },
55
+ "description": "Negative space — near-mistakes past sessions made."
56
+ },
57
+ "notes": {
58
+ "type": "string",
59
+ "description": "Free-form prose for the next session, one string; the empty string clears it."
60
+ }
61
+ }
62
+ },
63
+ "history_entry": {
64
+ "title": "History Entry",
65
+ "type": "object",
66
+ "required": [
67
+ "unit",
68
+ "outcome"
69
+ ],
70
+ "additionalProperties": true,
71
+ "description": "Pushed onto the 5-entry `history` ring; `unit` and `outcome` are required, `notes` optional, other keys pass through.",
72
+ "properties": {
73
+ "unit": {
74
+ "type": "string",
75
+ "minLength": 1
76
+ },
77
+ "outcome": {
78
+ "type": "string",
79
+ "minLength": 1
80
+ },
81
+ "notes": {
82
+ "type": "string"
83
+ }
84
+ }
85
+ },
86
+ "accounting": {
87
+ "anyOf": [
88
+ {
89
+ "type": "object",
90
+ "description": "Per-unit cost. Written verbatim, beside ts/unit/role/status, as one line of <checkpoint>.log.jsonl - the ledger the N-sessions experiment reads. The keys named here have fixed meanings so a reader holding only the ledger knows what each number counts, in which unit, and which model produced it; any other key passes through unchanged (a refused key is friction, not safety). Lines written before this schema existed may spell these differently; the schema governs new writes only.",
91
+ "properties": {
92
+ "tokens": {
93
+ "type": "integer",
94
+ "minimum": 0,
95
+ "description": "Tokens the unit consumed as the harness's subagent counter reports them: every class it reports (input, output, cache creation) summed, EXCLUDING cache reads. In a non-null accounting line it is required — a line without it is refused before anything is written; only `accounting: null`, the default, is exempt, and that logs no cost at all. A rounded self-estimate is allowed only if `note` says it is one; a unit whose cost is unknown cannot be compared with any other."
96
+ },
97
+ "cache_read_tokens": {
98
+ "type": "integer",
99
+ "minimum": 0,
100
+ "description": "Tokens the unit's requests served from prompt cache, kept OUT of `tokens` because they dominate a real session's traffic and would swamp the work signal. Optional: omit it when the harness reports no such figure - a written 0 means the unit read nothing from cache."
101
+ },
102
+ "duration_ms": {
103
+ "type": "integer",
104
+ "minimum": 0,
105
+ "description": "Wall-clock from spawning the subagent to its final message, in whole milliseconds. In a non-null line it is required, and it is the only duration key with a defined meaning: older lines carry `duration`, `duration_min` or `duration_s`, which this schema neither reads nor renames."
106
+ },
107
+ "model": {
108
+ "type": "string",
109
+ "description": "The exact identifier of the model the subagent actually ran on (e.g. `claude-sonnet-5`) and nothing else - a caveat about how it was chosen belongs in `note`. Not required here: when the checkpoint's job.roles names this unit's role, clock_out already requires it and refuses a value off that role's list, spelled exactly; a checkpoint with no job.roles leaves it optional. This schema does not change that gate."
110
+ },
111
+ "tool_uses": {
112
+ "type": "integer",
113
+ "minimum": 0,
114
+ "description": "Tool calls the subagent made, as the harness counts them. Optional; it is the denominator that makes `tokens` comparable across units of different size (per-unit cost is linear in tool calls, not in unit length)."
115
+ },
116
+ "note": {
117
+ "type": "string",
118
+ "description": "Free text for what the fields above cannot say: that `tokens` is an estimate, that this is a retry or a second scope under the same unit id, what a harness counter excludes. Anything that is not a model identifier goes here, not in `model`."
119
+ }
120
+ },
121
+ "required": [
122
+ "tokens",
123
+ "duration_ms"
124
+ ],
125
+ "additionalProperties": true
126
+ },
127
+ {
128
+ "type": "null"
129
+ }
130
+ ],
131
+ "default": null,
132
+ "title": "Accounting"
133
+ }
134
+ },
135
+ "required": [
136
+ "checkpoint",
137
+ "unit_id",
138
+ "status",
139
+ "handoff_patch",
140
+ "history_entry"
141
+ ],
142
+ "type": "object",
143
+ "title": "shiftwork_clock_outArguments"
144
+ },
145
+ "output_schema": {
146
+ "type": "object",
147
+ "additionalProperties": true,
148
+ "title": "shiftwork_clock_outDictOutput"
149
+ }
150
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "shiftwork_plan",
3
+ "description": "Shift-work plan: read-only batch view of a checkpoint — which units its `depends_on` graph permits to run in parallel. Answers `{\"result\": \"plan\", \"batches\", \"ready\", \"sequence\", \"width\", \"cursor\"}`: `ready` is `batches[0]`, the units whose dependencies are all satisfied, and `width` is the largest batch length, the widest fan-out. Only `cursor` can be clocked: shiftwork_clock_in briefs it and shiftwork_clock_out refuses every other id, so a `ready` member beyond the cursor is what the graph permits, not a unit the clock accepts yet. Units with status `done` or `dropped` are satisfied — dropped from the graph, and edges pointing at them treated as already resolved; `todo`, `in_progress` and `blocked` stay in it. Every unit has priority 0, because the checkpoint schema has no priority field, so order inside a batch is `plan.units` order. Same refusal sentences as `work_plan` for a duplicate id, an unknown dependency or a cycle, and the same refusals `shiftwork_status` gives for a checkpoint it cannot read. It reports what the dependency graph permits; it does not move the cursor, which is echoed unchanged so the single-pointer contract and the batch view can be read side by side. An orchestrator stays responsible for what it actually dispatches: `depends_on` encodes logical order, not file contention. Never mutates.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "properties": {
9
+ "checkpoint": {
10
+ "title": "Checkpoint",
11
+ "type": "string"
12
+ }
13
+ },
14
+ "required": [
15
+ "checkpoint"
16
+ ],
17
+ "type": "object",
18
+ "title": "shiftwork_planArguments"
19
+ },
20
+ "output_schema": {
21
+ "type": "object",
22
+ "additionalProperties": true,
23
+ "title": "shiftwork_planDictOutput"
24
+ }
25
+ }
@@ -69,7 +69,12 @@ dependencies = ["httpx>=0.27", "jsonschema>=4.21", "pyyaml>=6.0"]
69
69
  # without this declaration the bar would silently skip in exactly the environment
70
70
  # CI runs (`pip install -e "runtime-py[dev,mcp]"`). Declared here so it runs.
71
71
  dev = ["pytest>=8.0", "ruff>=0.4", "hatchling>=1.24"]
72
- mcp = ["mcp>=2.0,<3"]
72
+ # Upper bound measured, not guessed: 2.0.0 and 2.0.1 wrap a tool crash as
73
+ # `Error executing tool <name>: <exc>`, which runtime-ts/src/mcp/server.ts mirrors.
74
+ # 2.1.0 added an UnexpectedToolError branch that drops the `: <exc>` suffix, so the
75
+ # crash text stops reaching the client and `--suite workplan` goes red on the
76
+ # reference while the port still carries it. Raise this only together with the port.
77
+ mcp = ["mcp>=2.0,<2.1"]
73
78
 
74
79
  [project.scripts]
75
80
  bantamkit-mcp = "bantamkit.mcpserver:main"
@@ -29,4 +29,4 @@ from bantamkit.structured import StructuredOutputError, extract_json, structured
29
29
  # file as its dynamic version source, so the wheel's metadata and the string the MCP
30
30
  # server advertises are the same committed bytes, and neither is a function of when
31
31
  # someone last ran `pip`.
32
- __version__ = "0.35.1"
32
+ __version__ = "0.35.3"
@@ -30,6 +30,7 @@ from bantamkit import (
30
30
  shiftwork,
31
31
  skillaudit,
32
32
  tokenledger,
33
+ updatecheck,
33
34
  workplan,
34
35
  )
35
36
  from bantamkit.assets import AssetNotFound, assets_root, load_skill, load_tool_asset
@@ -965,6 +966,18 @@ def status_report(
965
966
  + ("unreadable" if size is None else str(size))
966
967
  + f" of {budget} bytes",
967
968
  f"event log: {'on' if log.enabled else 'off'}",
969
+ # THE UPDATE LINE IS NOT A CONDITION, and that is the ruling of 2026-09-19 rather
970
+ # than an omission. A newer version existing is not a fault: the server is serving
971
+ # correctly, and `degraded_notice`'s docstring above is the operator's own reason —
972
+ # a footer on every result is noise, and noise trains a reader to skip it. So this
973
+ # never flips line 1, never enters `degraded_conditions`, and never rides another
974
+ # tool's reply. It also means the record is opened ONLY here, when someone asked for
975
+ # a report, and not on the per-call path whose cost `docs/status.md` documents.
976
+ #
977
+ # `updatecheck` opens one file and asks nobody anything — AS-7(3) is intact, and
978
+ # `test_selfupdate.py::test_only_the_update_flag_reaches_this_module_from_the_server`
979
+ # still holds, because the name this reaches is `updatecheck` and not `selfupdate`.
980
+ updatecheck.update_line(str(identity["version"])),
968
981
  ]
969
982
  if conditions:
970
983
  lines.append(f"{_plural(len(conditions), 'problem')}:")
@@ -1894,6 +1907,25 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
1894
1907
 
1895
1908
  @server.resource("bantamkit://skills/{name}")
1896
1909
  def skill_resource(name: str) -> str:
1910
+ # A skill that pairs with a tool is served on that tool's surfaces and no other. The
1911
+ # pairing is the one `filegraph.py` spells: `load_tool("file_graph")` beside
1912
+ # `load_skill("file-graph")` — the skill's name with `-` for `_`. `file-graph` is the
1913
+ # eval agent's system-prompt snippet telling it to call `file_graph`, a tool whose
1914
+ # asset claims only `agent`; handing that text to an MCP client sends it to a tool
1915
+ # `tools/list` does not carry and `tools/call` refuses as unknown (job60 row 46).
1916
+ # Same sentence and same error code on both runtimes; `tools/conformance/suites/
1917
+ # instructions.mjs` reads every skill on disk over `resources/read` and resolves the
1918
+ # tools it names against `tools/list`.
1919
+ paired = name.replace("-", "_")
1920
+ try:
1921
+ asset = load_tool_asset(paired)
1922
+ except AssetNotFound:
1923
+ asset = None
1924
+ if asset is not None and "mcp" not in asset["surfaces"]:
1925
+ raise ResourceError(
1926
+ f"skill asset {name} is not served here: it pairs with tool {paired}, "
1927
+ f"whose asset claims surfaces {asset['surfaces']}, not mcp"
1928
+ )
1897
1929
  try:
1898
1930
  return load_skill(name)
1899
1931
  except AssetNotFound:
@@ -133,7 +133,7 @@ _FACTS_UNREADABLE = (
133
133
  _ARCHIVE_UNREADABLE = (
134
134
  "an archive that could not be listed is not an empty archive, and answering "
135
135
  "'nothing is archived' here is what makes compaction look like deletion — the "
136
- "facts compact() moved are still on disk under this path"
136
+ "facts that compaction moved are still on disk under this path"
137
137
  )
138
138
  # The same distinction one syscall down, for `restore`, which stats one named path
139
139
  # instead of listing (see `archived()` for why). A refused stat is not an absent file,
@@ -1310,5 +1310,5 @@ class MemoryStore:
1310
1310
  if size > self.index_budget:
1311
1311
  raise MemoryBudgetExceeded(
1312
1312
  f"memory index is {size} bytes, budget is {self.index_budget}: "
1313
- f"run compact() or tersen descriptions"
1313
+ f"compact the store or tersen descriptions"
1314
1314
  )
@@ -55,11 +55,14 @@ a store or a transport exists, the same shape `--assets-root` and `--install` us
55
55
  from __future__ import annotations
56
56
 
57
57
  import json
58
+ import os
58
59
  import shlex
59
60
  import subprocess
60
61
  import sys
62
+ import tempfile
61
63
  from collections.abc import Callable
62
64
  from dataclasses import dataclass
65
+ from datetime import UTC, datetime
63
66
 
64
67
  from bantamkit import __version__
65
68
 
@@ -359,6 +362,83 @@ def compare_versions(installed: str, latest: str) -> int:
359
362
  return 1 if left > right else -1
360
363
 
361
364
 
365
+ #: The `checked_at` stamp's shape, spelled rather than defaulted. `datetime.isoformat()`
366
+ #: renders `+00:00` and `Date.prototype.toISOString` renders `.000Z`, so a record written by
367
+ #: the two runtimes would differ in bytes neither reader cares about. Both sides render this
368
+ #: one shape instead, and `runtime-ts` strips its milliseconds to reach it.
369
+ STAMP = "%Y-%m-%dT%H:%M:%SZ"
370
+
371
+
372
+ def record_update(latest: str, now: str | None = None) -> bool:
373
+ """Put the version this flag just fetched into the record `bantamkit_status` reads.
374
+
375
+ NO SECOND NETWORK CALL AND NO NEW FAILURE MODE, which is the whole reason the writer is
376
+ here rather than anywhere a reader could reach. `update` already holds `latest` — it was
377
+ parsed out of the answer the operator's own `--update` asked for — so this writes what is
378
+ in hand. `updatecheck` reads that file and NEVER writes it; the split is structural and
379
+ both halves are gated (`test_updatecheck.py::test_the_source_writes_nothing_and_reaches_
380
+ no_network`), so a future reader cannot grow a write by accident.
381
+
382
+ `updatecheck` IS IMPORTED INSIDE THE BODY, and not for style. `updatecheck` imports
383
+ `PROGRAM` and `compare_versions` from THIS module at its top level, so a module-level
384
+ import here would be a cycle that fails at interpreter start. The function-local import
385
+ is the direction that works, and `_names_reached_by` in `test_selfupdate.py` sees it —
386
+ `bantamkit.updatecheck` is on that gate's allowlist by name.
387
+
388
+ IT CREATES NO DIRECTORY. `<homedir>/.bantamkit` is made by an install, and a `--update`
389
+ run on a machine that has never had one writes nothing rather than deciding where this
390
+ toolbox's home directory should be. A cwd-relative `.bantamkit` is a MEMORY STORE (J54-3)
391
+ and the path here comes from `updatecheck.record_path()`, which hangs off `_home()`.
392
+
393
+ THE OTHER RUNTIME'S KEY IS LEFT EXACTLY AS FOUND. npm and PyPI are two registries that
394
+ can disagree at one version number — job56 shipped a day where they did — so this fills
395
+ `updatecheck.KEY` and copies the rest of the record through untouched. An UNREADABLE
396
+ record is replaced rather than merged: there is nothing in it to preserve.
397
+
398
+ TEMP FILE AND RENAME, so a reader never sees half a record: `os.replace` is atomic on
399
+ POSIX and on Windows, and the temp file is made in the SAME directory so the rename is
400
+ never across a filesystem.
401
+
402
+ RETURNS whether it wrote, and RAISES FOR NOTHING. A failed write must not turn a
403
+ successful `--update` into a failure: the operator's install was updated either way, and
404
+ the worst case is a status line that still says `never checked`.
405
+ """
406
+ # The MODULE is named in the import so the gate's allowlist can name it too: a
407
+ # `from bantamkit import updatecheck` records only `bantamkit`, which would let any
408
+ # sibling module in under the same allowance.
409
+ from bantamkit.updatecheck import KEY, SOURCE_RECORD, load_record, record_path
410
+
411
+ path = record_path()
412
+ directory = path.parent
413
+ if not directory.is_dir():
414
+ return False
415
+ source, existing = load_record(path)
416
+ payload: dict[str, object] = dict(existing) if source == SOURCE_RECORD and existing else {}
417
+ payload["checked_at"] = now or datetime.now(UTC).strftime(STAMP)
418
+ payload[KEY] = {"distribution": DISTRIBUTION, "latest": latest}
419
+ try:
420
+ body = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
421
+ except (TypeError, ValueError):
422
+ return False
423
+ try:
424
+ handle, temporary = tempfile.mkstemp(
425
+ dir=str(directory), prefix=".update-check-", suffix=".json"
426
+ )
427
+ except OSError:
428
+ return False
429
+ try:
430
+ with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
431
+ stream.write(body)
432
+ os.replace(temporary, path)
433
+ except OSError:
434
+ try:
435
+ os.unlink(temporary)
436
+ except OSError:
437
+ pass
438
+ return False
439
+ return True
440
+
441
+
362
442
  def update(
363
443
  installed: str,
364
444
  origin: Origin,
@@ -389,6 +469,15 @@ def update(
389
469
  raise UpdateRefused(UNREACHABLE.format(reason=exc)) from None
390
470
 
391
471
  latest = latest_from_index_payload(body)
472
+
473
+ # THE RECORD IS WRITTEN HERE AND NOT IN ONE OF THE ARMS BELOW, because what it records is
474
+ # "the index said X on this date" — which is true the moment the fetch returned, whatever
475
+ # this flag then decides to do about it. An operator whose shape has no route gets the
476
+ # refusal AND a `bantamkit_status` that now knows the number; one who is already current
477
+ # gets a line that says so with a date. The return value is deliberately dropped: a record
478
+ # that could not be written is not a reason to fail an update that worked.
479
+ record_update(latest)
480
+
392
481
  header = COMPARISON.format(program=PROGRAM, installed=installed, latest=latest)
393
482
  order = compare_versions(installed, latest)
394
483
  if order == 0: