bantamkit 0.27.0__py3-none-any.whl

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 (119) hide show
  1. bantamkit/__init__.py +32 -0
  2. bantamkit/agent.py +458 -0
  3. bantamkit/assets/contracts/default.yaml +90 -0
  4. bantamkit/assets/evals/devteam/manifest.yaml +351 -0
  5. bantamkit/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. bantamkit/assets/evals/devteam/repo/README.md +12 -0
  7. bantamkit/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. bantamkit/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. bantamkit/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. bantamkit/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. bantamkit/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. bantamkit/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. bantamkit/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. bantamkit/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. bantamkit/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. bantamkit/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. bantamkit/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. bantamkit/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. bantamkit/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. bantamkit/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. bantamkit/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. bantamkit/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. bantamkit/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. bantamkit/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. bantamkit/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. bantamkit/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. bantamkit/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. bantamkit/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. bantamkit/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. bantamkit/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. bantamkit/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. bantamkit/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. bantamkit/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. bantamkit/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. bantamkit/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. bantamkit/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. bantamkit/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. bantamkit/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. bantamkit/assets/evals/fixtures/.gitkeep +0 -0
  40. bantamkit/assets/evals/fixtures/catalog.json +6 -0
  41. bantamkit/assets/evals/perturbations/task-completion.yaml +576 -0
  42. bantamkit/assets/evals/tasks/.gitkeep +0 -0
  43. bantamkit/assets/evals/tasks/extract-contact.yaml +14 -0
  44. bantamkit/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. bantamkit/assets/evals/tasks/extract-order.yaml +15 -0
  46. bantamkit/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. bantamkit/assets/evals/tasks/extract-versions.yaml +17 -0
  48. bantamkit/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. bantamkit/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. bantamkit/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. bantamkit/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. bantamkit/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. bantamkit/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. bantamkit/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. bantamkit/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. bantamkit/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. bantamkit/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. bantamkit/assets/evals/tasks/recall-owner.yaml +13 -0
  59. bantamkit/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. bantamkit/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. bantamkit/assets/evals/tasks/shop-compare.yaml +9 -0
  62. bantamkit/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. bantamkit/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. bantamkit/assets/evals/tasks/shop-total.yaml +9 -0
  65. bantamkit/assets/profiles/default.yaml +31 -0
  66. bantamkit/assets/profiles/patient.yaml +31 -0
  67. bantamkit/assets/rubrics/.gitkeep +0 -0
  68. bantamkit/assets/rubrics/code-quality.yaml +20 -0
  69. bantamkit/assets/rubrics/grounded-completion.yaml +37 -0
  70. bantamkit/assets/rubrics/task-completion.yaml +28 -0
  71. bantamkit/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. bantamkit/assets/skills/.gitkeep +0 -0
  73. bantamkit/assets/skills/file-graph.md +7 -0
  74. bantamkit/assets/skills/memory.md +35 -0
  75. bantamkit/assets/tools/.gitkeep +0 -0
  76. bantamkit/assets/tools/bantamkit_read.json +48 -0
  77. bantamkit/assets/tools/bantamkit_status.json +25 -0
  78. bantamkit/assets/tools/build_identity.json +17 -0
  79. bantamkit/assets/tools/document_list.json +12 -0
  80. bantamkit/assets/tools/document_read.json +31 -0
  81. bantamkit/assets/tools/file_graph.json +12 -0
  82. bantamkit/assets/tools/memory_compact.json +31 -0
  83. bantamkit/assets/tools/memory_recall.json +38 -0
  84. bantamkit/assets/tools/memory_save.json +61 -0
  85. bantamkit/assets/tools/shiftwork_clock_in.json +25 -0
  86. bantamkit/assets/tools/shiftwork_clock_out.json +60 -0
  87. bantamkit/assets/tools/shiftwork_status.json +25 -0
  88. bantamkit/assets/tools/skill_audit.json +70 -0
  89. bantamkit/assets/tools/validate_json.json +31 -0
  90. bantamkit/assets.py +67 -0
  91. bantamkit/budget.py +114 -0
  92. bantamkit/client.py +329 -0
  93. bantamkit/contract.py +522 -0
  94. bantamkit/criticreplay.py +3241 -0
  95. bantamkit/critique.py +301 -0
  96. bantamkit/docread.py +1744 -0
  97. bantamkit/evalrun.py +2003 -0
  98. bantamkit/eventlog.py +282 -0
  99. bantamkit/filegraph.py +218 -0
  100. bantamkit/loopguard.py +101 -0
  101. bantamkit/mcpreport.py +763 -0
  102. bantamkit/mcpserver.py +1334 -0
  103. bantamkit/memory/__init__.py +28 -0
  104. bantamkit/memory/__main__.py +291 -0
  105. bantamkit/memory/component.py +569 -0
  106. bantamkit/memory/divergence.py +744 -0
  107. bantamkit/memory/layers.py +257 -0
  108. bantamkit/memory/store.py +940 -0
  109. bantamkit/pdfread.py +1402 -0
  110. bantamkit/profile.py +46 -0
  111. bantamkit/shiftwork.py +212 -0
  112. bantamkit/skillaudit.py +853 -0
  113. bantamkit/statusline.py +313 -0
  114. bantamkit/structured.py +125 -0
  115. bantamkit/textutil.py +30 -0
  116. bantamkit-0.27.0.dist-info/METADATA +207 -0
  117. bantamkit-0.27.0.dist-info/RECORD +119 -0
  118. bantamkit-0.27.0.dist-info/WHEEL +4 -0
  119. bantamkit-0.27.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,257 @@
1
+ """Layer resolution: project-store discovery and explicit cross-project grants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import os
7
+ import stat as stat_module
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ import yaml
12
+
13
+ from bantamkit.memory.store import MemoryValidationError
14
+
15
+ PROJECT_STORE = Path(".bantamkit") / "memory"
16
+ CONFIG_NAME = "config.yaml"
17
+
18
+ # The operator's one lever over binding, and the only one they actually hold: an
19
+ # MCP server's cwd is chosen by the HOST, not by the user, so every rule derived
20
+ # from cwd is a rule the operator cannot control. The launch environment is the
21
+ # surface every host exposes — `{"command": ..., "env": {"BANTAMKIT_MEMORY_DIR":
22
+ # "/abs/path/to/store"}}`. The name is spelled here and nowhere else, including
23
+ # in the tests, because a magic string duplicated between a resolver and its test
24
+ # is how the two come to be reading different variables.
25
+ MEMORY_DIR_ENV = "BANTAMKIT_MEMORY_DIR"
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class StoreBinding:
30
+ """Which store the walk bound to, and whether that store can answer at all.
31
+
32
+ `state` is one of three, and they are three because two of them were one:
33
+
34
+ - "populated" — the walk found a store and it holds facts.
35
+ - "empty" — the walk found a store and it holds none. A recall against it
36
+ returns nothing, and that nothing means "wrong filing cabinet",
37
+ not "no match".
38
+ - "designated" — the walk found no store anywhere up the tree. `path` is where
39
+ one WOULD go; nothing is there and nothing was created.
40
+
41
+ Measured 2026-08-22, and this is why the state is named here instead of being
42
+ re-derived at each call site: MCP server pid 34377 (cwd
43
+ /Users/kktest/Documents/Claude/Projects/trader-platform) walks past a project
44
+ with no store of its own and binds to ~/.bantamkit/memory — created
45
+ 2026-08-10 22:15, 0 files in facts/, no index.md. Its `memory_recall` binds
46
+ successfully and returns the empty list a 64-fact store returns for a question
47
+ nothing matches. The same thing fires inside this repo's own linked worktrees:
48
+ a worktree has no `.bantamkit/` of its own, so
49
+ /Users/kktest/Documents/Claude/Projects/bantamkit-membind resolves to that same
50
+ empty home store while the canonical checkout resolves to its 64 facts. A
51
+ caller that only gets a path cannot tell those apart without re-running the
52
+ walk itself and hoping it reproduced it; `path` plus `state` is that answer.
53
+
54
+ `searched_from` is the resolved directory the walk started at, so a caller can
55
+ say why this store and not another without reconstructing the ascent. It is
56
+ `None` when no walk ran — see `origin`.
57
+
58
+ `origin` is "walk" or "pin". A pinned binding that still reported
59
+ `searched_from` would be asserting an ascent that never happened, and a
60
+ diagnostic field that lies is the same defect as a state field that lies.
61
+ Every consumer that wants to say WHY this store reads the pair.
62
+ """
63
+
64
+ path: Path
65
+ state: str # "populated" | "empty" | "designated"
66
+ fact_count: int
67
+ searched_from: Path | None
68
+ origin: str = "walk" # "walk" | "pin"
69
+
70
+
71
+ def discover_project_store(start: str | Path | None = None) -> Path:
72
+ """Walk up from `start` (default cwd) looking for an existing .bantamkit/memory.
73
+
74
+ Returns the nearest existing store dir; if none exists anywhere up the
75
+ tree, designates `start/.bantamkit/memory` without creating anything.
76
+ Ancestor path is fully resolved; the returned store path is not resolved
77
+ further — a symlinked store keeps its config beside the symlink. Callers
78
+ needing store identity comparison must resolve() at the comparison site.
79
+
80
+ `BANTAMKIT_MEMORY_DIR` outranks all of it — see `_pinned_store`. When a pin is
81
+ set, `start` is never consulted; when it is not, this is the walk it always was.
82
+
83
+ The path this returns does not move: `resolve_project_store` runs the same
84
+ walk and only adds what was found there.
85
+ """
86
+ pin = _pinned_store()
87
+ if pin is not None:
88
+ return pin
89
+ return _walk_to_store(_resolved_base(start))
90
+
91
+
92
+ def resolve_project_store(start: str | Path | None = None) -> StoreBinding:
93
+ """Same walk as `discover_project_store`, plus what is actually in the store.
94
+
95
+ Read-only in both directions: designating a path stays the non-destructive act
96
+ it already was, and an existing store is only listed, never touched.
97
+
98
+ Counted the way the store itself reads facts (`facts/*.md`, see
99
+ `MemoryStore._facts`), so a count of 0 here means recall has nothing to return,
100
+ not that the counting rule differs from the reading rule. An unreadable
101
+ `facts/` raises `MemoryValidationError` rather than reporting "empty" —
102
+ reporting a store you could not read as a store with nothing in it is the exact
103
+ conflation this exists to end, and it matches `load_grants` above, where a
104
+ grant that is wrong raises instead of being silently dropped. That raise is
105
+ `count_facts`' doing and only `count_facts`' doing: this docstring claimed it
106
+ for a release while `Path.glob` was quietly swallowing the error (measured: a
107
+ store holding one fact at mode 0o000 came back `state="empty", fact_count=0`).
108
+
109
+ A pin never yields "designated": `_pinned_store` has already established that
110
+ the pinned directory is there, or raised saying it is not.
111
+ """
112
+ pin = _pinned_store()
113
+ if pin is not None:
114
+ return _count_into_binding(pin, searched_from=None, origin="pin")
115
+ base = _resolved_base(start)
116
+ path = _walk_to_store(base)
117
+ if not path.is_dir():
118
+ return StoreBinding(
119
+ path=path, state="designated", fact_count=0, searched_from=base, origin="walk"
120
+ )
121
+ return _count_into_binding(path, searched_from=base, origin="walk")
122
+
123
+
124
+ def count_facts(root: str | Path) -> int:
125
+ """How many `facts/*.md` a store holds, counted so that unreadable is never zero.
126
+
127
+ `Path.glob` is unusable here, and that is the entire reason this function exists:
128
+ it suppresses the `OSError` raised by its own directory scan and yields nothing,
129
+ so `facts/` at mode 0o000 with a fact in it counts 0 — a store you could not read,
130
+ reported as a store with nothing in it. `os.scandir` raises, and each caller
131
+ decides what to do with the raise; nobody gets to be told "empty" by accident.
132
+
133
+ A `facts/` that is simply absent is 0 and not an error: nothing is stored there
134
+ and that much really is readable. The name filter mirrors `MemoryStore._facts`'
135
+ `glob("*.md")` exactly — dotfiles and directories included, as `glob` includes
136
+ them — because a count that disagrees with the read is the other half of the
137
+ same lie (`test_a_readable_store_is_still_counted_exactly_as_before`).
138
+ """
139
+ facts = Path(root) / "facts"
140
+ try:
141
+ with os.scandir(facts) as entries:
142
+ return sum(1 for entry in entries if fnmatch.fnmatch(entry.name, "*.md"))
143
+ except FileNotFoundError:
144
+ return 0
145
+
146
+
147
+ def _count_into_binding(path: Path, searched_from: Path | None, origin: str) -> StoreBinding:
148
+ try:
149
+ count = count_facts(path)
150
+ except OSError as e:
151
+ raise MemoryValidationError(
152
+ f"memory store is unreadable: {path / 'facts'}: {e.strerror}; a store that "
153
+ f"could not be listed is not a store with nothing in it, and answering "
154
+ f"'empty' here is the conflation this binding exists to end"
155
+ ) from e
156
+ state = "populated" if count else "empty"
157
+ return StoreBinding(
158
+ path=path, state=state, fact_count=count, searched_from=searched_from, origin=origin
159
+ )
160
+
161
+
162
+ def _pinned_store() -> Path | None:
163
+ """The store named by `MEMORY_DIR_ENV`, or None if the operator named none.
164
+
165
+ Three rulings live here, and each one is a choice about who gets blamed:
166
+
167
+ - A blank value is NOT a pin. A host that emits `"BANTAMKIT_MEMORY_DIR": ""`
168
+ has named no store, and failing there would break the walk for an operator
169
+ who pinned nothing. Same stance as `assets_root()` on `BANTAMKIT_ASSETS`.
170
+ - A relative pin RAISES. A path resolved against cwd is a pin whose meaning
171
+ depends on the exact thing the pin exists to escape. `~` is expanded first,
172
+ because an MCP host passes `env` verbatim with no shell to expand it, so the
173
+ tilde arrives literally and would otherwise fail as a nonexistent directory.
174
+ - A pin that is not there RAISES, and is never created. Creating a store
175
+ because someone typo'd a path is how an empty store came to answer for a
176
+ populated one in the first place; the raise is `load_grants`' stance below,
177
+ where a grant you wrote that is wrong is surfaced rather than dropped.
178
+
179
+ `os.stat` rather than `Path.is_dir()` on purpose. `is_dir()` swallows
180
+ PermissionError and answers False, which would report an operator's real store
181
+ as a typo — the walk lives with that (it must try many candidates and cannot
182
+ raise on each unreadable one), but a pin is a single path the operator named
183
+ out loud, so it gets the accurate reason. The path is not symlink-resolved,
184
+ matching the walk: a symlinked store keeps its config beside the symlink.
185
+ """
186
+ raw = os.environ.get(MEMORY_DIR_ENV)
187
+ if raw is None or not raw.strip():
188
+ return None
189
+ pin = Path(raw).expanduser()
190
+ if not pin.is_absolute():
191
+ raise MemoryValidationError(
192
+ f"pinned memory store must be an absolute path, got {raw!r} "
193
+ f"({MEMORY_DIR_ENV}={raw}); a relative pin is resolved against a cwd "
194
+ f"the MCP host chose, which is what the pin exists to override"
195
+ )
196
+ try:
197
+ info = os.stat(pin)
198
+ except OSError as e:
199
+ raise MemoryValidationError(
200
+ f"pinned memory store is unreachable: {pin}: {e.strerror} "
201
+ f"({MEMORY_DIR_ENV}={raw}); nothing was created"
202
+ ) from e
203
+ if not stat_module.S_ISDIR(info.st_mode):
204
+ raise MemoryValidationError(
205
+ f"pinned memory store is not a directory: {pin} ({MEMORY_DIR_ENV}={raw})"
206
+ )
207
+ return pin
208
+
209
+
210
+ def _resolved_base(start: str | Path | None) -> Path:
211
+ return (Path(start) if start is not None else Path.cwd()).resolve()
212
+
213
+
214
+ def _walk_to_store(base: Path) -> Path:
215
+ for d in (base, *base.parents):
216
+ candidate = d / PROJECT_STORE
217
+ if candidate.is_dir():
218
+ return candidate
219
+ return base / PROJECT_STORE
220
+
221
+
222
+ def load_grants(project_store: str | Path) -> list[Path]:
223
+ """Read extra read-only store paths from the config beside the project store.
224
+
225
+ Missing config -> no grants. A config that exists but is wrong — unparsable,
226
+ not a mapping, non-list/non-str `extra_stores`, or a listed path that is not
227
+ an existing directory — raises MemoryValidationError: a grant you wrote that
228
+ is wrong is a mistake to surface at construction, not silently drop.
229
+ Returned grant paths are fully resolved.
230
+ """
231
+ config_path = Path(project_store).parent / CONFIG_NAME
232
+ if not config_path.exists():
233
+ return []
234
+ if not config_path.is_file():
235
+ raise MemoryValidationError(f"invalid memory config {config_path}: not a file")
236
+ try:
237
+ data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
238
+ except (yaml.YAMLError, OSError, UnicodeDecodeError) as e:
239
+ raise MemoryValidationError(f"invalid memory config {config_path}: {e}") from e
240
+ if data is None:
241
+ return []
242
+ if not isinstance(data, dict):
243
+ raise MemoryValidationError(f"invalid memory config {config_path}: expected a mapping")
244
+ raw = data.get("extra_stores", [])
245
+ if not isinstance(raw, list) or not all(isinstance(p, str) for p in raw):
246
+ raise MemoryValidationError(
247
+ f"invalid memory config {config_path}: extra_stores must be a list of paths"
248
+ )
249
+ grants: list[Path] = []
250
+ for entry in raw:
251
+ resolved = (config_path.parent / entry).resolve()
252
+ if not resolved.is_dir():
253
+ raise MemoryValidationError(
254
+ f"granted store does not exist: {resolved} (from {config_path})"
255
+ )
256
+ grants.append(resolved)
257
+ return grants