bantamkit 0.35.6__tar.gz → 0.35.8__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 (235) hide show
  1. {bantamkit-0.35.6 → bantamkit-0.35.8}/PKG-INFO +4 -4
  2. {bantamkit-0.35.6 → bantamkit-0.35.8}/README.md +2 -2
  3. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/memory_compact.json +1 -2
  4. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/memory_recall.json +1 -3
  5. bantamkit-0.35.8/_assets/tools/shiftwork_clock_in.json +37 -0
  6. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/shiftwork_clock_out.json +1 -1
  7. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/validate_json.json +1 -1
  8. bantamkit-0.35.8/_assets/tools/work_plan.json +55 -0
  9. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/__init__.py +1 -1
  10. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/contract.py +393 -6
  11. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/eventlog.py +24 -1
  12. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/hookadapter.py +273 -50
  13. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/hostinstall.py +148 -23
  14. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/mcpreport.py +29 -4
  15. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/mcpserver.py +87 -11
  16. bantamkit-0.35.8/src/bantamkit/mcpstdio.py +146 -0
  17. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/memory/__main__.py +17 -2
  18. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/memory/component.py +81 -7
  19. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/memory/layers.py +41 -1
  20. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/memory/store.py +325 -38
  21. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/selfupdate.py +212 -73
  22. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/shiftwork.py +244 -42
  23. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/workplan.py +67 -7
  24. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/served-tool-surface.json +19 -16
  25. bantamkit-0.35.8/tests/test_contract_patterns.py +190 -0
  26. bantamkit-0.35.8/tests/test_contract_refs.py +155 -0
  27. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_eventlog.py +25 -0
  28. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_hookadapter.py +230 -2
  29. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_hostinstall.py +106 -0
  30. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_hostinstall_hooks.py +113 -0
  31. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_mcpreport.py +42 -0
  32. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_mcpserver.py +15 -0
  33. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_memory.py +326 -0
  34. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_memory_compact_tool.py +8 -1
  35. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_memory_component.py +12 -6
  36. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_memory_layers.py +51 -0
  37. bantamkit-0.35.8/tests/test_memory_strings.py +372 -0
  38. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_nativeexport.py +10 -7
  39. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_selfupdate.py +367 -33
  40. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_shiftwork.py +420 -2
  41. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_skillaudit.py +53 -0
  42. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_status_surface.py +16 -4
  43. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_workplan.py +82 -0
  44. bantamkit-0.35.6/_assets/tools/shiftwork_clock_in.json +0 -37
  45. bantamkit-0.35.6/_assets/tools/work_plan.json +0 -49
  46. {bantamkit-0.35.6 → bantamkit-0.35.8}/.gitignore +0 -0
  47. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/contracts/default.yaml +0 -0
  48. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/manifest.yaml +0 -0
  49. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/HISTORY.md +0 -0
  50. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/README.md +0 -0
  51. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
  52. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
  53. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
  54. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
  55. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
  56. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
  57. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
  58. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
  59. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
  60. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
  61. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
  62. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
  63. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
  64. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
  65. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
  66. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
  67. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
  68. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
  69. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
  70. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
  71. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
  72. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
  73. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
  74. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
  75. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
  76. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
  77. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
  78. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
  79. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
  80. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
  81. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
  82. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
  83. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/fixtures/.gitkeep +0 -0
  84. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/fixtures/catalog.json +0 -0
  85. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/perturbations/task-completion.yaml +0 -0
  86. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/.gitkeep +0 -0
  87. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/extract-contact.yaml +0 -0
  88. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/extract-invoice.yaml +0 -0
  89. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/extract-order.yaml +0 -0
  90. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/extract-schedule.yaml +0 -0
  91. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/extract-versions.yaml +0 -0
  92. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
  93. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
  94. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
  95. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
  96. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-db-port.yaml +0 -0
  97. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-deploy.yaml +0 -0
  98. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
  99. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
  100. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-oncall.yaml +0 -0
  101. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
  102. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/recall-owner.yaml +0 -0
  103. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
  104. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
  105. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/shop-compare.yaml +0 -0
  106. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
  107. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
  108. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/evals/tasks/shop-total.yaml +0 -0
  109. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/pricing/default.json +0 -0
  110. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/profiles/default.yaml +0 -0
  111. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/profiles/patient.yaml +0 -0
  112. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/rubrics/.gitkeep +0 -0
  113. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/rubrics/code-quality.yaml +0 -0
  114. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/rubrics/grounded-completion.yaml +0 -0
  115. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/rubrics/task-completion.yaml +0 -0
  116. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/schemas/shiftwork-checkpoint.json +0 -0
  117. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/skills/.gitkeep +0 -0
  118. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/skills/file-graph.md +0 -0
  119. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/skills/memory.md +0 -0
  120. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/.gitkeep +0 -0
  121. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/bantamkit_read.json +0 -0
  122. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/bantamkit_status.json +0 -0
  123. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/build_identity.json +0 -0
  124. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/document_list.json +0 -0
  125. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/document_read.json +0 -0
  126. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/file_graph.json +0 -0
  127. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/memory_dream.json +0 -0
  128. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/memory_save.json +0 -0
  129. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/repo_map.json +0 -0
  130. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/shiftwork_plan.json +0 -0
  131. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/shiftwork_status.json +0 -0
  132. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/skill_audit.json +0 -0
  133. {bantamkit-0.35.6 → bantamkit-0.35.8}/_assets/tools/token_ledger.json +0 -0
  134. {bantamkit-0.35.6 → bantamkit-0.35.8}/hatch_build.py +0 -0
  135. {bantamkit-0.35.6 → bantamkit-0.35.8}/pyproject.toml +0 -0
  136. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/agent.py +0 -0
  137. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/assets.py +0 -0
  138. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/budget.py +0 -0
  139. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/client.py +0 -0
  140. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/criticreplay.py +0 -0
  141. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/critique.py +0 -0
  142. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/docmanifest.py +0 -0
  143. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/docread.py +0 -0
  144. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/evalrun.py +0 -0
  145. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/filegraph.py +0 -0
  146. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/loopguard.py +0 -0
  147. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/memory/__init__.py +0 -0
  148. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/memory/divergence.py +0 -0
  149. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/memory/dream.py +0 -0
  150. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/pdfread.py +0 -0
  151. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/pricing.py +0 -0
  152. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/profile.py +0 -0
  153. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/repomap.py +0 -0
  154. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/skillaudit.py +0 -0
  155. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/statusline.py +0 -0
  156. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/structured.py +0 -0
  157. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/textutil.py +0 -0
  158. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/tokenledger.py +0 -0
  159. {bantamkit-0.35.6 → bantamkit-0.35.8}/src/bantamkit/updatecheck.py +0 -0
  160. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/cli_exit_status_probe.py +0 -0
  161. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/conftest.py +0 -0
  162. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/bad-crc.docx +0 -0
  163. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/charref-4301-digits.html +0 -0
  164. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/charset-table.json +0 -0
  165. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/compression-method-9.docx +0 -0
  166. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/corrupt-deflate.docx +0 -0
  167. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/encrypted-member.docx +0 -0
  168. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/encrypted-mimetype.odt +0 -0
  169. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
  170. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/internal-dtd-entity.docx +0 -0
  171. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/rfc2231-charset.eml +0 -0
  172. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/rfc822-nested-twice.eml +0 -0
  173. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
  174. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/docread/x-uuencode.eml +0 -0
  175. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/f8404ab-perturbation-baseline.json +0 -0
  176. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/data/platform-assumption-baseline.json +0 -0
  177. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/docread_fixtures.py +0 -0
  178. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/perturbation_baseline_harness.py +0 -0
  179. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/rbp16_effect_probe.py +0 -0
  180. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/rbp18_payload_probe.py +0 -0
  181. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_adapter.py +0 -0
  182. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_agent.py +0 -0
  183. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_amendguard.py +0 -0
  184. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_bantamkit_gitignore.py +0 -0
  185. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_bantamkit_read_tool.py +0 -0
  186. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_budget.py +0 -0
  187. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_build_identity.py +0 -0
  188. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_client.py +0 -0
  189. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_compaction_corpus_survey.py +0 -0
  190. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_conformance.py +0 -0
  191. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_conformance_harness_resilience.py +0 -0
  192. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_conformance_suite_table_gate.py +0 -0
  193. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_contract_fanout.py +0 -0
  194. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_criticreplay.py +0 -0
  195. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_critique.py +0 -0
  196. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_doc_commands_gate.py +0 -0
  197. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_docread.py +0 -0
  198. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_docread_ceilings.py +0 -0
  199. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_document_manifest_parity.py +0 -0
  200. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_document_setup.py +0 -0
  201. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_document_tasks.py +0 -0
  202. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_document_tools.py +0 -0
  203. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_encoding_gate.py +0 -0
  204. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_evalrun.py +0 -0
  205. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_field_program_gates.py +0 -0
  206. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_field_programs.py +0 -0
  207. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_filegraph.py +0 -0
  208. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_install_shape.py +0 -0
  209. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_ladder_statistics.py +0 -0
  210. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_launcher_which.py +0 -0
  211. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_layers.py +0 -0
  212. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_loopguard.py +0 -0
  213. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_mcp_endpoint.py +0 -0
  214. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_mcpdrift.py +0 -0
  215. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_memory_divergence.py +0 -0
  216. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_memory_dream.py +0 -0
  217. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_memory_store_tripwire.py +0 -0
  218. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_mutmatrix.py +0 -0
  219. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_newline_gate.py +0 -0
  220. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_packaging.py +0 -0
  221. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_pdfread.py +0 -0
  222. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_pinharness_ledger.py +0 -0
  223. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_platform_assumption_gate.py +0 -0
  224. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_pricing.py +0 -0
  225. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_readme_separation.py +0 -0
  226. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_repo_map_tool.py +0 -0
  227. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_repomap.py +0 -0
  228. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_served_tool_count_records.py +0 -0
  229. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_statusline.py +0 -0
  230. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_structured.py +0 -0
  231. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_thread_exception_gate.py +0 -0
  232. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_tokenledger.py +0 -0
  233. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_tool_manifest.py +0 -0
  234. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_updatecheck.py +0 -0
  235. {bantamkit-0.35.6 → bantamkit-0.35.8}/tests/test_version_agreement.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: bantamkit
3
- Version: 0.35.6
3
+ Version: 0.35.8
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
@@ -103,9 +103,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
103
103
  one platform only), copy `wheels/` across, and install from it:
104
104
 
105
105
  ```bash
106
- python -m pip download "bantamkit[mcp]==0.35.6" -d wheels
106
+ python -m pip download "bantamkit[mcp]==0.35.8" -d wheels
107
107
  python -m venv <env>
108
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.6"
108
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.8"
109
109
  <env>/bin/bantamkit-mcp --install cursor
110
110
  ```
111
111
 
@@ -74,9 +74,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
74
74
  one platform only), copy `wheels/` across, and install from it:
75
75
 
76
76
  ```bash
77
- python -m pip download "bantamkit[mcp]==0.35.6" -d wheels
77
+ python -m pip download "bantamkit[mcp]==0.35.8" -d wheels
78
78
  python -m venv <env>
79
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.6"
79
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.8"
80
80
  <env>/bin/bantamkit-mcp --install cursor
81
81
  ```
82
82
 
@@ -10,8 +10,7 @@
10
10
  "properties": {
11
11
  "reserve": {
12
12
  "type": "integer",
13
- "minimum": 0,
14
- "description": "Bytes of index headroom to free below the budget. Omit for the default: the size of the largest index line the store holds, capped at half the budget."
13
+ "description": "Bytes of index headroom to free below the budget: the target is `budget - reserve`, a negative value counts as 0, any value is capped at half the budget, and 0 compacts to the budget itself. Omit for the default: the room the degraded-index warning demands (the budget minus `(90 * budget - 1) // 100`) plus the size of the largest index line the store holds -- at the default 24000-byte budget with a 39-byte largest line that is 2401 + 39 = 2440, a 21560-byte target."
15
14
  }
16
15
  }
17
16
  },
@@ -17,9 +17,7 @@
17
17
  },
18
18
  "k": {
19
19
  "type": "integer",
20
- "minimum": 1,
21
- "maximum": 5,
22
- "description": "Facts wanted. Raised to the store default of 3 when smaller; never lowered below it."
20
+ "description": "Facts wanted, clamped to 1..5. Raised to the store default of 3 when smaller; never lowered below it."
23
21
  }
24
22
  }
25
23
  },
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "shiftwork_clock_in",
3
+ "description": "Shift-work clock-in: schema-validate the checkpoint file and return the brief for the unit at plan.cursor — {unit, role, invariants, handoff, do_not, files} — to hand to the spawned agent verbatim. The unit briefed is the one at plan.cursor, or, when unit_id is given, that unit — either way it must appear in shiftwork_plan's `ready`, the batch the dependency graph says may run now: the cursor unit is held to the same ready test as unit_id. A unit that is not ready (a done or dropped cursor, a cursor whose dependencies have not run, a unit_id naming no unit) is refused with {\"result\": \"error\", \"reason\": \"unit <id> is not ready; ready is <a, b>\"}, and on both paths the refusals of the batch view itself (cycle, unknown dependency, unreadable checkpoint) pass through verbatim; a cursor naming no unit still escalates. Clock-in still never moves plan.cursor; clock_out does. Structured refusals, never exceptions: result=escalate when handoff.open_questions is non-empty, result=success when every unit is done or dropped, result=error when the checkpoint fails validation. Clock-in also WRITES: on result=brief, and only then, it appends one line {event: brief, ts, unit, role} to <checkpoint>.log.jsonl — best-effort: a log it cannot write costs the brief nothing (the brief still returns, no error, only the record is lost). clock_out reads those lines back to write `briefed` on every accounting line, so the ledger is no longer one line per clock-out; a brief line carries `event` and no `status`. See docs/shiftwork.md.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "properties": {
9
+ "checkpoint": {
10
+ "title": "Checkpoint",
11
+ "type": "string"
12
+ },
13
+ "unit_id": {
14
+ "anyOf": [
15
+ {
16
+ "type": "string"
17
+ },
18
+ {
19
+ "type": "null"
20
+ }
21
+ ],
22
+ "default": null,
23
+ "title": "Unit Id"
24
+ }
25
+ },
26
+ "required": [
27
+ "checkpoint"
28
+ ],
29
+ "type": "object",
30
+ "title": "shiftwork_clock_inArguments"
31
+ },
32
+ "output_schema": {
33
+ "type": "object",
34
+ "additionalProperties": true,
35
+ "title": "shiftwork_clock_inDictOutput"
36
+ }
37
+ }
@@ -87,7 +87,7 @@
87
87
  "anyOf": [
88
88
  {
89
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.",
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, except the five the runtime writes itself — unit, role, status, ts, event — which are refused (result=error) before anything is written; a self-reported briefed is overwritten, not refused. Lines written before this schema existed may spell these differently; the schema governs new writes only.",
91
91
  "properties": {
92
92
  "tokens": {
93
93
  "type": "integer",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "validate_json",
3
- "description": "Validate candidate output text against a JSON Schema. Returns {valid, feedback}; when invalid, feed the feedback back to the model and retry.",
3
+ "description": "Validate candidate output text against a JSON Schema. Returns {valid, feedback}; when invalid, feed the feedback back to the model and retry. The output may be any JSON value; otherwise the first fenced block, or the first object or array in the text, is used. $schema selects draft-04, draft-06, draft-07, 2019-09 or 2020-12 (default); $ref resolves only inside the one schema (\"#\", \"#/...\", \"#name\"), so a remote reference or an embedded $id is refused. pattern is Python re syntax, without inline flags, look-behind, backreferences, atomic or possessive groups, conditional groups or \\N{...}; a document's patterns get 500 ms in total.",
4
4
  "surfaces": [
5
5
  "mcp"
6
6
  ],
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "work_plan",
3
+ "description": "Work plan: turn a dependency graph into the batches that may run in parallel. Takes `nodes`, each `{id, depends_on, priority}` — `depends_on` absent or null means none, `priority` defaults to 0 — and answers `{\"result\": \"plan\", \"batches\", \"sequence\", \"width\"}`: batch k holds every node whose dependencies all appear in batches below k, `sequence` is those batches flattened in order, and `width` is the largest batch length — the widest fan-out, which is the number an orchestrator needs to decide whether it can afford the batch. Order inside a batch is priority descending, then the order the nodes were given; both halves are contract, because determinism inside a batch is what two implementations are held to. Empty `nodes` is an ANSWER, not a refusal: no batches, no sequence, width 0. Four refusals, checked in this order and one sentence each: `nodes[<i>].<field> must be ...` for a node whose `id` is not a string, whose `depends_on` is not a list of strings or whose `priority` is not an integer (named by position, never coerced), `duplicate node id <id>`, `node <id> depends on <dep>, which no node declares`, and `the graph has a cycle: <a> -> <b> -> <a>`. It reports what the graph permits and decides nothing about what may actually be dispatched together — `depends_on` encodes logical order, not file contention, so a batch this tool calls parallel may still hold work that writes one file. Pure computation over the nodes it was handed: it opens no file and takes no path. Never mutates.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "properties": {
9
+ "nodes": {
10
+ "title": "Nodes",
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "properties": {
15
+ "id": {
16
+ "type": "string",
17
+ "description": "The node's identifier, unique across `nodes`"
18
+ },
19
+ "depends_on": {
20
+ "anyOf": [
21
+ {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "string"
25
+ }
26
+ },
27
+ {
28
+ "type": "null"
29
+ }
30
+ ],
31
+ "description": "Ids this node waits for; every one of them must be declared by some node. Optional: absent or null means none"
32
+ },
33
+ "priority": {
34
+ "type": "integer",
35
+ "description": "Tie-break inside a batch, descending. Optional; 0 when absent"
36
+ }
37
+ },
38
+ "required": [
39
+ "id"
40
+ ]
41
+ }
42
+ }
43
+ },
44
+ "required": [
45
+ "nodes"
46
+ ],
47
+ "type": "object",
48
+ "title": "work_planArguments"
49
+ },
50
+ "output_schema": {
51
+ "type": "object",
52
+ "additionalProperties": true,
53
+ "title": "work_planDictOutput"
54
+ }
55
+ }
@@ -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.6"
32
+ __version__ = "0.35.8"
@@ -2,11 +2,24 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import builtins
6
+ import contextvars
7
+ import importlib
5
8
  import json
9
+ import queue
6
10
  import re
11
+ import subprocess
12
+ import sys
13
+ import threading
14
+ import time
15
+ from collections.abc import Iterator
16
+ from re import _constants as _sre
17
+ from re import _parser as _sre_parse
7
18
 
8
19
  import jsonschema
9
20
  import yaml
21
+ from jsonschema.exceptions import _WrappedReferencingError
22
+ from referencing.exceptions import NoSuchAnchor, PointerToNowhere
10
23
 
11
24
  from bantamkit.assets import AssetNotFound, assets_root
12
25
  from bantamkit.client import BantamError, Message
@@ -108,11 +121,17 @@ def validation_error_message(where: str, detail: str) -> str:
108
121
  return load_contract()["validation_error"].format(where=where, detail=detail)
109
122
 
110
123
 
111
- def extract_json(text: str) -> dict | list:
124
+ def _unfenced(text: str) -> str:
125
+ """The text `extract_json` searches: stripped, and the first fenced block's body if any."""
112
126
  text = text.strip()
113
127
  fence = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL)
114
128
  if fence:
115
129
  text = fence.group(1).strip()
130
+ return text
131
+
132
+
133
+ def extract_json(text: str) -> dict | list:
134
+ text = _unfenced(text)
116
135
  start_brace = text.find("{")
117
136
  start_bracket = text.find("[")
118
137
  if start_brace == -1 and start_bracket == -1:
@@ -127,20 +146,388 @@ def extract_json(text: str) -> dict | list:
127
146
  return obj
128
147
 
129
148
 
149
+ _REF_KEYWORDS = ("$ref", "$dynamicRef", "$recursiveRef")
150
+ # The keywords whose value holds subschemas, in ANY dialect — the walk the pre-scan takes.
151
+ # `runtime-ts/src/pyjsonschema.ts` (`subschemas`) is the same walk in the same order, which
152
+ # is what makes the two refusal sentences name the same place.
153
+ _WALK_SUBSCHEMA = frozenset(
154
+ {
155
+ "additionalItems",
156
+ "additionalProperties",
157
+ "contains",
158
+ "else",
159
+ "if",
160
+ "items",
161
+ "not",
162
+ "propertyNames",
163
+ "then",
164
+ "unevaluatedItems",
165
+ "unevaluatedProperties",
166
+ }
167
+ )
168
+ _WALK_MAP = frozenset(
169
+ {"$defs", "definitions", "dependencies", "dependentSchemas", "patternProperties", "properties"}
170
+ )
171
+ _WALK_LIST = frozenset({"allOf", "anyOf", "oneOf", "prefixItems"})
172
+
173
+
174
+ def _subschemas(schema: object, path: str = "") -> Iterator[tuple[str, dict]]:
175
+ """Every subschema of `schema`, root first, in document order, with its path.
176
+
177
+ Structural, not blind: a PROPERTY named `$ref` is not a reference, and this walk never
178
+ mistakes it for one.
179
+ """
180
+ if not isinstance(schema, dict):
181
+ return
182
+ yield path, schema
183
+ for k, v in schema.items():
184
+ at = k if path == "" else f"{path}/{k}"
185
+ if k == "items" and isinstance(v, list):
186
+ for i, x in enumerate(v):
187
+ yield from _subschemas(x, f"{at}/{i}")
188
+ elif k in _WALK_SUBSCHEMA:
189
+ yield from _subschemas(v, at)
190
+ elif k in _WALK_MAP and isinstance(v, dict):
191
+ for n, x in v.items():
192
+ yield from _subschemas(x, f"{at}/{n}")
193
+ elif k in _WALK_LIST and isinstance(v, list):
194
+ for i, x in enumerate(v):
195
+ yield from _subschemas(x, f"{at}/{i}")
196
+
197
+
198
+ def _draft3_sentence(uri: str) -> str:
199
+ return (
200
+ f"the schema's $schema {json.dumps(uri)} names draft 3, which this validator does not "
201
+ "support (draft-04, draft-06, draft-07, 2019-09 and 2020-12 are)"
202
+ )
203
+
204
+
205
+ def _names_draft3(uri: object) -> bool:
206
+ if not isinstance(uri, str):
207
+ return False
208
+ found = jsonschema.validators.validator_for({"$schema": uri}, default=None)
209
+ return found is jsonschema.Draft3Validator
210
+
211
+
212
+ # ---- `pattern`: the dialect both runtimes share, and its clock (#131, #132) ----------------
213
+
214
+ # The opcodes `re` parses that the Node port cannot translate faithfully: look-behind
215
+ # (fixed-width here, any width in JS), backreferences (a JS backreference to a group that did
216
+ # not take part matches the empty string; here it fails), atomic groups, possessive
217
+ # quantifiers and conditional groups. Inline flags and `\N{...}` are found below.
218
+ _UNSUPPORTED_OPS = frozenset(
219
+ {_sre.ATOMIC_GROUP, _sre.POSSESSIVE_REPEAT, _sre.GROUPREF, _sre.GROUPREF_EXISTS}
220
+ )
221
+
222
+
223
+ def _names_a_character(pattern: str) -> bool:
224
+ """`\\N{...}` anywhere: it parses to a plain LITERAL, so the tree cannot show it."""
225
+ i = 0
226
+ while i < len(pattern):
227
+ if pattern[i] == "\\":
228
+ if pattern[i + 1 : i + 2] == "N":
229
+ return True
230
+ i += 2
231
+ else:
232
+ i += 1
233
+ return False
234
+
235
+
236
+ def _tree_unsupported(items: object) -> bool:
237
+ for op, av in items: # type: ignore[attr-defined]
238
+ if op in _UNSUPPORTED_OPS:
239
+ return True
240
+ if op in (_sre.ASSERT, _sre.ASSERT_NOT):
241
+ direction, sub = av
242
+ if direction < 0 or _tree_unsupported(sub):
243
+ return True
244
+ elif op is _sre.SUBPATTERN:
245
+ _group, add_flags, del_flags, sub = av
246
+ if add_flags or del_flags or _tree_unsupported(sub):
247
+ return True
248
+ elif op in (_sre.MAX_REPEAT, _sre.MIN_REPEAT):
249
+ if _tree_unsupported(av[2]):
250
+ return True
251
+ elif op is _sre.BRANCH:
252
+ if any(_tree_unsupported(sub) for sub in av[1]):
253
+ return True
254
+ return False
255
+
256
+
257
+ def _pattern_unsupported(pattern: str) -> bool:
258
+ """True when `re` parses `pattern` and it uses a construct outside the shared dialect.
259
+
260
+ A pattern `re` refuses is NOT this function's: it is left to `check_schema`, which raises
261
+ where it always raised. `runtime-ts/src/pyjsonschema.ts` (`translatePyPattern`) parses
262
+ with `re._parser`'s own grammar and marks the same constructs, which is what makes the
263
+ refusal one sentence on both runtimes rather than two verdicts.
264
+ """
265
+ # `\\N{...}` first, whatever else the pattern holds: the port has no Unicode name table,
266
+ # so it cannot tell a defined name from an undefined one, and neither side tries to.
267
+ if _names_a_character(pattern):
268
+ return True
269
+ try:
270
+ parsed = _sre_parse.parse(pattern)
271
+ except (re.error, OverflowError, ValueError, RecursionError):
272
+ return False
273
+ # `(?u)` is the default for a str pattern; any other global flag is refused.
274
+ if parsed.state.flags & ~re.UNICODE:
275
+ return True
276
+ return _tree_unsupported(parsed.data)
277
+
278
+
279
+ def _clip(text: str, limit: int = 200) -> str:
280
+ """At most `limit` codepoints of `text`, then `…(N more)` (#153)."""
281
+ if len(text) <= limit:
282
+ return text
283
+ return f"{text[:limit]}…({len(text) - limit} more)"
284
+
285
+
286
+ def _unsupported_pattern_sentence(pattern: str) -> str:
287
+ return (
288
+ f"the pattern {_clip(repr(pattern))} is not supported: this validator refuses inline "
289
+ "flags, look-behind, backreferences, atomic groups, possessive quantifiers, "
290
+ "conditional groups and \\N{...} escapes"
291
+ )
292
+
293
+
294
+ PATTERN_BUDGET_MS = 500
295
+
296
+
297
+ def _pattern_budget_sentence(pattern: str) -> str:
298
+ return (
299
+ f"the pattern {_clip(repr(pattern))} did not finish within the {PATTERN_BUDGET_MS} ms "
300
+ "this validator allows a document's patterns; rewrite it so it cannot backtrack "
301
+ "catastrophically, as (a+)+$ does"
302
+ )
303
+
304
+
305
+ class _PatternRefusal(Exception):
306
+ """Raised out of `jsonschema` mid-validation; `schema_error` answers its sentence."""
307
+
308
+
309
+ # The search runs in a child interpreter because `re` holds the GIL for the whole of a
310
+ # search: a catastrophic pattern in a worker thread still stops the event loop that answers
311
+ # `ping` (measured: 0 ticks of the main thread during a 372 ms search). A child can be killed;
312
+ # a running `re.search` cannot be interrupted from another thread on any platform.
313
+ _CHILD_SOURCE = """
314
+ import json, re, sys
315
+ for line in sys.stdin.buffer:
316
+ pattern, text = json.loads(line)
317
+ try:
318
+ out = "1" if re.search(pattern, text) else "0"
319
+ except Exception as e:
320
+ out = json.dumps([type(e).__name__, str(e)])
321
+ sys.stdout.write(out + "\\n")
322
+ sys.stdout.flush()
323
+ """
324
+
325
+
326
+ class _PatternChild:
327
+ """One long-lived `python -I -S` that answers `re.search`, restarted after a timeout."""
328
+
329
+ def __init__(self) -> None:
330
+ self._lock = threading.Lock()
331
+ self._proc: subprocess.Popen[bytes] | None = None
332
+ self._lines: queue.Queue[bytes | None] = queue.Queue()
333
+
334
+ def _start(self) -> subprocess.Popen[bytes]:
335
+ proc = subprocess.Popen(
336
+ [sys.executable, "-I", "-S", "-c", _CHILD_SOURCE],
337
+ stdin=subprocess.PIPE,
338
+ stdout=subprocess.PIPE,
339
+ stderr=subprocess.DEVNULL,
340
+ )
341
+ lines: queue.Queue[bytes | None] = queue.Queue()
342
+
343
+ def pump() -> None:
344
+ assert proc.stdout is not None
345
+ for line in proc.stdout:
346
+ lines.put(line)
347
+ lines.put(None)
348
+
349
+ threading.Thread(target=pump, daemon=True).start()
350
+ self._proc, self._lines = proc, lines
351
+ return proc
352
+
353
+ def _stop(self) -> None:
354
+ proc, self._proc = self._proc, None
355
+ if proc is not None:
356
+ proc.kill()
357
+ proc.wait()
358
+
359
+ def search(self, pattern: str, text: str, timeout: float) -> bool | None:
360
+ """The search's truth, or None when it ran past `timeout` seconds (the child is killed)."""
361
+ with self._lock:
362
+ proc = self._proc if self._proc is not None and self._proc.poll() is None else None
363
+ if proc is None:
364
+ self._stop()
365
+ proc = self._start()
366
+ assert proc.stdin is not None
367
+ proc.stdin.write(json.dumps([pattern, text]).encode("ascii") + b"\n")
368
+ proc.stdin.flush()
369
+ try:
370
+ line = self._lines.get(timeout=timeout)
371
+ except queue.Empty:
372
+ self._stop()
373
+ return None
374
+ if line is None:
375
+ self._stop()
376
+ raise RuntimeError("the pattern child exited without answering")
377
+ answer = line.decode("ascii").strip()
378
+ if answer in ("0", "1"):
379
+ return answer == "1"
380
+ kind, message = json.loads(answer)
381
+ raise (re.error if kind == "error" else getattr(builtins, kind, RuntimeError))(message)
382
+
383
+
384
+ _PATTERN_CHILD = _PatternChild()
385
+ _PATTERN_CLOCK: contextvars.ContextVar[list[float] | None] = contextvars.ContextVar(
386
+ "_PATTERN_CLOCK", default=None
387
+ )
388
+
389
+
390
+ class _BoundedRe:
391
+ """`jsonschema`'s `re`, with `search` charged to the running validation's clock.
392
+
393
+ Outside `schema_error` (no clock) it is `re` itself. Installed on the three `jsonschema`
394
+ modules that call `re.search` on a schema's pattern, so every keyword that searches —
395
+ `pattern`, `patternProperties`, `additionalProperties`, `unevaluatedProperties` — is
396
+ bounded without re-implementing any of them.
397
+ """
398
+
399
+ def __getattr__(self, name: str) -> object:
400
+ return getattr(re, name)
401
+
402
+ def search(self, pattern: object, string: object, flags: int = 0) -> object:
403
+ clock = _PATTERN_CLOCK.get()
404
+ if clock is None or flags or not isinstance(pattern, str) or not isinstance(string, str):
405
+ return re.search(pattern, string, flags) # type: ignore[arg-type]
406
+ remaining = PATTERN_BUDGET_MS / 1000 - clock[0]
407
+ if remaining <= 0:
408
+ raise _PatternRefusal(_pattern_budget_sentence(pattern))
409
+ started = time.monotonic()
410
+ try:
411
+ found = _PATTERN_CHILD.search(pattern, string, remaining)
412
+ finally:
413
+ clock[0] += time.monotonic() - started
414
+ if found is None:
415
+ raise _PatternRefusal(_pattern_budget_sentence(pattern))
416
+ return found
417
+
418
+
419
+ for _module in ("jsonschema._keywords", "jsonschema._utils", "jsonschema._legacy_keywords"):
420
+ importlib.import_module(_module).re = _BoundedRe() # type: ignore[attr-defined]
421
+
422
+
423
+ def _unsupported_schema(schema: dict) -> str | None:
424
+ """The one sentence BOTH runtimes answer for a schema the validator will not resolve.
425
+
426
+ `jsonschema` resolves a `$ref` against a base URI and a registry; the Node port resolves
427
+ it inside the one resource a caller schema is (`pyjsonschema.ts`, module header). So
428
+ that the two never disagree, the reference narrows itself to the same rule BEFORE
429
+ `jsonschema.validate` runs, eagerly and in document order: draft 3 anywhere, a reference
430
+ that is not fragment-only, an embedded `$id` (`id` under draft 4) beside any reference.
431
+ Each is answered as `{valid: false, feedback}` rather than raised — a model handed the
432
+ sentence can act on it (#125). A pointer to nowhere or an unknown anchor is found
433
+ lazily, from the `referencing` exceptions, in `schema_error`.
434
+ """
435
+ # `validator_for` on a non-string `$schema` raises (AttributeError) — both runtimes raise.
436
+ cls = jsonschema.validators.validator_for(schema)
437
+ if cls is jsonschema.Draft3Validator:
438
+ return _draft3_sentence(schema["$schema"])
439
+ id_key = "id" if cls is jsonschema.Draft4Validator else "$id"
440
+ refs: list[tuple[str, str]] = []
441
+ ids: list[str] = []
442
+ for path, sub in _subschemas(schema):
443
+ if path != "":
444
+ if _names_draft3(sub.get("$schema")):
445
+ return _draft3_sentence(sub["$schema"])
446
+ if isinstance(sub.get(id_key), str):
447
+ ids.append(path)
448
+ for k in _REF_KEYWORDS:
449
+ v = sub.get(k)
450
+ if isinstance(v, str):
451
+ refs.append((k, v))
452
+ for k, v in refs:
453
+ # `$recursiveRef`'s value is never read by the reference, so it is never judged.
454
+ if k != "$recursiveRef" and v != "" and not v.startswith("#"):
455
+ return (
456
+ f"the reference {json.dumps(v)} cannot be resolved: only references into this "
457
+ 'schema ("#", "#/..." and "#name") are supported'
458
+ )
459
+ if refs and ids:
460
+ return (
461
+ f'the schema declares "{id_key}" at {ids[0]}; references are only resolved in a '
462
+ "schema with a single resource"
463
+ )
464
+ # #131: every `pattern` and `patternProperties` key, in document order.
465
+ for _path, sub in _subschemas(schema):
466
+ candidates = []
467
+ if isinstance(sub.get("pattern"), str):
468
+ candidates.append(sub["pattern"])
469
+ if isinstance(sub.get("patternProperties"), dict):
470
+ candidates.extend(sub["patternProperties"])
471
+ for candidate in candidates:
472
+ if isinstance(candidate, str) and _pattern_unsupported(candidate):
473
+ return _unsupported_pattern_sentence(candidate)
474
+ return None
475
+
476
+
130
477
  def schema_error(output: str, schema: dict) -> str | None:
131
- """Return a pointed validation error for `output`, or None if it satisfies `schema`."""
478
+ """Return a pointed validation error for `output`, or None if it satisfies `schema`.
479
+
480
+ A schema the validator will not resolve (see `_unsupported_schema`) returns its one
481
+ sentence here too, so `validate_json` answers it instead of raising.
482
+ """
132
483
  try:
133
- data = extract_json(output)
134
- except ValueError as e:
135
- return parse_error_message(e)
484
+ # #152: a whole-text JSON document of any type first — `"x"`, `1.0` and `null` are
485
+ # JSON texts — and only then the object/array search `extract_json` does.
486
+ data = json.loads(_unfenced(output))
487
+ except ValueError:
488
+ try:
489
+ data = extract_json(output)
490
+ except ValueError as e:
491
+ return parse_error_message(e)
492
+ if isinstance(schema, dict):
493
+ refusal = _unsupported_schema(schema)
494
+ if refusal is not None:
495
+ return refusal
496
+ token = _PATTERN_CLOCK.set([0.0])
136
497
  try:
137
498
  jsonschema.validate(data, schema)
499
+ except _PatternRefusal as e:
500
+ return str(e)
138
501
  except jsonschema.ValidationError as e:
139
502
  where = "/".join(str(p) for p in e.absolute_path) or "root"
140
- return validation_error_message(where, e.message)
503
+ return validation_error_message(_clip(where), _clip_detail(e.message, e.instance))
504
+ except _WrappedReferencingError as e:
505
+ wrapped = e._wrapped
506
+ if isinstance(wrapped, PointerToNowhere):
507
+ ref = json.dumps("#" + wrapped.ref)
508
+ return f"the reference {ref} points at nothing in this schema"
509
+ if isinstance(wrapped, NoSuchAnchor):
510
+ anchor = json.dumps("#" + wrapped.anchor)
511
+ return f"the reference {anchor} names no anchor in this schema"
512
+ raise
513
+ finally:
514
+ _PATTERN_CLOCK.reset(token)
141
515
  return None
142
516
 
143
517
 
518
+ def _clip_detail(message: str, instance: object) -> str:
519
+ """#153: the offending value quoted at most 200 codepoints, the sentence at most 1000.
520
+
521
+ Every `repr` of the instance in the message is clipped first, so a `maxLength` failure on a
522
+ 1 MiB string still says `is too long`; the whole sentence is then capped, which bounds the
523
+ values a message quotes that are PARTS of the instance (an unexpected property's name).
524
+ """
525
+ quoted = repr(instance)
526
+ if len(quoted) > 200:
527
+ message = message.replace(quoted, _clip(quoted))
528
+ return _clip(message, 1000)
529
+
530
+
144
531
  def render_evidence(messages: list[Message], budget: int = 4096) -> str:
145
532
  """Tool call/observation pairs from a run's transcript, as critic-readable lines."""
146
533
  contract = load_contract()
@@ -196,10 +196,23 @@ class EventLog:
196
196
  path: Path | None,
197
197
  cap_bytes: int = CAP_BYTES,
198
198
  clock: Callable[[], int] = _default_clock,
199
+ requires_dir: Path | None = None,
199
200
  ) -> None:
200
201
  self.path = path
201
202
  self.cap_bytes = cap_bytes
202
203
  self._clock = clock
204
+ #: job65/#154: the directory that must already exist before a record is written.
205
+ #:
206
+ #: `from_env` sets it to the store root when the switch is `on` (the default file
207
+ #: lives INSIDE the store). WAS: the `mkdir` in `_append` brought a never-saved store
208
+ #: into existence -- `--store <absent>` plus one `work_plan` call left
209
+ #: `<store>/events/mcp.jsonl` behind, and the next layered session walking up through
210
+ #: it bound that directory as its store. The log does not create the store: while the
211
+ #: store is not there a record has nowhere to go and is DROPPED -- not redirected, and
212
+ #: not counted as `write_failed`, because nothing was offered to the filesystem. The
213
+ #: first save creates the store; records after it land. A literal path is the
214
+ #: operator's own choice and keeps `requires_dir` None.
215
+ self.requires_dir = requires_dir
203
216
  #: Set the first time `record` swallows an `OSError`, and never cleared.
204
217
  #:
205
218
  #: FAILING TO LOG STILL NEVER FAILS THE TOOL — that contract is unchanged and
@@ -225,7 +238,13 @@ class EventLog:
225
238
  clock: Callable[[], int] = _default_clock,
226
239
  ) -> EventLog:
227
240
  source = os.environ if env is None else env
228
- return cls(resolve_path(store_root, source.get(EVENT_LOG_ENV)), clock=clock)
241
+ raw = source.get(EVENT_LOG_ENV)
242
+ in_store = raw is not None and raw.strip().lower() in _ON
243
+ return cls(
244
+ resolve_path(store_root, raw),
245
+ clock=clock,
246
+ requires_dir=Path(store_root) if in_store else None,
247
+ )
229
248
 
230
249
  @property
231
250
  def enabled(self) -> bool:
@@ -280,6 +299,10 @@ class EventLog:
280
299
  intent, not a live path.
281
300
  """
282
301
  assert self.path is not None
302
+ # job65/#154: `is_dir()` raises PermissionError for EACCES, which lands in `record`'s
303
+ # `except OSError` as a lost record -- the verdict `eventlog.ts` reaches too.
304
+ if self.requires_dir is not None and not self.requires_dir.is_dir():
305
+ return
283
306
  # AUDIT FINDING (J51-1): this `mkdir(parents=True)` can bring a whole `.bantamkit`
284
307
  # directory into existence on its own -- `BANTAMKIT_EVENT_LOG=on` with a project
285
308
  # store that was never saved to reaches here first -- entirely bypassing