bantamkit 0.36.1__tar.gz → 0.36.2__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 (240) hide show
  1. {bantamkit-0.36.1 → bantamkit-0.36.2}/PKG-INFO +12 -7
  2. {bantamkit-0.36.1 → bantamkit-0.36.2}/README.md +11 -6
  3. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/skills/memory.md +1 -0
  4. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/memory_recall.json +9 -1
  5. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/memory_save.json +17 -1
  6. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/__init__.py +1 -1
  7. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/hookadapter.py +159 -52
  8. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/mcpserver.py +24 -16
  9. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/memory/__main__.py +123 -2
  10. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/memory/component.py +26 -5
  11. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/memory/store.py +224 -2
  12. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/served-tool-surface.json +26 -2
  13. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_eventlog.py +1 -1
  14. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_hookadapter.py +311 -3
  15. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_mcpserver.py +1 -1
  16. bantamkit-0.36.2/tests/test_memory_lifecycle.py +293 -0
  17. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_unknown_arguments.py +1 -1
  18. {bantamkit-0.36.1 → bantamkit-0.36.2}/.gitignore +0 -0
  19. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/contracts/default.yaml +0 -0
  20. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/manifest.yaml +0 -0
  21. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/HISTORY.md +0 -0
  22. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/README.md +0 -0
  23. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
  24. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
  25. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
  26. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
  27. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
  28. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
  29. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
  30. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
  31. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
  32. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
  33. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
  34. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
  35. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
  36. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
  37. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
  38. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
  39. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
  40. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
  41. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
  42. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
  43. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
  44. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
  45. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
  46. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
  47. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
  48. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
  49. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
  50. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
  51. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
  52. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
  53. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
  54. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
  55. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/fixtures/.gitkeep +0 -0
  56. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/fixtures/catalog.json +0 -0
  57. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/perturbations/task-completion.yaml +0 -0
  58. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/.gitkeep +0 -0
  59. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/extract-contact.yaml +0 -0
  60. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/extract-invoice.yaml +0 -0
  61. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/extract-order.yaml +0 -0
  62. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/extract-schedule.yaml +0 -0
  63. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/extract-versions.yaml +0 -0
  64. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
  65. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
  66. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
  67. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
  68. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-db-port.yaml +0 -0
  69. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-deploy.yaml +0 -0
  70. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
  71. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
  72. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-oncall.yaml +0 -0
  73. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
  74. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/recall-owner.yaml +0 -0
  75. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
  76. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
  77. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/shop-compare.yaml +0 -0
  78. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
  79. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
  80. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/evals/tasks/shop-total.yaml +0 -0
  81. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/pricing/default.json +0 -0
  82. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/profiles/default.yaml +0 -0
  83. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/profiles/patient.yaml +0 -0
  84. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/rubrics/.gitkeep +0 -0
  85. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/rubrics/code-quality.yaml +0 -0
  86. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/rubrics/grounded-completion.yaml +0 -0
  87. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/rubrics/task-completion.yaml +0 -0
  88. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/schemas/shiftwork-checkpoint.json +0 -0
  89. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/skills/.gitkeep +0 -0
  90. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/skills/file-graph.md +0 -0
  91. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/.gitkeep +0 -0
  92. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/bantamkit_read.json +0 -0
  93. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/bantamkit_status.json +0 -0
  94. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/build_identity.json +0 -0
  95. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/document_list.json +0 -0
  96. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/document_read.json +0 -0
  97. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/file_graph.json +0 -0
  98. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/memory_compact.json +0 -0
  99. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/memory_dream.json +0 -0
  100. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/repo_map.json +0 -0
  101. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/shiftwork_clock_in.json +0 -0
  102. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/shiftwork_clock_out.json +0 -0
  103. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/shiftwork_plan.json +0 -0
  104. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/shiftwork_status.json +0 -0
  105. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/skill_audit.json +0 -0
  106. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/token_ledger.json +0 -0
  107. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/validate_json.json +0 -0
  108. {bantamkit-0.36.1 → bantamkit-0.36.2}/_assets/tools/work_plan.json +0 -0
  109. {bantamkit-0.36.1 → bantamkit-0.36.2}/hatch_build.py +0 -0
  110. {bantamkit-0.36.1 → bantamkit-0.36.2}/pyproject.toml +0 -0
  111. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/agent.py +0 -0
  112. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/assets.py +0 -0
  113. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/budget.py +0 -0
  114. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/client.py +0 -0
  115. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/contract.py +0 -0
  116. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/criticreplay.py +0 -0
  117. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/critique.py +0 -0
  118. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/docmanifest.py +0 -0
  119. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/docread.py +0 -0
  120. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/evalrun.py +0 -0
  121. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/eventlog.py +0 -0
  122. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/filegraph.py +0 -0
  123. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/hostinstall.py +0 -0
  124. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/loopguard.py +0 -0
  125. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/mcpreport.py +0 -0
  126. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/mcpstdio.py +0 -0
  127. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/memory/__init__.py +0 -0
  128. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/memory/divergence.py +0 -0
  129. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/memory/dream.py +0 -0
  130. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/memory/layers.py +0 -0
  131. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/pdfread.py +0 -0
  132. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/pricing.py +0 -0
  133. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/profile.py +0 -0
  134. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/repomap.py +0 -0
  135. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/selfupdate.py +0 -0
  136. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/shiftwork.py +0 -0
  137. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/skillaudit.py +0 -0
  138. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/statusline.py +0 -0
  139. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/structured.py +0 -0
  140. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/textutil.py +0 -0
  141. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/tokenledger.py +0 -0
  142. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/updatecheck.py +0 -0
  143. {bantamkit-0.36.1 → bantamkit-0.36.2}/src/bantamkit/workplan.py +0 -0
  144. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/cli_exit_status_probe.py +0 -0
  145. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/conftest.py +0 -0
  146. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/bad-crc.docx +0 -0
  147. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/charref-4301-digits.html +0 -0
  148. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/charset-table.json +0 -0
  149. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/compression-method-9.docx +0 -0
  150. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/corrupt-deflate.docx +0 -0
  151. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/encrypted-member.docx +0 -0
  152. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/encrypted-mimetype.odt +0 -0
  153. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
  154. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/internal-dtd-entity.docx +0 -0
  155. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/rfc2231-charset.eml +0 -0
  156. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/rfc822-nested-twice.eml +0 -0
  157. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
  158. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/docread/x-uuencode.eml +0 -0
  159. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/f8404ab-perturbation-baseline.json +0 -0
  160. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/data/platform-assumption-baseline.json +0 -0
  161. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/docread_fixtures.py +0 -0
  162. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/perturbation_baseline_harness.py +0 -0
  163. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/rbp16_effect_probe.py +0 -0
  164. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/rbp18_payload_probe.py +0 -0
  165. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_adapter.py +0 -0
  166. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_agent.py +0 -0
  167. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_amendguard.py +0 -0
  168. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_assets.py +0 -0
  169. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_bantamkit_gitignore.py +0 -0
  170. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_bantamkit_read_tool.py +0 -0
  171. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_budget.py +0 -0
  172. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_build_identity.py +0 -0
  173. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_cli_oneshot.py +0 -0
  174. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_client.py +0 -0
  175. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_compaction_corpus_survey.py +0 -0
  176. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_conformance.py +0 -0
  177. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_conformance_harness_resilience.py +0 -0
  178. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_conformance_suite_table_gate.py +0 -0
  179. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_contract_fanout.py +0 -0
  180. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_contract_patterns.py +0 -0
  181. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_contract_refs.py +0 -0
  182. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_criticreplay.py +0 -0
  183. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_critique.py +0 -0
  184. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_doc_commands_gate.py +0 -0
  185. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_docread.py +0 -0
  186. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_docread_ceilings.py +0 -0
  187. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_document_manifest_parity.py +0 -0
  188. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_document_setup.py +0 -0
  189. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_document_tasks.py +0 -0
  190. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_document_tools.py +0 -0
  191. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_encoding_gate.py +0 -0
  192. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_evalrun.py +0 -0
  193. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_field_program_gates.py +0 -0
  194. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_field_programs.py +0 -0
  195. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_filegraph.py +0 -0
  196. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_hostinstall.py +0 -0
  197. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_hostinstall_hooks.py +0 -0
  198. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_install_shape.py +0 -0
  199. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_ladder_statistics.py +0 -0
  200. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_launcher_which.py +0 -0
  201. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_layers.py +0 -0
  202. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_loopguard.py +0 -0
  203. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_mcp_endpoint.py +0 -0
  204. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_mcp_request_guard.py +0 -0
  205. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_mcpdrift.py +0 -0
  206. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_mcpreport.py +0 -0
  207. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_mcpstdio_frames.py +0 -0
  208. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory.py +0 -0
  209. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory_compact_tool.py +0 -0
  210. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory_component.py +0 -0
  211. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory_divergence.py +0 -0
  212. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory_dream.py +0 -0
  213. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory_layers.py +0 -0
  214. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory_store_tripwire.py +0 -0
  215. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_memory_strings.py +0 -0
  216. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_mutmatrix.py +0 -0
  217. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_nativeexport.py +0 -0
  218. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_newline_gate.py +0 -0
  219. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_packaging.py +0 -0
  220. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_pdfread.py +0 -0
  221. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_pinharness_ledger.py +0 -0
  222. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_platform_assumption_gate.py +0 -0
  223. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_pricing.py +0 -0
  224. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_readme_separation.py +0 -0
  225. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_repo_map_tool.py +0 -0
  226. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_repomap.py +0 -0
  227. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_selfupdate.py +0 -0
  228. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_served_tool_count_records.py +0 -0
  229. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_shiftwork.py +0 -0
  230. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_skillaudit.py +0 -0
  231. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_status_surface.py +0 -0
  232. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_statusline.py +0 -0
  233. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_structured.py +0 -0
  234. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_thread_exception_gate.py +0 -0
  235. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_tokenledger.py +0 -0
  236. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_tool_annotations.py +0 -0
  237. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_tool_manifest.py +0 -0
  238. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_updatecheck.py +0 -0
  239. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_version_agreement.py +0 -0
  240. {bantamkit-0.36.1 → bantamkit-0.36.2}/tests/test_workplan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bantamkit
3
- Version: 0.36.1
3
+ Version: 0.36.2
4
4
  Summary: Persistent memory for coding agents: an MCP server for Claude Code, Claude Desktop, Cursor and VS Code Copilot, 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
@@ -74,8 +74,9 @@ Then restart the host (in Claude Code, open a new session) and ask the agent to
74
74
  - **Offline after one install.** `--install` records the venv's console script by absolute path,
75
75
  so no launch needs the network or your shell's PATH.
76
76
  - **Claude Code hooks, if you want them.** `--install-hooks` puts the memory index in front of
77
- each new session, surfaces matching facts on each prompt, and nudges a save at the end of a
78
- long session that saved nothing
77
+ each new session, surfaces matching facts on each prompt, mirrors each fact Claude Code writes
78
+ into its own auto-memory into the project store (one way, new writes only), and nudges a save at
79
+ the end of a long session that saved nothing
79
80
  ([docs/hooks.md](https://github.com/Ink01101011/bantamkit/blob/main/docs/hooks.md)).
80
81
  - **More than memory.** JSON Schema validation, a token ledger read off the host's transcripts,
81
82
  a skill-catalogue audit, and shift-work tools that plan and hand off multi-step jobs —
@@ -105,7 +106,7 @@ Then restart the host (in Claude Code, open a new session) and ask the agent to
105
106
  | [What it serves](#-what-it-serves) | 14 tools, one prompt, two resource templates |
106
107
  | [Requirements](#-requirements) | Python and dependencies |
107
108
  | [The asset pack](#-the-asset-pack) | `--assets-root`, `BANTAMKIT_ASSETS` |
108
- | [The operator CLI: `python -m bantamkit.memory`](#-the-operator-cli-python--m-bantamkitmemory) | status, lint, compact, archived, archive, restore |
109
+ | [The operator CLI: `python -m bantamkit.memory`](#-the-operator-cli-python--m-bantamkitmemory) | status, lint, compact, archived, archive, restore, list, candidates, merge |
109
110
  | [Where the Python and Node servers differ](#-where-the-python-and-node-servers-differ) | One store; pdf/.doc/.rtf, CLI name, `build_id`, YAML only PyYAML reads |
110
111
  | [Module API](#-module-api) | `import bantamkit`: the library half, measured from the wheel |
111
112
  | [Development](#-development) | Clone, test, lint, conformance |
@@ -144,9 +145,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
144
145
  one platform only), copy `wheels/` across, and install from it:
145
146
 
146
147
  ```bash
147
- python -m pip download "bantamkit[mcp]==0.36.1" -d wheels
148
+ python -m pip download "bantamkit[mcp]==0.36.2" -d wheels
148
149
  python -m venv <env>
149
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.36.1"
150
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.36.2"
150
151
  <env>/bin/bantamkit-mcp --install cursor
151
152
  ```
152
153
 
@@ -373,9 +374,13 @@ python -m bantamkit.memory compact # archive the stalest facts
373
374
  python -m bantamkit.memory archived # list what compaction has moved out
374
375
  python -m bantamkit.memory archive <name>
375
376
  python -m bantamkit.memory restore <name>
377
+ python -m bantamkit.memory list [--type T] [--outcome error|success] [--status active|done]
378
+ python -m bantamkit.memory candidates [--older-than DAYS] [--apply]
379
+ python -m bantamkit.memory merge --names A B --name NEW --type T --description D --body TEXT [--apply]
376
380
  ```
377
381
 
378
- `archive` moves a fact out without deleting it; `restore` brings it back by name.
382
+ `archive` moves a fact out without deleting it; `restore` brings it back by name. `candidates`
383
+ and `merge` are dry runs unless `--apply` is given.
379
384
  Full reference: [docs/memory.md → The operator CLI](https://github.com/Ink01101011/bantamkit/blob/main/docs/memory.md#the-operator-cli).
380
385
 
381
386
  ## 🔀 Where the Python and Node servers differ
@@ -43,8 +43,9 @@ Then restart the host (in Claude Code, open a new session) and ask the agent to
43
43
  - **Offline after one install.** `--install` records the venv's console script by absolute path,
44
44
  so no launch needs the network or your shell's PATH.
45
45
  - **Claude Code hooks, if you want them.** `--install-hooks` puts the memory index in front of
46
- each new session, surfaces matching facts on each prompt, and nudges a save at the end of a
47
- long session that saved nothing
46
+ each new session, surfaces matching facts on each prompt, mirrors each fact Claude Code writes
47
+ into its own auto-memory into the project store (one way, new writes only), and nudges a save at
48
+ the end of a long session that saved nothing
48
49
  ([docs/hooks.md](https://github.com/Ink01101011/bantamkit/blob/main/docs/hooks.md)).
49
50
  - **More than memory.** JSON Schema validation, a token ledger read off the host's transcripts,
50
51
  a skill-catalogue audit, and shift-work tools that plan and hand off multi-step jobs —
@@ -74,7 +75,7 @@ Then restart the host (in Claude Code, open a new session) and ask the agent to
74
75
  | [What it serves](#-what-it-serves) | 14 tools, one prompt, two resource templates |
75
76
  | [Requirements](#-requirements) | Python and dependencies |
76
77
  | [The asset pack](#-the-asset-pack) | `--assets-root`, `BANTAMKIT_ASSETS` |
77
- | [The operator CLI: `python -m bantamkit.memory`](#-the-operator-cli-python--m-bantamkitmemory) | status, lint, compact, archived, archive, restore |
78
+ | [The operator CLI: `python -m bantamkit.memory`](#-the-operator-cli-python--m-bantamkitmemory) | status, lint, compact, archived, archive, restore, list, candidates, merge |
78
79
  | [Where the Python and Node servers differ](#-where-the-python-and-node-servers-differ) | One store; pdf/.doc/.rtf, CLI name, `build_id`, YAML only PyYAML reads |
79
80
  | [Module API](#-module-api) | `import bantamkit`: the library half, measured from the wheel |
80
81
  | [Development](#-development) | Clone, test, lint, conformance |
@@ -113,9 +114,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
113
114
  one platform only), copy `wheels/` across, and install from it:
114
115
 
115
116
  ```bash
116
- python -m pip download "bantamkit[mcp]==0.36.1" -d wheels
117
+ python -m pip download "bantamkit[mcp]==0.36.2" -d wheels
117
118
  python -m venv <env>
118
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.36.1"
119
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.36.2"
119
120
  <env>/bin/bantamkit-mcp --install cursor
120
121
  ```
121
122
 
@@ -342,9 +343,13 @@ python -m bantamkit.memory compact # archive the stalest facts
342
343
  python -m bantamkit.memory archived # list what compaction has moved out
343
344
  python -m bantamkit.memory archive <name>
344
345
  python -m bantamkit.memory restore <name>
346
+ python -m bantamkit.memory list [--type T] [--outcome error|success] [--status active|done]
347
+ python -m bantamkit.memory candidates [--older-than DAYS] [--apply]
348
+ python -m bantamkit.memory merge --names A B --name NEW --type T --description D --body TEXT [--apply]
345
349
  ```
346
350
 
347
- `archive` moves a fact out without deleting it; `restore` brings it back by name.
351
+ `archive` moves a fact out without deleting it; `restore` brings it back by name. `candidates`
352
+ and `merge` are dry runs unless `--apply` is given.
348
353
  Full reference: [docs/memory.md → The operator CLI](https://github.com/Ink01101011/bantamkit/blob/main/docs/memory.md#the-operator-cli).
349
354
 
350
355
  ## 🔀 Where the Python and Node servers differ
@@ -12,6 +12,7 @@ past-you would have used. Do this before acting, not after.
12
12
  - The user corrects you or states a preference → type "feedback"
13
13
  - A durable fact about the user → "user", about ongoing work → "project",
14
14
  a URL/dashboard/ticket → "reference"
15
+ - Tag a lesson with outcome: error|success; close a project fact by saving it again with status: done.
15
16
 
16
17
  ## When NOT to save
17
18
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memory_recall",
3
3
  "title": "Recall memories",
4
- "description": "Search persistent memory for facts a past session saved. Call it BEFORE starting a task that resembles past work, and when the user refers to something decided earlier; use memory_save to add a fact, and memory_compact or memory_dream only to tidy the store — this tool never changes what a fact says. Reads every layer in order — the project store, any read-only grants in .bantamkit/config.yaml, then the machine-wide profile — and returns the top word matches, never fewer than the store default of 3 when that many match; a query that is exactly a fact's name returns that fact alone. Why it is not read-only: every hit is appended to the usage ledger (~/.bantamkit/usage/<store-id>.jsonl) as a recall event — the staleness order memory_compact archives by — and a project fact file still in the 0.35.x format is rewritten once into the current one; both are best-effort and never fail a recall. A fact file that cannot be read is named in the reply, never silently skipped.",
4
+ "description": "Search persistent memory for facts a past session saved. Call it BEFORE starting a task that resembles past work, and when the user refers to something decided earlier; use memory_save to add a fact, and memory_compact or memory_dream only to tidy the store — this tool never changes what a fact says. Reads every layer in order — the project store, any read-only grants in .bantamkit/config.yaml, then the machine-wide profile — and returns the top word matches, never fewer than the store default of 3 when that many match; a query that is exactly a fact's name returns that fact alone. Why it is not read-only: every hit is appended to the usage ledger (~/.bantamkit/usage/<store-id>.jsonl) as a recall event — the staleness order memory_compact archives by — and a project fact file still in the 0.35.x format is rewritten once into the current one; both are best-effort and never fail a recall. A fact file that cannot be read is named in the reply, never silently skipped. Pass outcome: error to see only recorded failures.",
5
5
  "surfaces": [
6
6
  "agent",
7
7
  "mcp"
@@ -19,6 +19,14 @@
19
19
  "k": {
20
20
  "type": "integer",
21
21
  "description": "Facts wanted, clamped to 1..5. Raised to the store default of 3 when smaller; never lowered below it."
22
+ },
23
+ "outcome": {
24
+ "type": "string",
25
+ "enum": [
26
+ "error",
27
+ "success"
28
+ ],
29
+ "description": "Keep only facts saved with this outcome — `error` answers \"what failed before\", `success` \"what worked\". Facts with no outcome are dropped from the answer; omit it for the normal search."
22
30
  }
23
31
  },
24
32
  "additionalProperties": false
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memory_save",
3
3
  "title": "Save a memory",
4
- "description": "Save ONE durable fact to persistent memory — a preference the user stated, a decision, the fix that worked — so a future session can recall it. Use it only for facts a future session needs; never for anything derivable from the repo, git history or docs, nor for one-off state of this conversation. To find a fact use memory_recall; to change one, save again under its EXACT name, which replaces it in place; to free room call memory_compact. Refused with nothing written, each with its own sentence: a description too close to an existing fact's (the reply names that fact — save under that SAME name or skip, never rename to force a copy), a spelling that normalises onto an existing name (`Deploy Command` onto `deploy-command`), and a save that would push the always-loaded index over its byte budget (the reply names the remedy). Every save lands in the writable project store (.bantamkit/memory), never in the profile or a read-only grant, and is appended to the usage ledger.",
4
+ "description": "Save ONE durable fact to persistent memory — a preference the user stated, a decision, the fix that worked — so a future session can recall it. Use it only for facts a future session needs; never for anything derivable from the repo, git history or docs, nor for one-off state of this conversation. To find a fact use memory_recall; to change one, save again under its EXACT name, which replaces it in place; to free room call memory_compact. Refused with nothing written, each with its own sentence: a description too close to an existing fact's (the reply names that fact — save under that SAME name or skip, never rename to force a copy), a spelling that normalises onto an existing name (`Deploy Command` onto `deploy-command`), and a save that would push the always-loaded index over its byte budget (the reply names the remedy). Every save lands in the writable project store (.bantamkit/memory), never in the profile or a read-only grant, and is appended to the usage ledger. Optional outcome (error|success) tags a lesson and status: done closes a project fact; nothing here archives — that is the operator's bantamkit-memory candidates/merge.",
5
5
  "surfaces": [
6
6
  "agent",
7
7
  "mcp"
@@ -44,6 +44,22 @@
44
44
  "type": "string"
45
45
  },
46
46
  "description": "names of related memories"
47
+ },
48
+ "outcome": {
49
+ "type": "string",
50
+ "enum": [
51
+ "error",
52
+ "success"
53
+ ],
54
+ "description": "What the lesson records: `error` (a failure to avoid) or `success` (a way that worked). Only on feedback and project facts; refused on user and reference. Read back by `bantamkit-memory list --outcome` and by memory_recall's `outcome` filter."
55
+ },
56
+ "status": {
57
+ "type": "string",
58
+ "enum": [
59
+ "active",
60
+ "done"
61
+ ],
62
+ "description": "Lifecycle of a project fact. Save an existing name with `done` when the work it describes has shipped; `bantamkit-memory candidates` lists done facts for the operator to archive. Absent means active; `active` is never written."
47
63
  }
48
64
  },
49
65
  "additionalProperties": false
@@ -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.36.1"
32
+ __version__ = "0.36.2"
@@ -73,9 +73,12 @@ from datetime import UTC, datetime
73
73
  from pathlib import Path
74
74
  from typing import Any
75
75
 
76
+ import yaml
77
+
78
+ from .client import BantamError
76
79
  from .memory.component import Memory
77
80
  from .memory.layers import discover_project_store, resolve_project_store
78
- from .memory.store import DURABLE_TYPES, Fact, MemoryStore, tokens
81
+ from .memory.store import DURABLE_TYPES, NAME_MAX_CHARS, Fact, MemoryStore, tokens
79
82
 
80
83
  # The process clock, for the `ms` on every log line. `time.time()` and not `monotonic`,
81
84
  # because the Node module reads `Date.now()` here and the two logs are read side by side.
@@ -1728,21 +1731,33 @@ def _post_save(run: HookRun, payload: dict[str, Any]) -> None:
1728
1731
  ledger["saved"] = int(ledger.get("saved") or 0) + 1
1729
1732
  _write_ledger(run, payload.get("session_id"), ledger)
1730
1733
  cwd = str(payload.get("cwd") or os.getcwd())
1731
- configured, scope = _configured_index_budget(run, cwd)
1732
- budget_source = "configured" if configured is not None else "default"
1733
- budget_scope = scope if configured is not None else None
1734
+ configured, _scope = _configured_index_budget(run, cwd)
1734
1735
  m = (
1735
1736
  Memory.layered(cwd)
1736
1737
  if configured is None
1737
1738
  else Memory.layered(cwd, index_budget=_budget_value(configured))
1738
1739
  )
1740
+ _compact_if_needed(run, payload, cwd, m, {"action": "saved"})
1741
+
1742
+
1743
+ def _compact_if_needed(
1744
+ run: HookRun, payload: dict[str, Any], cwd: str, m: Memory, record: dict[str, Any]
1745
+ ) -> None:
1746
+ """The auto-compact tail shared by `_post_save` and the native mirror (#228).
1747
+
1748
+ `record` already carries `action` (and for the mirror `file`, `name`, `nativeBranch`);
1749
+ it is logged as is when the index is under the band.
1750
+ """
1751
+ configured, scope = _configured_index_budget(run, cwd)
1752
+ budget_source = "configured" if configured is not None else "default"
1753
+ budget_scope = scope if configured is not None else None
1739
1754
  size, budget = m.index_accounting()
1740
1755
  if size is None or size < COMPACT_AT * budget:
1741
1756
  _log(
1742
1757
  run,
1743
1758
  {
1759
+ **record,
1744
1760
  "event": "PostToolUse",
1745
- "action": "saved",
1746
1761
  "bytes": size,
1747
1762
  "budget": budget,
1748
1763
  "budgetSource": budget_source,
@@ -2282,6 +2297,142 @@ def _maybe_dream(run: HookRun, payload: dict[str, Any]) -> None:
2282
2297
  )
2283
2298
 
2284
2299
 
2300
+ # ---------------------------------------------- PostToolUse[Write|Edit|MultiEdit] -> mirror
2301
+ # #228: a fact the host writes into its native memory directory is saved, one way, into the
2302
+ # project store. One file per call (the one the tool just wrote); never a scan, never a write
2303
+ # into the native directory, never a read of MEMORY.md.
2304
+
2305
+ MIRROR_TOOLS = frozenset({"Write", "Edit", "MultiEdit"})
2306
+ #: A `.md` path with a `memory` directory segment (the old Stop-nudge shape).
2307
+ _MEMORY_SEGMENT = re.compile(r"[\\/]memory[\\/].*\.md\Z", re.DOTALL)
2308
+ _WIKILINK = re.compile(r"\[\[([^\]]+)\]\]")
2309
+ _MIRROR_NAME = re.compile(r"^[a-z0-9][a-z0-9-]*\Z")
2310
+ _MIRROR_TYPES = ("user", "feedback", "project", "reference")
2311
+
2312
+
2313
+ def _mirror_name(value: Any) -> str | None:
2314
+ name = str(value).strip().lower().replace(".", "-").replace("_", "-")
2315
+ if not _MIRROR_NAME.match(name) or len(name) > NAME_MAX_CHARS:
2316
+ return None
2317
+ return name
2318
+
2319
+
2320
+ def _mirror_links(meta: dict[str, Any], body: str) -> list[str]:
2321
+ raw: list[str] = []
2322
+ listed = meta.get("links")
2323
+ if isinstance(listed, list):
2324
+ raw.extend(str(x) for x in listed)
2325
+ raw.extend(_WIKILINK.findall(body))
2326
+ out: list[str] = []
2327
+ for item in raw:
2328
+ name = _mirror_name(item)
2329
+ if name is not None and name not in out:
2330
+ out.append(name)
2331
+ return out
2332
+
2333
+
2334
+ def _parse_native_fact(text: str) -> tuple[dict[str, Any], str] | None:
2335
+ try:
2336
+ _, front, body = text.split("---\n", 2)
2337
+ meta = yaml.safe_load(front)
2338
+ except (ValueError, yaml.YAMLError):
2339
+ return None
2340
+ # NO KEYS IS NO FACT (J74-4): `{}` is a dict to PyYAML and an empty or comment-only
2341
+ # frontmatter is `None`; the port refuses `{}` as #120's empty flow mapping. One rule,
2342
+ # spelled the same on both sides, so all three are a `frontmatter` skip.
2343
+ if not isinstance(meta, dict) or not meta:
2344
+ return None
2345
+ return meta, body
2346
+
2347
+
2348
+ def _mirror_native_write(run: HookRun, payload: dict[str, Any]) -> None:
2349
+ ti = payload.get("tool_input")
2350
+ fp = ti.get("file_path") if isinstance(ti, dict) else None
2351
+ if not isinstance(fp, str) or not fp:
2352
+ return
2353
+ native = _resolve_native_memory(run, payload)
2354
+ base: dict[str, Any] = {"event": "PostToolUse", "file": fp, "nativeBranch": native.branch}
2355
+
2356
+ def skip(reason: str, **more: Any) -> None:
2357
+ _log(run, {"action": "mirror-skipped", **base, **more, "reason": reason})
2358
+
2359
+ inside = False
2360
+ if native.dir is not None and fp.endswith(".md") and os.path.basename(fp) != NATIVE_INDEX:
2361
+ root = os.path.realpath(native.dir)
2362
+ parent = os.path.realpath(os.path.dirname(fp))
2363
+ inside = parent == root or (parent + os.sep).startswith(root.rstrip(os.sep) + os.sep)
2364
+ if not inside:
2365
+ if (
2366
+ fp.endswith(".md")
2367
+ and _MEMORY_SEGMENT.search(fp)
2368
+ and os.path.basename(fp) != NATIVE_INDEX
2369
+ ):
2370
+ skip("not-native")
2371
+ return
2372
+ try:
2373
+ with open(fp, encoding="utf-8", errors="replace") as fh:
2374
+ text = fh.read()
2375
+ except OSError:
2376
+ skip("frontmatter")
2377
+ return
2378
+ parsed = _parse_native_fact(text)
2379
+ if parsed is None:
2380
+ skip("frontmatter")
2381
+ return
2382
+ meta, raw_body = parsed
2383
+ nested = meta.get("metadata") if isinstance(meta.get("metadata"), dict) else {}
2384
+ if nested.get("source") == "bantamkit":
2385
+ skip("export-stub")
2386
+ return
2387
+ source = meta.get("name")
2388
+ if not (isinstance(source, str) and source):
2389
+ source = os.path.basename(fp)[: -len(".md")]
2390
+ name = _mirror_name(source)
2391
+ if name is None:
2392
+ skip("name")
2393
+ return
2394
+ fact_type = meta.get("type") if isinstance(meta.get("type"), str) else nested.get("type")
2395
+ if fact_type not in _MIRROR_TYPES:
2396
+ fact_type = "project"
2397
+ # A list or a mapping is not a description (J74-4): `str()` and `String()` spell one
2398
+ # differently, so neither side may turn it into text.
2399
+ raw_description = meta.get("description")
2400
+ if isinstance(raw_description, (list, dict)):
2401
+ raw_description = None
2402
+ description = _collapse_whitespace(raw_description or "")
2403
+ if not description:
2404
+ skip("description", name=name)
2405
+ return
2406
+ body = raw_body.strip()
2407
+ if not body:
2408
+ skip("body", name=name)
2409
+ return
2410
+ links = _mirror_links(meta, body)
2411
+ cwd = str(payload.get("cwd") or os.getcwd())
2412
+ configured, _scope = _configured_index_budget(run, cwd)
2413
+ m = (
2414
+ Memory.layered(cwd)
2415
+ if configured is None
2416
+ else Memory.layered(cwd, index_budget=_budget_value(configured))
2417
+ )
2418
+ try:
2419
+ result = m.store.save(fact_type, name, description, body, tuple(links))
2420
+ except BantamError as e:
2421
+ _log(run, {"action": "mirror-refused", **base, "name": name, "reason": str(e)})
2422
+ return
2423
+ ledger = _read_ledger(run, payload.get("session_id"))
2424
+ ledger["saved"] = int(ledger.get("saved") or 0) + 1
2425
+ _write_ledger(run, payload.get("session_id"), ledger)
2426
+ action = "mirrored" if result.status == "saved" else "mirror-duplicate"
2427
+ _compact_if_needed(
2428
+ run,
2429
+ payload,
2430
+ cwd,
2431
+ m,
2432
+ {"action": action, "file": fp, "name": name, "nativeBranch": native.branch},
2433
+ )
2434
+
2435
+
2285
2436
  # -------------------------------------------------------------------------- Stop
2286
2437
  # The experience collector. Once per session, when the session did real work and nothing
2287
2438
  # durable was written, hand the turn back with one instruction. The host's `type:prompt`
@@ -2289,48 +2440,6 @@ def _maybe_dream(run: HookRun, payload: dict[str, Any]) -> None:
2289
2440
 
2290
2441
  _TOOL_USE = re.compile(r'"type":\s*"tool_use"')
2291
2442
  _SAVE_CALL = re.compile(r'"name":\s*"mcp__bantamkit__memory_save"')
2292
- #: The native write tools whose `file_path` can put a fact into a `memory/` directory.
2293
- _SAVE_WRITE_TOOLS = frozenset({"Write", "Edit", "MultiEdit"})
2294
- #: The path shape of a native memory write: a `.md` under any `memory/` directory. The class
2295
- #: is backslash-or-slash, spelled as a module constant because a backslash inside an f-string
2296
- #: expression is a syntax error before CPython 3.12 and this package supports 3.11.
2297
- _SAVE_WRITE_PATH = re.compile(r"[\\/]memory[\\/].*\.md\Z", re.DOTALL)
2298
- #: A line can only hold a native write if it names one of the tools; the rest are not parsed.
2299
- _SAVE_LINE_HINT = re.compile(r'"name":\s*"(?:Write|Edit|MultiEdit)"')
2300
-
2301
-
2302
- def _transcript_wrote_memory(text: str) -> bool:
2303
- """Did the transcript WRITE a native memory file (job65, #169)? Only a `tool_use` counts.
2304
-
2305
- A Write/Edit/MultiEdit whose `file_path` is a `.md` under a `memory/` directory. Before
2306
- this a regex over the whole text took ANY `"file_path"` under `memory/` -- a Read of
2307
- `docs/memory/notes.md` included -- as a save, and silenced the nudge. (A `memory_save`
2308
- call is still recognised by its tool name, as it always was; that half was never the
2309
- defect.) Lines are prefiltered on the tool names and only those parsed; a line that does
2310
- not parse is not a save.
2311
- """
2312
- for line in text.split("\n"):
2313
- if not _SAVE_LINE_HINT.search(line):
2314
- continue
2315
- try:
2316
- record = json.loads(line)
2317
- except ValueError:
2318
- continue
2319
- msg = record.get("message") if isinstance(record, dict) else None
2320
- content = msg.get("content") if isinstance(msg, dict) else None
2321
- if not isinstance(content, list):
2322
- continue
2323
- for block in content:
2324
- if not isinstance(block, dict) or block.get("type") != "tool_use":
2325
- continue
2326
- if block.get("name") not in _SAVE_WRITE_TOOLS:
2327
- continue
2328
- ti = block.get("input")
2329
- fp = ti.get("file_path") if isinstance(ti, dict) else None
2330
- if isinstance(fp, str) and _SAVE_WRITE_PATH.search(fp):
2331
- return True
2332
- return False
2333
-
2334
2443
 
2335
2444
  def _stop(run: HookRun, payload: dict[str, Any]) -> None:
2336
2445
  if payload.get("stop_hook_active"):
@@ -2346,11 +2455,7 @@ def _stop(run: HookRun, payload: dict[str, Any]) -> None:
2346
2455
  except OSError:
2347
2456
  return
2348
2457
  tool_uses = len(_TOOL_USE.findall(text))
2349
- saved = bool(
2350
- int(ledger.get("saved") or 0) > 0
2351
- or _SAVE_CALL.search(text)
2352
- or _transcript_wrote_memory(text)
2353
- )
2458
+ saved = bool(int(ledger.get("saved") or 0) > 0 or _SAVE_CALL.search(text))
2354
2459
  if tool_uses < STOP_NUDGE_MIN_TOOL_CALLS or saved:
2355
2460
  _log(run, {"event": "Stop", "action": "pass", "toolUses": tool_uses, "saved": saved})
2356
2461
  return
@@ -2435,6 +2540,8 @@ def run_hook(stdin: Any = None) -> None:
2435
2540
  _append_usage_event(run, payload)
2436
2541
  if payload.get("tool_name") == "mcp__bantamkit__memory_save":
2437
2542
  _post_save(run, payload)
2543
+ elif payload.get("tool_name") in MIRROR_TOOLS:
2544
+ _mirror_native_write(run, payload)
2438
2545
  return
2439
2546
  if event == "PreCompact":
2440
2547
  _pre_compact(run, payload)
@@ -1464,10 +1464,18 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
1464
1464
  return f'{document[:-2]},\n "bantamkit_degraded": {key}\n}}'
1465
1465
 
1466
1466
  def memory_save(
1467
- type: str, name: str, description: str, body: str, links: list[str] | None = None
1467
+ type: str,
1468
+ name: str,
1469
+ description: str,
1470
+ body: str,
1471
+ links: list[str] | None = None,
1472
+ outcome: str | None = None,
1473
+ status: str | None = None,
1468
1474
  ) -> str:
1469
1475
  with _record_raise(log, "memory_save"):
1470
- outcome = memory.save_outcome(type, name, description, body, links)
1476
+ outcome_ = memory.save_outcome(
1477
+ type, name, description, body, links, outcome=outcome, status=status
1478
+ )
1471
1479
  if log.enabled:
1472
1480
  # Only when a log is actually on: `index_accounting` re-parses `facts/`, and
1473
1481
  # a diagnostic must not put that on the path of an operator who did not ask
@@ -1477,10 +1485,10 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
1477
1485
  detail: dict[str, Any] = {"budget": budget}
1478
1486
  if index_bytes is not None:
1479
1487
  detail["index_bytes"] = index_bytes
1480
- log.record("memory_save", outcome.status, detail)
1481
- return _noted(outcome.reply)
1488
+ log.record("memory_save", outcome_.status, detail)
1489
+ return _noted(outcome_.reply)
1482
1490
 
1483
- def memory_recall(query: str, k: int | None = None) -> str:
1491
+ def memory_recall(query: str, k: int | None = None, outcome: str | None = None) -> str:
1484
1492
  if k is not None:
1485
1493
  # Clamped, not refused, and the manifest says so: `assets/tools/memory_recall.json`
1486
1494
  # used to advertise `minimum: 1, maximum: 5` on `k` while this line and the
@@ -1490,19 +1498,19 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
1490
1498
  # answer for a stricter one. The description now states the clamp.
1491
1499
  k = max(1, min(k, 5))
1492
1500
  with _record_raise(log, "memory_recall"):
1493
- outcome = memory.recall_outcome(query, k)
1501
+ outcome_ = memory.recall_outcome(query, k, outcome=outcome)
1494
1502
  detail = {
1495
- "budget": outcome.budget,
1496
- "candidates": outcome.candidates,
1497
- "layers": outcome.layers,
1498
- "reached": outcome.reached,
1499
- "returned": outcome.returned,
1500
- "unreadable": outcome.unreadable,
1503
+ "budget": outcome_.budget,
1504
+ "candidates": outcome_.candidates,
1505
+ "layers": outcome_.layers,
1506
+ "reached": outcome_.reached,
1507
+ "returned": outcome_.returned,
1508
+ "unreadable": outcome_.unreadable,
1501
1509
  }
1502
- if outcome.source is not None:
1503
- detail["source"] = outcome.source
1504
- log.record("memory_recall", outcome.status, detail)
1505
- return _noted(outcome.reply)
1510
+ if outcome_.source is not None:
1511
+ detail["source"] = outcome_.source
1512
+ log.record("memory_recall", outcome_.status, detail)
1513
+ return _noted(outcome_.reply)
1506
1514
 
1507
1515
  def memory_compact(reserve: int | None = None) -> str:
1508
1516
  """The model's half of compaction; the hook (`docs/hooks.md`) is the automatic half.