bantamkit 0.37.1__tar.gz → 0.37.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 (303) hide show
  1. {bantamkit-0.37.1 → bantamkit-0.37.2}/PKG-INFO +3 -3
  2. {bantamkit-0.37.1 → bantamkit-0.37.2}/README.md +2 -2
  3. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/__init__.py +1 -1
  4. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/component.py +24 -9
  5. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillaudit.py +101 -20
  6. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skilldraft.py +4 -2
  7. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillfile.py +64 -17
  8. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillinstall.py +168 -12
  9. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillpromote.py +62 -3
  10. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillpropose.py +287 -1
  11. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillreport.py +18 -3
  12. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillaudit.py +43 -0
  13. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillfile.py +15 -0
  14. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillimprove_signals.py +62 -0
  15. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillpromote.py +155 -0
  16. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillreport.py +26 -0
  17. {bantamkit-0.37.1 → bantamkit-0.37.2}/.gitignore +0 -0
  18. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/contracts/default.yaml +0 -0
  19. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/manifest.yaml +0 -0
  20. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/HISTORY.md +0 -0
  21. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/README.md +0 -0
  22. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
  23. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
  24. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
  25. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
  26. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
  27. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
  28. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
  29. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
  30. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
  31. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
  32. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
  33. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
  34. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
  35. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
  36. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
  37. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
  38. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
  39. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
  40. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
  41. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
  42. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
  43. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
  44. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
  45. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
  46. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
  47. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
  48. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
  49. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
  50. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
  51. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
  52. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
  53. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
  54. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/fixtures/.gitkeep +0 -0
  55. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/fixtures/catalog.json +0 -0
  56. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/perturbations/task-completion.yaml +0 -0
  57. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/.gitkeep +0 -0
  58. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/extract-contact.yaml +0 -0
  59. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/extract-invoice.yaml +0 -0
  60. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/extract-order.yaml +0 -0
  61. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/extract-schedule.yaml +0 -0
  62. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/extract-versions.yaml +0 -0
  63. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
  64. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
  65. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
  66. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
  67. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-db-port.yaml +0 -0
  68. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-deploy.yaml +0 -0
  69. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
  70. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
  71. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-oncall.yaml +0 -0
  72. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
  73. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/recall-owner.yaml +0 -0
  74. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
  75. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
  76. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/shop-compare.yaml +0 -0
  77. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
  78. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
  79. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/evals/tasks/shop-total.yaml +0 -0
  80. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/pricing/default.json +0 -0
  81. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/profiles/default.yaml +0 -0
  82. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/profiles/patient.yaml +0 -0
  83. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/rubrics/.gitkeep +0 -0
  84. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/rubrics/code-quality.yaml +0 -0
  85. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/rubrics/grounded-completion.yaml +0 -0
  86. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/rubrics/task-completion.yaml +0 -0
  87. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/schemas/shiftwork-checkpoint.json +0 -0
  88. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/skills/.gitkeep +0 -0
  89. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/skills/file-graph.md +0 -0
  90. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/skills/memory-queries.md +0 -0
  91. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/skills/memory.md +0 -0
  92. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/.gitkeep +0 -0
  93. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/bantamkit_read.json +0 -0
  94. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/bantamkit_status.json +0 -0
  95. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/build_identity.json +0 -0
  96. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/document_list.json +0 -0
  97. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/document_read.json +0 -0
  98. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/file_graph.json +0 -0
  99. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/memory_compact.json +0 -0
  100. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/memory_dream.json +0 -0
  101. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/memory_queries.json +0 -0
  102. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/memory_recall.json +0 -0
  103. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/memory_save.json +0 -0
  104. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/repo_map.json +0 -0
  105. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/shiftwork_clock_in.json +0 -0
  106. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/shiftwork_clock_out.json +0 -0
  107. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/shiftwork_plan.json +0 -0
  108. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/shiftwork_status.json +0 -0
  109. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/skill_audit.json +0 -0
  110. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/token_ledger.json +0 -0
  111. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/validate_json.json +0 -0
  112. {bantamkit-0.37.1 → bantamkit-0.37.2}/_assets/tools/work_plan.json +0 -0
  113. {bantamkit-0.37.1 → bantamkit-0.37.2}/hatch_build.py +0 -0
  114. {bantamkit-0.37.1 → bantamkit-0.37.2}/pyproject.toml +0 -0
  115. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/agent.py +0 -0
  116. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/assets.py +0 -0
  117. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/budget.py +0 -0
  118. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/client.py +0 -0
  119. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/contract.py +0 -0
  120. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/criticreplay.py +0 -0
  121. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/critique.py +0 -0
  122. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/docmanifest.py +0 -0
  123. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/docread.py +0 -0
  124. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/evalrun.py +0 -0
  125. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/eventlog.py +0 -0
  126. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/filegraph.py +0 -0
  127. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/hookadapter.py +0 -0
  128. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/hostinstall.py +0 -0
  129. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/loopguard.py +0 -0
  130. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/mcpreport.py +0 -0
  131. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/mcpserver.py +0 -0
  132. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/mcpstdio.py +0 -0
  133. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/__init__.py +0 -0
  134. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/__main__.py +0 -0
  135. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/divergence.py +0 -0
  136. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/dream.py +0 -0
  137. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/layers.py +0 -0
  138. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/native.py +0 -0
  139. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/queries.py +0 -0
  140. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/retrieval.py +0 -0
  141. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/skills.py +0 -0
  142. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/store.py +0 -0
  143. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/memory/usage.py +0 -0
  144. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/pdfread.py +0 -0
  145. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/pricing.py +0 -0
  146. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/profile.py +0 -0
  147. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/repomap.py +0 -0
  148. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/selfupdate.py +0 -0
  149. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/shiftwork.py +0 -0
  150. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillhost.py +0 -0
  151. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/skillmine.py +0 -0
  152. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/statusline.py +0 -0
  153. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/storage/__init__.py +0 -0
  154. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/storage/coldindex.py +0 -0
  155. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/storage/hotindex.py +0 -0
  156. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/storage/lock.py +0 -0
  157. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/storage/logsegment.py +0 -0
  158. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/storage/retention.py +0 -0
  159. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/storage/tiers.py +0 -0
  160. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/structured.py +0 -0
  161. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/textutil.py +0 -0
  162. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/tokenledger.py +0 -0
  163. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/__init__.py +0 -0
  164. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/bashturn.py +0 -0
  165. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/context.py +0 -0
  166. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/graph.py +0 -0
  167. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/log.py +0 -0
  168. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/recallfile.py +0 -0
  169. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/refs.py +0 -0
  170. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/repo.py +0 -0
  171. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/touch/summary.py +0 -0
  172. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/updatecheck.py +0 -0
  173. {bantamkit-0.37.1 → bantamkit-0.37.2}/src/bantamkit/workplan.py +0 -0
  174. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/cli_exit_status_probe.py +0 -0
  175. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/conftest.py +0 -0
  176. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/bad-crc.docx +0 -0
  177. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/charref-4301-digits.html +0 -0
  178. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/charset-table.json +0 -0
  179. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/compression-method-9.docx +0 -0
  180. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/corrupt-deflate.docx +0 -0
  181. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/encrypted-member.docx +0 -0
  182. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/encrypted-mimetype.odt +0 -0
  183. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
  184. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/internal-dtd-entity.docx +0 -0
  185. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/rfc2231-charset.eml +0 -0
  186. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/rfc822-nested-twice.eml +0 -0
  187. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
  188. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/docread/x-uuencode.eml +0 -0
  189. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/f8404ab-perturbation-baseline.json +0 -0
  190. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/platform-assumption-baseline.json +0 -0
  191. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/data/served-tool-surface.json +0 -0
  192. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/docread_fixtures.py +0 -0
  193. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/perturbation_baseline_harness.py +0 -0
  194. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/rbp16_effect_probe.py +0 -0
  195. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/rbp18_payload_probe.py +0 -0
  196. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_adapter.py +0 -0
  197. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_agent.py +0 -0
  198. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_amendguard.py +0 -0
  199. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_assets.py +0 -0
  200. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_bantamkit_gitignore.py +0 -0
  201. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_bantamkit_read_tool.py +0 -0
  202. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_budget.py +0 -0
  203. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_build_identity.py +0 -0
  204. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_cli_oneshot.py +0 -0
  205. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_client.py +0 -0
  206. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_compaction_corpus_survey.py +0 -0
  207. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_conformance.py +0 -0
  208. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_conformance_harness_resilience.py +0 -0
  209. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_conformance_suite_table_gate.py +0 -0
  210. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_contract_fanout.py +0 -0
  211. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_contract_patterns.py +0 -0
  212. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_contract_refs.py +0 -0
  213. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_criticreplay.py +0 -0
  214. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_critique.py +0 -0
  215. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_doc_commands_gate.py +0 -0
  216. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_docread.py +0 -0
  217. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_docread_ceilings.py +0 -0
  218. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_document_manifest_parity.py +0 -0
  219. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_document_setup.py +0 -0
  220. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_document_tasks.py +0 -0
  221. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_document_tools.py +0 -0
  222. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_encoding_gate.py +0 -0
  223. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_evalrun.py +0 -0
  224. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_eventlog.py +0 -0
  225. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_field_program_gates.py +0 -0
  226. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_field_programs.py +0 -0
  227. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_filegraph.py +0 -0
  228. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_hookadapter.py +0 -0
  229. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_hostinstall.py +0 -0
  230. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_hostinstall_hooks.py +0 -0
  231. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_install_shape.py +0 -0
  232. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_ladder_statistics.py +0 -0
  233. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_launcher_which.py +0 -0
  234. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_layers.py +0 -0
  235. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_loopguard.py +0 -0
  236. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_mcp_endpoint.py +0 -0
  237. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_mcp_request_guard.py +0 -0
  238. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_mcpdrift.py +0 -0
  239. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_mcpreport.py +0 -0
  240. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_mcpserver.py +0 -0
  241. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_mcpstdio_frames.py +0 -0
  242. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory.py +0 -0
  243. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_compact_tool.py +0 -0
  244. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_component.py +0 -0
  245. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_divergence.py +0 -0
  246. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_dream.py +0 -0
  247. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_layers.py +0 -0
  248. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_lifecycle.py +0 -0
  249. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_native.py +0 -0
  250. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_queries.py +0 -0
  251. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_retrieval.py +0 -0
  252. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_skills.py +0 -0
  253. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_store_tripwire.py +0 -0
  254. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_strings.py +0 -0
  255. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_memory_usage.py +0 -0
  256. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_mutmatrix.py +0 -0
  257. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_nativeexport.py +0 -0
  258. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_newline_gate.py +0 -0
  259. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_packaging.py +0 -0
  260. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_pdfread.py +0 -0
  261. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_pinharness_ledger.py +0 -0
  262. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_platform_assumption_gate.py +0 -0
  263. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_pricing.py +0 -0
  264. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_readme_separation.py +0 -0
  265. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_repo_map_tool.py +0 -0
  266. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_repomap.py +0 -0
  267. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_selfupdate.py +0 -0
  268. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_served_tool_count_records.py +0 -0
  269. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_shiftwork.py +0 -0
  270. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillaudit_roots.py +0 -0
  271. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skilldraft.py +0 -0
  272. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillhost.py +0 -0
  273. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillimprove.py +0 -0
  274. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillinstall.py +0 -0
  275. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillmine.py +0 -0
  276. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillmine_daily.py +0 -0
  277. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_skillpropose.py +0 -0
  278. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_status_surface.py +0 -0
  279. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_statusline.py +0 -0
  280. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_bar.py +0 -0
  281. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_coldindex.py +0 -0
  282. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_hotindex.py +0 -0
  283. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_log.py +0 -0
  284. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_log_consumers.py +0 -0
  285. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_recall_index.py +0 -0
  286. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_retention.py +0 -0
  287. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_storage_tiers.py +0 -0
  288. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_structured.py +0 -0
  289. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_thread_exception_gate.py +0 -0
  290. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_tokenledger.py +0 -0
  291. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_tool_annotations.py +0 -0
  292. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_tool_manifest.py +0 -0
  293. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_touch_bashturn.py +0 -0
  294. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_touch_context.py +0 -0
  295. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_touch_graph.py +0 -0
  296. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_touch_log.py +0 -0
  297. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_touch_recall_file.py +0 -0
  298. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_touch_refs.py +0 -0
  299. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_touch_summary.py +0 -0
  300. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_unknown_arguments.py +0 -0
  301. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_updatecheck.py +0 -0
  302. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_version_agreement.py +0 -0
  303. {bantamkit-0.37.1 → bantamkit-0.37.2}/tests/test_workplan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bantamkit
3
- Version: 0.37.1
3
+ Version: 0.37.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
@@ -144,9 +144,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
144
144
  one platform only), copy `wheels/` across, and install from it:
145
145
 
146
146
  ```bash
147
- python -m pip download "bantamkit[mcp]==0.37.1" -d wheels
147
+ python -m pip download "bantamkit[mcp]==0.37.2" -d wheels
148
148
  python -m venv <env>
149
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.37.1"
149
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.37.2"
150
150
  <env>/bin/bantamkit-mcp --install cursor
151
151
  ```
152
152
 
@@ -113,9 +113,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
113
113
  one platform only), copy `wheels/` across, and install from it:
114
114
 
115
115
  ```bash
116
- python -m pip download "bantamkit[mcp]==0.37.1" -d wheels
116
+ python -m pip download "bantamkit[mcp]==0.37.2" -d wheels
117
117
  python -m venv <env>
118
- <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.37.1"
118
+ <env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.37.2"
119
119
  <env>/bin/bantamkit-mcp --install cursor
120
120
  ```
121
121
 
@@ -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.37.1"
32
+ __version__ = "0.37.2"
@@ -375,7 +375,8 @@ class Memory:
375
375
 
376
376
  `only_present`: bound only when some root is a directory, so a machine with no skill
377
377
  has exactly the layers it had before the layer existed. A short-lived hook sees a
378
- skill the moment it is installed; a long-lived server retries on recall.
378
+ skill the moment it is installed; a long-lived server retries on each public read
379
+ (`_bind_pending_skills`).
379
380
  """
380
381
  if not roots:
381
382
  return
@@ -384,6 +385,21 @@ class Memory:
384
385
  return
385
386
  self._layers.append((SKILL_LAYER, SkillStore(roots), False))
386
387
 
388
+ def _bind_pending_skills(self) -> None:
389
+ """The `[skill]` layer `_add_skills` held back, bound once a root has appeared.
390
+
391
+ Called on entry to every public reader of `_layers`, never between two reads in one
392
+ call: a layer inserted mid-call would shift the indexes that call already holds.
393
+ """
394
+ if self._pending_skills and any(r.is_dir() for r in self._pending_skills):
395
+ layer = (SKILL_LAYER, SkillStore(self._pending_skills), False)
396
+ position = next(
397
+ (i for i, (label, _, _) in enumerate(self._layers) if label == NATIVE_LAYER),
398
+ len(self._layers),
399
+ )
400
+ self._layers.insert(position, layer)
401
+ self._pending_skills = []
402
+
387
403
  def _add_native(self, native: str | Path | None) -> None:
388
404
  """The host's auto-memory as the last, read-only layer, when a directory was resolved."""
389
405
  if native is not None and str(native).strip():
@@ -673,14 +689,7 @@ class Memory:
673
689
  """
674
690
  if not 0.0 <= min_ratio <= 1.0:
675
691
  raise MemoryValidationError(MIN_RATIO_RANGE)
676
- if self._pending_skills and any(r.is_dir() for r in self._pending_skills):
677
- layer = (SKILL_LAYER, SkillStore(self._pending_skills), False)
678
- position = next(
679
- (i for i, (label, _, _) in enumerate(self._layers) if label == NATIVE_LAYER),
680
- len(self._layers),
681
- )
682
- self._layers.insert(position, layer)
683
- self._pending_skills = []
692
+ self._bind_pending_skills()
684
693
  budget = self.k if k is None else max(k, self.k)
685
694
  name = query.strip()
686
695
  lookup: str | None = None
@@ -739,6 +748,7 @@ class Memory:
739
748
 
740
749
  def layer_stores(self) -> list[tuple[str, Any, bool]]:
741
750
  """`(label, store, writable)` per layer, in precedence order (the touch graph's input)."""
751
+ self._bind_pending_skills()
742
752
  return list(self._layers)
743
753
 
744
754
  def recall_file(
@@ -758,6 +768,7 @@ class Memory:
758
768
  `store.lookup(name, stamp=False)`, and a name no layer holds any more is passed over.
759
769
  `status` is `answered` when a fact is shown, else `none`.
760
770
  """
771
+ self._bind_pending_skills()
761
772
  rf = _recall_file_mod
762
773
  budget = self.k if k is None else max(k, self.k)
763
774
  ignored = "" if query is None else f"\n{rf.QUERY_IGNORED}"
@@ -1054,11 +1065,13 @@ class Memory:
1054
1065
 
1055
1066
  def inject_candidates(self, prompt: str, head: str | None = None) -> list[Fact]:
1056
1067
  """What the `UserPromptSubmit` hook would inject for `prompt`, before its byte cap."""
1068
+ self._bind_pending_skills()
1057
1069
  got = self._pooled(prompt, 3, RECALL_MIN_SCORE_RATIO, head=head)
1058
1070
  return [fact for _, fact in got[0]]
1059
1071
 
1060
1072
  def rank_all(self, prompt: str, depth: int = 50, head: str | None = None) -> list[Fact]:
1061
1073
  """The ranking past the gate, `depth` deep: what recall@3 is measured on."""
1074
+ self._bind_pending_skills()
1062
1075
  got = self._pooled(prompt, depth, RECALL_MIN_SCORE_RATIO, head=head)
1063
1076
  return [fact for _, fact in got[0]]
1064
1077
 
@@ -1110,6 +1123,7 @@ class Memory:
1110
1123
  name order, each with its body's first `BODY_EXCERPT` code points, plus the
1111
1124
  instructions for writing theirs. A name whose fact cannot be read is passed over and
1112
1125
  is not counted in `remaining`."""
1126
+ self._bind_pending_skills()
1113
1127
  missing = self._missing(self._named_facts())
1114
1128
  remaining = len(missing)
1115
1129
  facts: list[dict[str, str]] = []
@@ -1147,6 +1161,7 @@ class Memory:
1147
1161
  """`memory_queries {"facts": [...]}` (Q6): every item is checked before anything is
1148
1162
  written, and the first bad one refuses the whole call (S5-S8). Then one write per
1149
1163
  layer sidecar; one that cannot be written is S9. `queries: []` removes an entry."""
1164
+ self._bind_pending_skills()
1150
1165
  labels = [label for label, _, _ in self._layers]
1151
1166
  plan: dict[int, dict[str, tuple[list[str], str] | None]] = {}
1152
1167
  for i, item in enumerate(facts):
@@ -328,6 +328,10 @@ class _Skill:
328
328
  #: `BAD_*` when the frontmatter is malformed, `None` when it is not.
329
329
  malformed: str | None = None
330
330
  phrases: tuple[str, ...] = field(default_factory=tuple)
331
+ #: Reached through a symlinked directory (#245); `_drop_aliases` reads it.
332
+ linked: bool = False
333
+ #: The real path of the directory holding the file, compared and never reported.
334
+ real: str = ""
331
335
 
332
336
  @property
333
337
  def in_plugin(self) -> bool:
@@ -368,34 +372,77 @@ def _relative_parts(path: Path, root: Path) -> list[str]:
368
372
  return list(path.relative_to(root).parts)
369
373
 
370
374
 
371
- def _scan_tree(root: Path) -> tuple[list[Path], list[tuple[str, str, str]]]:
372
- """One walk, two answers: every `SKILL.md` under `root`, and every VERSION DIRECTORY.
375
+ def _scan_tree(root: Path) -> tuple[list[Path], list[tuple[str, str, str]], frozenset[Path]]:
376
+ """One walk, three answers: every `SKILL.md` under `root`, every VERSION DIRECTORY, and
377
+ which of those files were reached THROUGH a symlinked directory.
373
378
 
374
- `os.walk` yields directory entries in whatever order the filesystem hands them over, and
375
- two machines hand them over differently. Sorting is what makes the scan order — and
376
- therefore the omission `what` lists and the duplicate tie-break's "last wins" — the same
377
- everywhere. `followlinks` stays off: a symlink loop is not a skill catalogue.
379
+ The walk is depth-first with each directory's entries in code-point order, and `found` is
380
+ sorted by path parts after it, which is the same order. Sorting is what makes the scan
381
+ order — and therefore the omission `what` lists, the duplicate tie-break's "last wins" and
382
+ which of two links to one directory is walked — the same everywhere. A directory that will
383
+ not open is skipped in silence, as `os.walk(onerror=None)` did.
384
+
385
+ SYMLINKED DIRECTORIES ARE FOLLOWED (#245, #234 N4). A skill directory a person linked into
386
+ `~/.claude/skills` is loaded by the host every session, so a walk that stops at the link
387
+ under-reports the catalogue by every such skill. A link is NOT followed when its real path
388
+ is on the chain of real directories above it (a link back to an ancestor: the loop) or is
389
+ a directory an earlier link in this walk already entered (two links to one directory: the
390
+ second is not walked). A dangling link is not a directory and is passed over; only a
391
+ dangling link NAMED `SKILL.md` is listed, and it is then `unreadable`, as before. Each
392
+ file reached through a link is in the third answer, and `_drop_aliases` later drops it
393
+ when its real directory is already counted — so a skill is counted once per real
394
+ directory, however many names reach it.
378
395
 
379
396
  THE SECOND ANSWER IS WHY THIS FUNCTION IS NOT JUST A FILE LIST. A version directory
380
397
  declares itself by holding a `skills/` directory — `<marketplace>/<plugin>/<version>/
381
398
  skills`, the first four segments of the path shape a skill has — and it declares itself
382
399
  whether or not anything under it can be read. Resolving over the files instead makes an
383
400
  EMPTY newer version invisible, so an older directory wins in silence; see
384
- `_resolve_versions`. A directory reached by following a symlink is not walked into on
385
- either runtime, so it declares nothing on either.
401
+ `_resolve_versions`. A directory reached through a symlink declares itself like any other,
402
+ because every skill under it carries that version and the resolution must know it.
386
403
  """
387
404
  found: list[Path] = []
388
405
  versions: list[tuple[str, str, str]] = []
389
- for dirpath, dirnames, filenames in os.walk(root, onerror=None):
390
- dirnames.sort()
391
- parts = _relative_parts(Path(dirpath), root)
392
- if len(parts) == VERSION_PATH_SEGMENTS and parts[3] == SKILLS_SEGMENT:
393
- versions.append((parts[0], parts[1], parts[2]))
394
- if SKILL_FILE in filenames:
395
- found.append(Path(dirpath) / SKILL_FILE)
406
+ linked: set[Path] = set()
407
+ entered: set[str] = set()
408
+
409
+ def visit(rel: list[str], chain: frozenset[str], via_link: bool) -> None:
410
+ here = root.joinpath(*rel)
411
+ try:
412
+ with os.scandir(here) as it:
413
+ entries = sorted(it, key=lambda e: e.name)
414
+ except OSError:
415
+ return
416
+ if len(rel) == VERSION_PATH_SEGMENTS and rel[3] == SKILLS_SEGMENT:
417
+ versions.append((rel[0], rel[1], rel[2]))
418
+ for entry in entries:
419
+ try:
420
+ directory = entry.is_dir()
421
+ except OSError:
422
+ directory = False
423
+ if not directory:
424
+ if entry.name == SKILL_FILE:
425
+ path = here / SKILL_FILE
426
+ found.append(path)
427
+ if via_link:
428
+ linked.add(path)
429
+ continue
430
+ real = os.path.realpath(here / entry.name)
431
+ if real in chain:
432
+ continue
433
+ # A Windows junction is a link to Node's `isSymbolicLink()`; it is one here too.
434
+ if entry.is_symlink() or getattr(entry, "is_junction", lambda: False)():
435
+ if real in entered:
436
+ continue
437
+ entered.add(real)
438
+ visit([*rel, entry.name], chain | {real}, True)
439
+ else:
440
+ visit([*rel, entry.name], chain | {real}, via_link)
441
+
442
+ visit([], frozenset({os.path.realpath(root)}), False)
396
443
  found.sort(key=lambda p: p.parts)
397
444
  versions.sort()
398
- return found, versions
445
+ return found, versions, frozenset(linked)
399
446
 
400
447
 
401
448
  def _walk(root: Path) -> list[Path]:
@@ -403,6 +450,38 @@ def _walk(root: Path) -> list[Path]:
403
450
  return _scan_tree(root)[0]
404
451
 
405
452
 
453
+ def _reach(skill: _Skill, path: Path, linked: frozenset[Path]) -> _Skill:
454
+ """Stamp how the walk reached `path`: through a link or not, and the real directory."""
455
+ skill.linked = path in linked
456
+ skill.real = os.path.realpath(path.parent)
457
+ return skill
458
+
459
+
460
+ def _drop_aliases(found: list[_Skill]) -> list[_Skill]:
461
+ """One counted skill per REAL directory (#245): a skill reached through a symlink is
462
+ dropped when its real directory is already counted — by a skill reached without a link
463
+ anywhere in the scan, or by an earlier linked one in scan order.
464
+
465
+ It runs after the version resolution and before the name dedupe, over the skills still
466
+ counted, and it removes the alias from the list rather than recording an omission: one
467
+ directory on disk is one skill, not a duplicate of itself. A skill reached WITHOUT a link
468
+ is never dropped here, so two roots that overlap on disk behave exactly as before, and a
469
+ skill #206 promoted by symlink is still counted once, at its source, because bantamkit's
470
+ roots come before the host's. Running after the resolution keeps a version alias (a
471
+ linked version directory that wins) counted rather than dropped in favour of a loser.
472
+ """
473
+ direct = {s.real for s in found if s.omitted is None and not s.linked}
474
+ seen: set[str] = set()
475
+ out: list[_Skill] = []
476
+ for skill in found:
477
+ if skill.omitted is None and skill.linked:
478
+ if skill.real in direct or skill.real in seen:
479
+ continue
480
+ seen.add(skill.real)
481
+ out.append(skill)
482
+ return out
483
+
484
+
406
485
  def _scalar_close(value: str, quote: str) -> int:
407
486
  """Index of the quote that CLOSES a scalar opened at index 0, or `-1` when none does.
408
487
 
@@ -719,10 +798,11 @@ def _scan(
719
798
  right from one that is right for two cancelling reasons.
720
799
  """
721
800
  base = Path(root)
722
- files, version_dirs = _scan_tree(base)
723
- found = [_load(path, base) for path in files]
801
+ files, version_dirs, linked = _scan_tree(base)
802
+ found = [_reach(_load(path, base), path, linked) for path in files]
724
803
  _apply_enabled(found, enabled)
725
804
  _resolve_versions(found, version_dirs, versions)
805
+ found = _drop_aliases(found)
726
806
  _apply_dedupe(found)
727
807
  return found
728
808
 
@@ -814,15 +894,16 @@ def scan_roots(
814
894
  version_dirs: list[tuple[str, str, str]] = []
815
895
  for label, root in roots:
816
896
  base = Path(root)
817
- files, dirs = _scan_tree(base)
897
+ files, dirs, linked = _scan_tree(base)
818
898
  for path in files:
819
- skill = _load(path, base)
899
+ skill = _reach(_load(path, base), path, linked)
820
900
  skill.relpath = f"{label}/{skill.relpath}"
821
901
  found.append(skill)
822
902
  version_dirs.extend(dirs)
823
903
  version_dirs.sort()
824
904
  _apply_enabled(found, enabled)
825
905
  _resolve_versions(found, version_dirs, versions)
906
+ found = _drop_aliases(found)
826
907
  _apply_dedupe(found)
827
908
  return found
828
909
 
@@ -497,8 +497,10 @@ def draft(
497
497
  home: str | Path,
498
498
  cwd: str | None,
499
499
  now: str,
500
+ own: str | Path | None = None,
500
501
  ) -> DraftOutcome:
501
- """Walk `chain` for one proposals record; log the outcome; never write a skill."""
502
+ """Walk `chain` for one proposals record; log the outcome; never write a skill. `own` is
503
+ passed to the gate (#247: a split's halves are gated with the skill being split as `own`)."""
502
504
  b = bundle(cluster)
503
505
  system, user = prompt(b)
504
506
  unreachable: list[str] = []
@@ -518,7 +520,7 @@ def draft(
518
520
  unreachable.append(d.label)
519
521
  continue
520
522
  text = unfence(answer)
521
- g = gate(text, b, home, cwd)
523
+ g = gate(text, b, home, cwd, own=own)
522
524
  outcome = DraftOutcome(d.label, g.ok, text if g.ok else None, g.reasons, unreachable)
523
525
  break
524
526
  if outcome is None:
@@ -306,8 +306,12 @@ def _fsync(fd: int) -> None:
306
306
  os.fsync(fd)
307
307
 
308
308
 
309
+ class _Taken(SkillFileError):
310
+ """A temp name already held by an entry this call did not make."""
311
+
312
+
309
313
  def _taken(path: str) -> SkillFileError:
310
- return SkillFileError(f"temp path {path} already exists; refusing to write through it")
314
+ return _Taken(f"temp path {path} already exists; refusing to write through it")
311
315
 
312
316
 
313
317
  def _write(path: str, data: bytes) -> None:
@@ -344,6 +348,26 @@ def _mkdir_temp(path: str) -> None:
344
348
  raise _taken(path) from None
345
349
 
346
350
 
351
+ # How many temp names one write tries before it refuses. A taken name is passed over, never
352
+ # written through (#250): an orphan a killed writer left under a recycled pid must not block
353
+ # every later writer, so only a run of taken names this long is refused.
354
+ TEMP_TRIES = 8
355
+
356
+
357
+ def _claim(target: Path | str, kind: str, make: Callable[[str], None]) -> str:
358
+ """A temp name beside `target` that `make` created exclusively; the last refusal if none."""
359
+ for _ in range(TEMP_TRIES - 1):
360
+ path = _temp(target, kind)
361
+ try:
362
+ make(path)
363
+ except _Taken:
364
+ continue
365
+ return path
366
+ path = _temp(target, kind)
367
+ make(path)
368
+ return path
369
+
370
+
347
371
  def _check_rel(rel: str) -> list[str]:
348
372
  parts = rel.split("/")
349
373
  if (
@@ -363,8 +387,7 @@ def _build(target: Path, files: dict[str, bytes]) -> str:
363
387
  for rel in rels:
364
388
  _check_rel(rel)
365
389
  target.parent.mkdir(parents=True, exist_ok=True)
366
- tmp = _temp(target, "tmp")
367
- _mkdir_temp(tmp)
390
+ tmp = _claim(target, "tmp", _mkdir_temp)
368
391
  try:
369
392
  for rel in rels:
370
393
  path = os.path.join(tmp, *rel.split("/"))
@@ -393,6 +416,23 @@ def write_dir_atomic(target: Path | str, files: dict[str, bytes]) -> None:
393
416
  raise
394
417
 
395
418
 
419
+ def rewrite_dir_atomic(target: Path | str, files: dict[str, bytes]) -> None:
420
+ """The existing directory `target` replaced by `files` in one rename, no backup (#244).
421
+
422
+ The new tree is built in an exclusively claimed temp dir beside `target`, the old one is
423
+ set aside into another (#250), and on any failure the old directory is back in place.
424
+ """
425
+ target = Path(target)
426
+ if not target.is_dir() or target.is_symlink():
427
+ raise SkillFileError(f"no skill at {target}")
428
+ tmp = _build(target, files)
429
+ try:
430
+ _swap_in(tmp, target)
431
+ except BaseException:
432
+ shutil.rmtree(tmp, ignore_errors=True)
433
+ raise
434
+
435
+
396
436
  def _copy_tree(src: Path, dst: str) -> None:
397
437
  """Directories and regular files into the existing `dst`, bytes as they are; a symlink is
398
438
  not followed or copied."""
@@ -414,8 +454,7 @@ def _set_aside(target: Path) -> tuple[str, str]:
414
454
  A rename onto the bare temp name would replace an empty dir planted there (POSIX allows
415
455
  it), so the name is claimed by an exclusive mkdir and `target` goes inside it (#250).
416
456
  """
417
- old = _temp(target, "old")
418
- _mkdir_temp(old)
457
+ old = _claim(target, "old", _mkdir_temp)
419
458
  moved = os.path.join(old, target.name)
420
459
  try:
421
460
  _rename(str(target), moved)
@@ -462,8 +501,7 @@ def _new_backup(backup_root: Path | str, name: str, now: str, fill: Callable[[st
462
501
  while os.path.lexists(final):
463
502
  n += 1
464
503
  final = home / f"{base}-{n}"
465
- tmp = _temp(final, "tmp")
466
- _mkdir_temp(tmp) # outside the try: a taken name is not ours to remove
504
+ tmp = _claim(final, "tmp", _mkdir_temp) # outside the try: a taken name is not ours to remove
467
505
  try:
468
506
  fill(tmp)
469
507
  _rename(tmp, str(final))
@@ -541,8 +579,7 @@ def undo(backup_root: Path | str, name: str, target: Path | str) -> str:
541
579
  shutil.rmtree(backup, ignore_errors=True)
542
580
  return f"removed {name}; it did not exist before {entry}"
543
581
  target.parent.mkdir(parents=True, exist_ok=True)
544
- tmp = _temp(target, "tmp")
545
- _mkdir_temp(tmp) # outside the try: a taken name is not ours to remove
582
+ tmp = _claim(target, "tmp", _mkdir_temp) # outside the try: a taken name is not ours to remove
546
583
  try:
547
584
  _copy_tree(backup, tmp)
548
585
  _swap_in(tmp, target)
@@ -566,18 +603,28 @@ def history_add(skill_dir: Path | str, version: int) -> None:
566
603
  _write_file_atomic(dst, data)
567
604
 
568
605
 
606
+ def _unlink_quietly(path: str) -> None:
607
+ try:
608
+ os.unlink(path)
609
+ except OSError:
610
+ pass
611
+
612
+
569
613
  def _write_file_atomic(path: Path, data: bytes) -> None:
570
- tmp = _temp(path, "tmp")
614
+ def make(tmp: str) -> None:
615
+ try:
616
+ _write(tmp, data)
617
+ except _Taken:
618
+ raise # what sits at a taken name is not ours to unlink
619
+ except BaseException:
620
+ _unlink_quietly(tmp)
621
+ raise
622
+
623
+ tmp = _claim(path, "tmp", make)
571
624
  try:
572
- _write(tmp, data)
573
625
  _rename(tmp, str(path))
574
- except SkillFileError:
575
- raise # the temp name was taken: what sits there is not ours to unlink
576
626
  except BaseException:
577
- try:
578
- os.unlink(tmp)
579
- except OSError:
580
- pass
627
+ _unlink_quietly(tmp)
581
628
  raise
582
629
 
583
630