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.
- bantamkit/__init__.py +32 -0
- bantamkit/agent.py +458 -0
- bantamkit/assets/contracts/default.yaml +90 -0
- bantamkit/assets/evals/devteam/manifest.yaml +351 -0
- bantamkit/assets/evals/devteam/repo/HISTORY.md +18 -0
- bantamkit/assets/evals/devteam/repo/README.md +12 -0
- bantamkit/assets/evals/devteam/repo/docs/architecture.md +17 -0
- bantamkit/assets/evals/devteam/repo/docs/runbook.md +10 -0
- bantamkit/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
- bantamkit/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/config.py +35 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/report.py +9 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
- bantamkit/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
- bantamkit/assets/evals/devteam/repo/tests/test_posting.py +13 -0
- bantamkit/assets/evals/devteam/repo/tests/test_settle.py +9 -0
- bantamkit/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
- bantamkit/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
- bantamkit/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
- bantamkit/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
- bantamkit/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
- bantamkit/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
- bantamkit/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
- bantamkit/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
- bantamkit/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-137.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-261.yaml +37 -0
- bantamkit/assets/evals/document/tasks/doc-small-388.yaml +37 -0
- bantamkit/assets/evals/fixtures/.gitkeep +0 -0
- bantamkit/assets/evals/fixtures/catalog.json +6 -0
- bantamkit/assets/evals/perturbations/task-completion.yaml +576 -0
- bantamkit/assets/evals/tasks/.gitkeep +0 -0
- bantamkit/assets/evals/tasks/extract-contact.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-invoice.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-order.yaml +15 -0
- bantamkit/assets/evals/tasks/extract-schedule.yaml +14 -0
- bantamkit/assets/evals/tasks/extract-versions.yaml +17 -0
- bantamkit/assets/evals/tasks/nav-prod-port.yaml +84 -0
- bantamkit/assets/evals/tasks/nav-release-bundle.yaml +87 -0
- bantamkit/assets/evals/tasks/recall-audit-retention.yaml +17 -0
- bantamkit/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-db-port.yaml +17 -0
- bantamkit/assets/evals/tasks/recall-deploy.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
- bantamkit/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
- bantamkit/assets/evals/tasks/recall-oncall.yaml +13 -0
- bantamkit/assets/evals/tasks/recall-org-quota.yaml +18 -0
- bantamkit/assets/evals/tasks/recall-owner.yaml +13 -0
- bantamkit/assets/evals/tasks/shop-basket-total.yaml +10 -0
- bantamkit/assets/evals/tasks/shop-cheapest.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-compare.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-gadget-value.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-stock-total.yaml +9 -0
- bantamkit/assets/evals/tasks/shop-total.yaml +9 -0
- bantamkit/assets/profiles/default.yaml +31 -0
- bantamkit/assets/profiles/patient.yaml +31 -0
- bantamkit/assets/rubrics/.gitkeep +0 -0
- bantamkit/assets/rubrics/code-quality.yaml +20 -0
- bantamkit/assets/rubrics/grounded-completion.yaml +37 -0
- bantamkit/assets/rubrics/task-completion.yaml +28 -0
- bantamkit/assets/schemas/shiftwork-checkpoint.json +188 -0
- bantamkit/assets/skills/.gitkeep +0 -0
- bantamkit/assets/skills/file-graph.md +7 -0
- bantamkit/assets/skills/memory.md +35 -0
- bantamkit/assets/tools/.gitkeep +0 -0
- bantamkit/assets/tools/bantamkit_read.json +48 -0
- bantamkit/assets/tools/bantamkit_status.json +25 -0
- bantamkit/assets/tools/build_identity.json +17 -0
- bantamkit/assets/tools/document_list.json +12 -0
- bantamkit/assets/tools/document_read.json +31 -0
- bantamkit/assets/tools/file_graph.json +12 -0
- bantamkit/assets/tools/memory_compact.json +31 -0
- bantamkit/assets/tools/memory_recall.json +38 -0
- bantamkit/assets/tools/memory_save.json +61 -0
- bantamkit/assets/tools/shiftwork_clock_in.json +25 -0
- bantamkit/assets/tools/shiftwork_clock_out.json +60 -0
- bantamkit/assets/tools/shiftwork_status.json +25 -0
- bantamkit/assets/tools/skill_audit.json +70 -0
- bantamkit/assets/tools/validate_json.json +31 -0
- bantamkit/assets.py +67 -0
- bantamkit/budget.py +114 -0
- bantamkit/client.py +329 -0
- bantamkit/contract.py +522 -0
- bantamkit/criticreplay.py +3241 -0
- bantamkit/critique.py +301 -0
- bantamkit/docread.py +1744 -0
- bantamkit/evalrun.py +2003 -0
- bantamkit/eventlog.py +282 -0
- bantamkit/filegraph.py +218 -0
- bantamkit/loopguard.py +101 -0
- bantamkit/mcpreport.py +763 -0
- bantamkit/mcpserver.py +1334 -0
- bantamkit/memory/__init__.py +28 -0
- bantamkit/memory/__main__.py +291 -0
- bantamkit/memory/component.py +569 -0
- bantamkit/memory/divergence.py +744 -0
- bantamkit/memory/layers.py +257 -0
- bantamkit/memory/store.py +940 -0
- bantamkit/pdfread.py +1402 -0
- bantamkit/profile.py +46 -0
- bantamkit/shiftwork.py +212 -0
- bantamkit/skillaudit.py +853 -0
- bantamkit/statusline.py +313 -0
- bantamkit/structured.py +125 -0
- bantamkit/textutil.py +30 -0
- bantamkit-0.27.0.dist-info/METADATA +207 -0
- bantamkit-0.27.0.dist-info/RECORD +119 -0
- bantamkit-0.27.0.dist-info/WHEEL +4 -0
- bantamkit-0.27.0.dist-info/entry_points.txt +2 -0
bantamkit/statusline.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""One line for a host's status bar, rendered from what a finished session left on disk.
|
|
2
|
+
|
|
3
|
+
WHAT THIS IS AND WHY IT IS NOT A SIXTH HOST. `bantamkit_status` and its prompt
|
|
4
|
+
(`docs/status.md`) are surfaces a host renders ON DEMAND: the model calls a tool, or a
|
|
5
|
+
person invokes a slash command. This one is rendered WITHOUT ANYONE ASKING, on the
|
|
6
|
+
host's own redraw path. That is the whole point of it, and it exists in **Claude Code
|
|
7
|
+
only** -- `statusLine` is a key in `~/.claude/settings.json` and has no analogue in
|
|
8
|
+
Claude Desktop, in Copilot CLI, or in either VS Code extension. It does not generalise
|
|
9
|
+
and nothing here should be written as though it will.
|
|
10
|
+
|
|
11
|
+
THREE PROPERTIES, EACH THE OPPOSITE OF A DEFECT ALREADY MEASURED IN THIS PRODUCT.
|
|
12
|
+
|
|
13
|
+
* **It never starts a server.** Nothing in this module opens a transport, builds a
|
|
14
|
+
`Memory`, or imports `mcpserver`. It reads at most one file. A status bar that spawned
|
|
15
|
+
an MCP server per redraw would cost more than everything it reports on.
|
|
16
|
+
* **It never fails loudly.** No traceback, no stderr, no empty line. `status_line` is
|
|
17
|
+
total: every path returns a string, and the outermost `except Exception` turns a
|
|
18
|
+
surprise into the UNKNOWN state. Job39 measured the alternative twice -- the packaged
|
|
19
|
+
`bin` dies with a raw Node stack trace, and the default memory store resolves against
|
|
20
|
+
`cwd` so a GUI host launched at `/` kills the server -- and both of those are what a
|
|
21
|
+
redraw path must not reproduce. **Unknown is a state to render, not an error to raise.**
|
|
22
|
+
* **It reads what is already true.** No clock, no probe, no heartbeat, no timer.
|
|
23
|
+
|
|
24
|
+
WHY THE EVENT LOG AND NOT `--mcp-report`. Two sources were defensible and a third would
|
|
25
|
+
have been an invention. `--mcp-report` (`docs/mcpreport.md`) reads the event log too,
|
|
26
|
+
but it reaches it through the HOST's log first. Measured on this machine 2026-08-24, 25
|
|
27
|
+
runs each: bare `node -e 0` p50 **18.1 ms**, `--statusline` p50 **69.4 ms**,
|
|
28
|
+
`--mcp-report` p50 **88.6 ms** over 22 slug directories, 112 files and 1674 records.
|
|
29
|
+
|
|
30
|
+
The 19 ms is the smaller half of the reason. The larger half is that **source A is
|
|
31
|
+
unbounded and bantamkit does not own it**: the host writes it, never rotates it, and it
|
|
32
|
+
was 1533 records when `docs/eventlog.md` was written and 1674 five days later. A redraw
|
|
33
|
+
path may not have its cost set by a file another program grows. Source B is bounded by
|
|
34
|
+
its own rotation rule at 1 MiB, and this reader bounds it again at `TAIL_BYTES`.
|
|
35
|
+
|
|
36
|
+
The third reason is shape. `--mcp-report` prints forty lines; a status bar takes one, so
|
|
37
|
+
an adapter over it would have to PARSE the report -- a third representation of a state
|
|
38
|
+
that already has two. `bantamkit-mcp --statusline` renders the one line directly.
|
|
39
|
+
|
|
40
|
+
WHAT THE LINE CAN AND CANNOT SAY. The four conditions in `docs/status.md` are computed
|
|
41
|
+
by a RUNNING server, and one of them (`event-log-unwritable`) is process state that no
|
|
42
|
+
outside reader can see at all. This module does not recompute them and does not pretend
|
|
43
|
+
to. What it has is the event log's own closed `outcome` vocabulary, and three of those
|
|
44
|
+
values name a failure of the server's own state rather than a property of the caller's
|
|
45
|
+
input -- see `ADVERSE`. A window that contains one of them is DEGRADED.
|
|
46
|
+
|
|
47
|
+
METADATA ONLY, THE SAME RULE THE LOG ITSELF IS UNDER, AND HERE IT IS STRUCTURAL. The
|
|
48
|
+
parser below keeps exactly two strings per record: a `tool` that matched
|
|
49
|
+
`^[a-z0-9_]{1,64}$` and an `outcome` that is a member of `OUTCOMES`. `detail` is never
|
|
50
|
+
read, `ts` is never read, `v` is never read. There is no field on the parsed record that
|
|
51
|
+
could hold borrowed text, so no code path can print one -- the same guard
|
|
52
|
+
`mcpreport.py` uses against the host's log, applied here to bantamkit's own, because the
|
|
53
|
+
log is a file on disk that anything can append to.
|
|
54
|
+
|
|
55
|
+
THE CROSS-RUNTIME CONTRACT IS `docs/statusline.md`. `runtime-ts/src/statusline.ts`
|
|
56
|
+
emits the same bytes and `tools/conformance/suites/statusline.mjs` compares the two as
|
|
57
|
+
processes. Change the rendering there and in both runtimes, or not at all.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import json
|
|
63
|
+
import re
|
|
64
|
+
from dataclasses import dataclass
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
|
|
67
|
+
from bantamkit.mcpreport import resolve_event_log_path
|
|
68
|
+
|
|
69
|
+
#: How many of the log's most recent records the line describes.
|
|
70
|
+
#:
|
|
71
|
+
#: The bar answers "what is happening now", not "what has ever happened" -- that second
|
|
72
|
+
#: question is `--mcp-report`'s and it reads both generations to answer it. A fixed
|
|
73
|
+
#: window also makes the two counts on the line bounded, so the rendered string cannot
|
|
74
|
+
#: grow with the size of somebody's log.
|
|
75
|
+
WINDOW = 50
|
|
76
|
+
|
|
77
|
+
#: The most bytes read off the end of the log, ever.
|
|
78
|
+
#:
|
|
79
|
+
#: The log rotates itself at 1 MiB (`eventlog.CAP_BYTES`), so under bantamkit's own
|
|
80
|
+
#: default this branch never fires. It fires when `BANTAMKIT_EVENT_LOG` names a LITERAL
|
|
81
|
+
#: path -- an operator can point that at anything, and a redraw path may not be at the
|
|
82
|
+
#: mercy of how big that anything is. A partial first line is discarded, so the tail is
|
|
83
|
+
#: parsed identically whether or not it was truncated.
|
|
84
|
+
#:
|
|
85
|
+
#: The rotated generation `<path>.1` is NOT read at all. It is history, and history is
|
|
86
|
+
#: the analyst's job; a bar that reached back into it would report a fault the current
|
|
87
|
+
#: session has already replaced.
|
|
88
|
+
TAIL_BYTES = 262144
|
|
89
|
+
|
|
90
|
+
#: Every `outcome` `docs/eventlog.md` defines, and nothing else is renderable.
|
|
91
|
+
#:
|
|
92
|
+
#: A closed set, not a shape check: this is the guard that keeps an appended line from
|
|
93
|
+
#: putting text of its own choosing on the operator's status bar.
|
|
94
|
+
OUTCOMES = frozenset(
|
|
95
|
+
{
|
|
96
|
+
# memory_save
|
|
97
|
+
"saved",
|
|
98
|
+
"duplicate",
|
|
99
|
+
"refused-validation",
|
|
100
|
+
"refused-budget",
|
|
101
|
+
# memory_recall
|
|
102
|
+
"answered",
|
|
103
|
+
"empty-no-match",
|
|
104
|
+
"empty-unreadable-layer",
|
|
105
|
+
"empty-nothing-saved",
|
|
106
|
+
# validate_json
|
|
107
|
+
"valid",
|
|
108
|
+
"invalid",
|
|
109
|
+
# shiftwork_clock_in
|
|
110
|
+
"brief",
|
|
111
|
+
"escalate",
|
|
112
|
+
"success",
|
|
113
|
+
"error",
|
|
114
|
+
# shiftwork_clock_out / shiftwork_status
|
|
115
|
+
"ok",
|
|
116
|
+
"status",
|
|
117
|
+
# build_identity
|
|
118
|
+
"complete",
|
|
119
|
+
"partial",
|
|
120
|
+
# any handler that fell over
|
|
121
|
+
"raised",
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
#: The three outcomes that mean BANTAMKIT is in trouble, rather than the caller's input.
|
|
126
|
+
#:
|
|
127
|
+
#: * `raised` -- a handler fell over. There is no reading of that which is fine.
|
|
128
|
+
#: * `refused-budget` -- the store's index budget stopped a save. This is the same
|
|
129
|
+
#: pressure `docs/status.md`'s `index-budget-low` warns about one step earlier; by the
|
|
130
|
+
#: time it is in the log the refusal has already happened.
|
|
131
|
+
#: * `empty-unreadable-layer` -- a bound memory layer could not be listed. That is
|
|
132
|
+
#: `docs/status.md`'s `memory-layer-unreadable`, observed from the outside.
|
|
133
|
+
#:
|
|
134
|
+
#: Deliberately NOT here: `refused-validation` (the caller handed over a bad memory),
|
|
135
|
+
#: `error` and `escalate` (the shiftwork register composing a refusal about a checkpoint,
|
|
136
|
+
#: which is the tool working), `invalid` (a schema said no, which is the answer), and
|
|
137
|
+
#: every `empty-*` that is simply nothing to recall. A bar that turned orange because a
|
|
138
|
+
#: user typo'd a fact name is a bar the user learns to ignore.
|
|
139
|
+
ADVERSE = ("raised", "refused-budget", "empty-unreadable-layer")
|
|
140
|
+
|
|
141
|
+
#: The host's log validates tool names with exactly this and so does `mcpreport.py`.
|
|
142
|
+
_TOOL = re.compile(r"^[a-z0-9_]{1,64}$")
|
|
143
|
+
|
|
144
|
+
#: U+1F7E2 / U+1F7E0 are the same two `docs/status.md` pins; U+26AA is this file's.
|
|
145
|
+
ACTIVE = "bantamkit Active 🟢"
|
|
146
|
+
DEGRADED = "bantamkit Degraded 🟠"
|
|
147
|
+
UNKNOWN = "bantamkit Unknown ⚪"
|
|
148
|
+
|
|
149
|
+
#: The separator. One space, U+00B7, one space.
|
|
150
|
+
SEP = " · "
|
|
151
|
+
|
|
152
|
+
#: Every reason the UNKNOWN state can carry. A closed set, like `OUTCOMES`.
|
|
153
|
+
OFF = "event log off"
|
|
154
|
+
ABSENT = "no event log yet"
|
|
155
|
+
UNREADABLE = "event log unreadable"
|
|
156
|
+
EMPTY = "event log empty"
|
|
157
|
+
SURPRISE = "state unavailable"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True)
|
|
161
|
+
class Reading:
|
|
162
|
+
"""What one look at the log found. Integers and closed-vocabulary tokens only."""
|
|
163
|
+
|
|
164
|
+
state: str
|
|
165
|
+
"""`active`, `degraded` or `unknown`."""
|
|
166
|
+
|
|
167
|
+
events: int = 0
|
|
168
|
+
"""Records in the window. `0` in every unknown state."""
|
|
169
|
+
|
|
170
|
+
problems: int = 0
|
|
171
|
+
"""Records in the window whose outcome is in `ADVERSE`."""
|
|
172
|
+
|
|
173
|
+
tool: str = ""
|
|
174
|
+
"""The tool of the record the line names, validated against `_TOOL`."""
|
|
175
|
+
|
|
176
|
+
outcome: str = ""
|
|
177
|
+
"""That record's outcome, a member of `OUTCOMES`."""
|
|
178
|
+
|
|
179
|
+
reason: str = ""
|
|
180
|
+
"""Why the state is unknown. Empty otherwise."""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _unknown(reason: str) -> Reading:
|
|
184
|
+
return Reading(state="unknown", reason=reason)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def read_tail(path: Path) -> bytes:
|
|
188
|
+
"""The last `TAIL_BYTES` of `path`, with a partial leading line discarded.
|
|
189
|
+
|
|
190
|
+
Opened in BINARY mode and never decoded as a whole: the log is UTF-8 by contract but
|
|
191
|
+
a truncated tail can start mid-codepoint, and a `UnicodeDecodeError` on the whole
|
|
192
|
+
buffer would throw away every intact record behind it. Each line is decoded on its
|
|
193
|
+
own inside `parse`, so one bad line costs one record.
|
|
194
|
+
|
|
195
|
+
Raises `OSError` -- the caller turns that into a rendered state.
|
|
196
|
+
"""
|
|
197
|
+
with open(path, "rb") as handle:
|
|
198
|
+
handle.seek(0, 2)
|
|
199
|
+
size = handle.tell()
|
|
200
|
+
start = size - TAIL_BYTES if size > TAIL_BYTES else 0
|
|
201
|
+
handle.seek(start)
|
|
202
|
+
data = handle.read()
|
|
203
|
+
if start:
|
|
204
|
+
cut = data.find(b"\n")
|
|
205
|
+
data = b"" if cut < 0 else data[cut + 1 :]
|
|
206
|
+
return data
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def parse(data: bytes) -> list[tuple[str, str]]:
|
|
210
|
+
"""`(tool, outcome)` for every line that is a record this module is willing to render.
|
|
211
|
+
|
|
212
|
+
THIS IS THE LEAK GUARD AND IT IS STRUCTURAL, NOT A FILTER. What comes back is a list
|
|
213
|
+
of pairs of validated tokens; there is no field here that can hold a path, a memory
|
|
214
|
+
body, a query, or a `detail` value, so no later code CAN print one. A record with an
|
|
215
|
+
unknown outcome, a tool that is not `^[a-z0-9_]{1,64}$`, a non-object body, or
|
|
216
|
+
unparseable JSON is dropped whole -- it is not rendered under some fallback spelling,
|
|
217
|
+
because a fallback spelling is a way for the file's own bytes to reach the bar.
|
|
218
|
+
"""
|
|
219
|
+
pairs: list[tuple[str, str]] = []
|
|
220
|
+
for raw in data.split(b"\n"):
|
|
221
|
+
if not raw:
|
|
222
|
+
continue
|
|
223
|
+
try:
|
|
224
|
+
record = json.loads(raw.decode("utf-8"))
|
|
225
|
+
except (ValueError, UnicodeDecodeError):
|
|
226
|
+
continue
|
|
227
|
+
if not isinstance(record, dict):
|
|
228
|
+
continue
|
|
229
|
+
tool = record.get("tool")
|
|
230
|
+
outcome = record.get("outcome")
|
|
231
|
+
if not isinstance(tool, str) or not isinstance(outcome, str):
|
|
232
|
+
continue
|
|
233
|
+
if _TOOL.match(tool) is None or outcome not in OUTCOMES:
|
|
234
|
+
continue
|
|
235
|
+
pairs.append((tool, outcome))
|
|
236
|
+
return pairs
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def summarise(pairs: list[tuple[str, str]]) -> Reading:
|
|
240
|
+
"""The window's state: degraded if it holds an `ADVERSE` outcome, else active.
|
|
241
|
+
|
|
242
|
+
The record the line NAMES is the most recent adverse one when degraded, and simply
|
|
243
|
+
the most recent one when active. Naming the last record while degraded would hide the
|
|
244
|
+
fault behind whatever happened to run after it.
|
|
245
|
+
"""
|
|
246
|
+
window = pairs[-WINDOW:]
|
|
247
|
+
if not window:
|
|
248
|
+
return _unknown(EMPTY)
|
|
249
|
+
problems = [pair for pair in window if pair[1] in ADVERSE]
|
|
250
|
+
tool, outcome = problems[-1] if problems else window[-1]
|
|
251
|
+
return Reading(
|
|
252
|
+
state="degraded" if problems else "active",
|
|
253
|
+
events=len(window),
|
|
254
|
+
problems=len(problems),
|
|
255
|
+
tool=tool,
|
|
256
|
+
outcome=outcome,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def probe(
|
|
261
|
+
env: dict[str, str], store: str | None = None, start: str | None = None
|
|
262
|
+
) -> Reading:
|
|
263
|
+
"""Resolve the log the same way the server would, read it, and summarise it.
|
|
264
|
+
|
|
265
|
+
`resolve_event_log_path` is reused rather than reimplemented so there is ONE
|
|
266
|
+
vocabulary for `BANTAMKIT_EVENT_LOG` in this runtime and not a second copy that can
|
|
267
|
+
drift. It DESIGNATES a store path without creating one, so drawing a status bar never
|
|
268
|
+
brings a memory store into existence.
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
path = resolve_event_log_path(env, store=store, start=start)
|
|
272
|
+
except Exception: # noqa: BLE001 - a redraw path renders, it does not raise
|
|
273
|
+
return _unknown(UNREADABLE)
|
|
274
|
+
if path is None:
|
|
275
|
+
return _unknown(OFF)
|
|
276
|
+
try:
|
|
277
|
+
data = read_tail(path)
|
|
278
|
+
except FileNotFoundError:
|
|
279
|
+
return _unknown(ABSENT)
|
|
280
|
+
except OSError:
|
|
281
|
+
return _unknown(UNREADABLE)
|
|
282
|
+
return summarise(parse(data))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def render(reading: Reading) -> str:
|
|
286
|
+
"""The one line, without its newline. Never empty, never multi-line.
|
|
287
|
+
|
|
288
|
+
An empty line would make the host's bar flicker between one row and none, which is
|
|
289
|
+
worse than any wording; that is why the unknown state has a rendering at all.
|
|
290
|
+
"""
|
|
291
|
+
if reading.state == "unknown":
|
|
292
|
+
return f"{UNKNOWN}{SEP}{reading.reason}"
|
|
293
|
+
events = f"{reading.events} event" + ("" if reading.events == 1 else "s")
|
|
294
|
+
named = f"{reading.tool} {reading.outcome}"
|
|
295
|
+
if reading.state == "degraded":
|
|
296
|
+
problems = f"{reading.problems} problem" + ("" if reading.problems == 1 else "s")
|
|
297
|
+
return f"{DEGRADED}{SEP}{events}{SEP}{problems}{SEP}{named}"
|
|
298
|
+
return f"{ACTIVE}{SEP}{events}{SEP}{named}"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def status_line(
|
|
302
|
+
env: dict[str, str], store: str | None = None, start: str | None = None
|
|
303
|
+
) -> str:
|
|
304
|
+
"""The total entry point: a string for every input, including inputs that are wrong.
|
|
305
|
+
|
|
306
|
+
The `except Exception` is not laziness, it is the contract. This runs on somebody's
|
|
307
|
+
redraw path with no operator watching; the failure this must not have is a traceback
|
|
308
|
+
where a status bar should be.
|
|
309
|
+
"""
|
|
310
|
+
try:
|
|
311
|
+
return render(probe(env, store=store, start=start))
|
|
312
|
+
except Exception: # noqa: BLE001 - see the docstring
|
|
313
|
+
return f"{UNKNOWN}{SEP}{SURPRISE}"
|
bantamkit/structured.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""JSON-Schema-enforced output: validate, retry with a pointed error, bounded budget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import jsonschema
|
|
6
|
+
|
|
7
|
+
from bantamkit.agent import Agent, _supports_response_format, response_format_for
|
|
8
|
+
from bantamkit.client import BantamError, Message, ModelClient
|
|
9
|
+
from bantamkit.contract import (
|
|
10
|
+
extract_json,
|
|
11
|
+
json_answer_retry,
|
|
12
|
+
parse_error_message,
|
|
13
|
+
schema_instruction,
|
|
14
|
+
schema_retry_feedback,
|
|
15
|
+
validation_error_message,
|
|
16
|
+
)
|
|
17
|
+
from bantamkit.profile import default as profile_default
|
|
18
|
+
|
|
19
|
+
__all__ = ["JsonAnswerGate", "StructuredOutputError", "extract_json", "structured"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StructuredOutputError(BantamError):
|
|
23
|
+
"""No schema-valid output within the retry budget.
|
|
24
|
+
|
|
25
|
+
Carries the transcript up to the raise, same payload and same reason as
|
|
26
|
+
`MaxTurnsExceeded`: when a schema gate gives up it raises from inside the
|
|
27
|
+
agent's post-hook chain, and without a slot for the transcript every
|
|
28
|
+
`schema-exhausted` run wrote `{"output": null, "messages": []}` — the runs
|
|
29
|
+
most worth diagnosing recording nothing. The agent fills the slot in
|
|
30
|
+
(`Agent.run`); a raiser that already has the transcript may pass it here.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, message: str, messages: list[Message] | None = None):
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.messages: list[Message] = list(messages or [])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def structured(
|
|
39
|
+
client: ModelClient, prompt: str, schema: dict, *, max_retries: int | None = None
|
|
40
|
+
) -> dict:
|
|
41
|
+
"""Tier 1 constrains the decode, tiers 2-3 (instruction + validate/retry) still gate.
|
|
42
|
+
|
|
43
|
+
A constrained server makes the retries rare; a server that ignores or rejects
|
|
44
|
+
`response_format` changes nothing, because enforcement is only ever as good as
|
|
45
|
+
the server and validation is what actually decides.
|
|
46
|
+
"""
|
|
47
|
+
if max_retries is None:
|
|
48
|
+
max_retries = profile_default("structured", "max_retries")
|
|
49
|
+
response_format = response_format_for(schema)
|
|
50
|
+
messages = [
|
|
51
|
+
Message(role="system", content=schema_instruction(schema)),
|
|
52
|
+
Message(role="user", content=prompt),
|
|
53
|
+
]
|
|
54
|
+
error = "no attempts made"
|
|
55
|
+
for _ in range(max_retries):
|
|
56
|
+
# Re-checked per attempt: a 400 on attempt 1 silently drops the tier from here on.
|
|
57
|
+
if _supports_response_format(client):
|
|
58
|
+
resp = client.chat(messages, response_format=response_format)
|
|
59
|
+
else:
|
|
60
|
+
resp = client.chat(messages)
|
|
61
|
+
content = resp.message.content or ""
|
|
62
|
+
try:
|
|
63
|
+
data = extract_json(content)
|
|
64
|
+
jsonschema.validate(data, schema)
|
|
65
|
+
return data
|
|
66
|
+
except ValueError as e:
|
|
67
|
+
error = parse_error_message(e)
|
|
68
|
+
except jsonschema.ValidationError as e:
|
|
69
|
+
where = "/".join(str(p) for p in e.absolute_path) or "root"
|
|
70
|
+
error = validation_error_message(where, e.message)
|
|
71
|
+
messages.append(resp.message)
|
|
72
|
+
messages.append(Message(role="user", content=schema_retry_feedback(error)))
|
|
73
|
+
raise StructuredOutputError(
|
|
74
|
+
f"no valid output after {max_retries} attempts; last error: {error}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class JsonAnswerGate:
|
|
79
|
+
"""Rescue the failure the 7b transcripts actually showed: right content, no JSON.
|
|
80
|
+
|
|
81
|
+
Fires only when the final answer has no extractable JSON at all. A parseable
|
|
82
|
+
answer that violates a schema is `SchemaGate`'s business; a parseable answer
|
|
83
|
+
that is simply wrong is the scorer's. This gate exists for the measured
|
|
84
|
+
mechanism — the model detours, answers in fluent prose, and the "ONLY this
|
|
85
|
+
JSON" instruction is a turn too far away — and it asks for exactly one
|
|
86
|
+
restatement.
|
|
87
|
+
|
|
88
|
+
Fail-open by construction: once the budget is spent the answer passes through
|
|
89
|
+
untouched. A gate that raised here would convert a scorable wrong answer into
|
|
90
|
+
an exception, and scoring must stay the judge.
|
|
91
|
+
|
|
92
|
+
`max_attempts` counts retries the gate may spend (default 1 = one restatement),
|
|
93
|
+
unlike `SchemaGate`, whose budget counts answers including the last rejected one.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, max_attempts: int | None = None):
|
|
97
|
+
self.max_attempts = (
|
|
98
|
+
max_attempts
|
|
99
|
+
if max_attempts is not None
|
|
100
|
+
else profile_default("json_answer", "max_attempts")
|
|
101
|
+
)
|
|
102
|
+
self.retries_used = 0
|
|
103
|
+
self._attempts = 0
|
|
104
|
+
|
|
105
|
+
def setup(self, agent: Agent) -> None:
|
|
106
|
+
self.retries_used = 0
|
|
107
|
+
self._attempts = 0
|
|
108
|
+
agent.add_post_hook(self)
|
|
109
|
+
|
|
110
|
+
def __call__(self, task: str, output: str) -> str | None:
|
|
111
|
+
# The budget is latched for the whole run (reset only in setup): when a
|
|
112
|
+
# later hook (e.g. a critique gate) keeps the run alive past this gate's
|
|
113
|
+
# fail-open, the budget must not re-arm and interleave retries with the
|
|
114
|
+
# critic's feedback.
|
|
115
|
+
try:
|
|
116
|
+
extract_json(output)
|
|
117
|
+
except ValueError:
|
|
118
|
+
pass
|
|
119
|
+
else:
|
|
120
|
+
return None
|
|
121
|
+
self._attempts += 1
|
|
122
|
+
if self._attempts > self.max_attempts:
|
|
123
|
+
return None
|
|
124
|
+
self.retries_used += 1
|
|
125
|
+
return json_answer_retry()
|
bantamkit/textutil.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Byte-budget truncation shared by the core loop and contract rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def truncate_counted(text: str, budget: int) -> tuple[str, int]:
|
|
7
|
+
"""`truncate`, plus the number of bytes it dropped. Zero when nothing was cut.
|
|
8
|
+
|
|
9
|
+
C-6, 2026-08-19. The cut was already announced, but IN BAND — inside the string
|
|
10
|
+
handed to the model, as `[truncated N bytes]`. The number was known here and thrown
|
|
11
|
+
away, so nothing outside the model's own context could tell a run whose observations
|
|
12
|
+
were cut from one whose were not. RB-P51's shape: a run that silently proceeded is
|
|
13
|
+
not the same run as one that reported what it dropped.
|
|
14
|
+
|
|
15
|
+
The second return value is what makes it a column. It is deliberately NOT parsed back
|
|
16
|
+
out of the marker by a caller: that would be a second transcription of one rule and a
|
|
17
|
+
check over the two would be a tautology (RB-P47).
|
|
18
|
+
"""
|
|
19
|
+
raw = text.encode()
|
|
20
|
+
if len(raw) <= budget:
|
|
21
|
+
return text, 0
|
|
22
|
+
dropped = len(raw) - budget
|
|
23
|
+
kept = raw[:budget].decode(errors="ignore")
|
|
24
|
+
return f"{kept}\n[truncated {dropped} bytes]", dropped
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def truncate(text: str, budget: int) -> str:
|
|
28
|
+
"""Byte-identical to what it always returned. Callers that want the count call
|
|
29
|
+
`truncate_counted`; `contract.py` and `filegraph.py` do not and are unchanged."""
|
|
30
|
+
return truncate_counted(text, budget)[0]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bantamkit
|
|
3
|
+
Version: 0.27.0
|
|
4
|
+
Summary: bantamweight tooling — harness primitives that lift small-model agents
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: jsonschema>=4.21
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: hatchling>=1.24; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
14
|
+
Provides-Extra: mcp
|
|
15
|
+
Requires-Dist: mcp<3,>=2.0; extra == 'mcp'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# bantamkit
|
|
19
|
+
|
|
20
|
+
Harness primitives that lift small-model agents: a per-person memory store, JSON
|
|
21
|
+
validation, shift-work accounting, a document reader, and a skill-catalogue
|
|
22
|
+
auditor — served to any MCP host over stdio.
|
|
23
|
+
|
|
24
|
+
This is the **Python** distribution. There is a second, independent
|
|
25
|
+
implementation of the same surface on npm as
|
|
26
|
+
[`bantamkit-mcp`](https://www.npmjs.com/package/bantamkit-mcp), written in pure
|
|
27
|
+
Node. The two share a memory store on disk and are held to the same answers by a
|
|
28
|
+
conformance suite, so you can install whichever one your host makes easy. What
|
|
29
|
+
the two do *not* agree on is written down in
|
|
30
|
+
[`docs/porting.md`](https://github.com/Ink01101011/bantamkit/blob/main/docs/porting.md)
|
|
31
|
+
and summarised at the bottom of this page.
|
|
32
|
+
|
|
33
|
+
## Install and run
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pipx run --spec "bantamkit[mcp]" bantamkit-mcp --assets-root
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
or into an environment you keep:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install "bantamkit[mcp]"
|
|
43
|
+
bantamkit-mcp --help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The `[mcp]` extra pulls the MCP SDK. Without it you still get the library and the
|
|
47
|
+
operator CLI, but not the server.
|
|
48
|
+
|
|
49
|
+
The uv equivalent is `uvx --from "bantamkit[mcp]" bantamkit-mcp`. uv is not
|
|
50
|
+
installed on the machine this README was measured on, so unlike every other
|
|
51
|
+
command here that one is the documented form rather than a measured one.
|
|
52
|
+
|
|
53
|
+
### Connect it to a host
|
|
54
|
+
|
|
55
|
+
Every host below runs the same command; only the file and the key around it change. If
|
|
56
|
+
you installed with `pip` into an environment you keep, replace the `command`/`args` pair
|
|
57
|
+
with the absolute path to the `bantamkit-mcp` console script in that environment.
|
|
58
|
+
|
|
59
|
+
**Claude Code** — one command, no file to edit. `-s user` makes it available in every
|
|
60
|
+
project; drop it for this project only.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add bantamkit -s user -- pipx run --spec "bantamkit[mcp]" bantamkit-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
67
|
+
on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Top-level key
|
|
68
|
+
`mcpServers`; each entry takes `command`, `args` and an optional `env`.
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{"mcpServers": {"bantamkit": {"command": "pipx", "args": ["run", "--spec", "bantamkit[mcp]", "bantamkit-mcp"]}}}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**GitHub Copilot in VS Code** — `.vscode/mcp.json` for one workspace, or the user
|
|
75
|
+
profile via the **MCP: Open User Configuration** command. Note the top-level key is
|
|
76
|
+
`servers`, not `mcpServers`.
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{"servers": {"bantamkit": {"type": "stdio", "command": "pipx", "args": ["run", "--spec", "bantamkit[mcp]", "bantamkit-mcp"]}}}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Cursor** — `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` globally. Back
|
|
83
|
+
to `mcpServers`.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{"mcpServers": {"bantamkit": {"command": "pipx", "args": ["run", "--spec", "bantamkit[mcp]", "bantamkit-mcp"]}}}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Anything else that speaks MCP over stdio** runs the console script and talks JSON-RPC
|
|
90
|
+
on its stdin and stdout. Nothing about this package is host-specific.
|
|
91
|
+
|
|
92
|
+
The Claude Code and Claude Desktop forms were taken from this machine — `claude mcp add
|
|
93
|
+
--help` and an existing config file. The VS Code and Cursor forms are from those
|
|
94
|
+
projects' own documentation, not from a host installed here.
|
|
95
|
+
|
|
96
|
+
Point `--start` at a directory to choose where project-store discovery begins, or
|
|
97
|
+
`--store` at a single path to disable layering entirely. Do not reach for
|
|
98
|
+
`--store` by reflex: the default is layered, and the layering is most of the
|
|
99
|
+
value.
|
|
100
|
+
|
|
101
|
+
## What it serves
|
|
102
|
+
|
|
103
|
+
Eleven tools, measured off a wheel installed into an empty virtualenv:
|
|
104
|
+
|
|
105
|
+
| tool | what it does |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `memory_save` | store one durable fact, deduped and budgeted |
|
|
108
|
+
| `memory_recall` | retrieve facts matching a query |
|
|
109
|
+
| `memory_compact` | archive the stalest facts to fit the index budget |
|
|
110
|
+
| `validate_json` | validate a document against a JSON Schema |
|
|
111
|
+
| `bantamkit_read` | read a document — text, office formats, pdf |
|
|
112
|
+
| `skill_audit` | audit a skill catalogue for findings |
|
|
113
|
+
| `shiftwork_clock_in` | open a unit of work and get its brief |
|
|
114
|
+
| `shiftwork_clock_out` | close a unit with status and accounting |
|
|
115
|
+
| `shiftwork_status` | report the open cursor |
|
|
116
|
+
| `bantamkit_status` | report store health against its budget |
|
|
117
|
+
| `build_identity` | report the fingerprint of the source on disk |
|
|
118
|
+
|
|
119
|
+
`build_identity` describes the tree on disk, not the code currently executing —
|
|
120
|
+
useful precisely when a machine carries two installs under one name.
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
Python 3.11 or newer. Three runtime dependencies — `httpx`, `jsonschema`,
|
|
125
|
+
`pyyaml` — plus `mcp` under the `[mcp]` extra.
|
|
126
|
+
|
|
127
|
+
## The asset pack
|
|
128
|
+
|
|
129
|
+
Contracts, schemas, eval tasks, rubrics and tool manifests ship inside the
|
|
130
|
+
package and are located at import time:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bantamkit-mcp --assets-root
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
It prints the resolved directory and its file count. A build that cannot find the
|
|
137
|
+
pack fails rather than producing an artifact without it — that refusal is
|
|
138
|
+
deliberate, because the silent version of it shipped once.
|
|
139
|
+
|
|
140
|
+
`BANTAMKIT_ASSETS` overrides the location.
|
|
141
|
+
|
|
142
|
+
## The operator CLI
|
|
143
|
+
|
|
144
|
+
Memory-store maintenance is a separate surface from the agent-facing tools, and
|
|
145
|
+
it is not served over MCP:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python -m bantamkit.memory status # index size, budget, headroom, archives
|
|
149
|
+
python -m bantamkit.memory lint # exit 1 if malformed or over budget
|
|
150
|
+
python -m bantamkit.memory compact # archive the stalest facts
|
|
151
|
+
python -m bantamkit.memory archive <name>
|
|
152
|
+
python -m bantamkit.memory restore <name>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`archive` moves a fact out of the store without deleting it; `restore` brings it
|
|
156
|
+
back by name.
|
|
157
|
+
|
|
158
|
+
## Sharing a store with the Node server
|
|
159
|
+
|
|
160
|
+
Both distributions read and write the same on-disk format, so one store can be
|
|
161
|
+
served by either. That is also why a surface present in one and absent from the
|
|
162
|
+
other is not merely a coverage gap — it is a way for two servers to disagree
|
|
163
|
+
about one person's data. Every feature lands in both implementations in the same
|
|
164
|
+
change, and a conformance case compares the two answers before it counts as
|
|
165
|
+
ported.
|
|
166
|
+
|
|
167
|
+
## Where the two implementations differ, on purpose
|
|
168
|
+
|
|
169
|
+
- **This side reads pdf, `.doc` and `.rtf`; the Node side refuses them by name.**
|
|
170
|
+
PDF is read by a stdlib reader written for this project; real OLE2 `.doc` and
|
|
171
|
+
`.rtf` go through `/usr/bin/textutil`, a macOS built-in that is probed at every
|
|
172
|
+
call and refused by name where it is absent.
|
|
173
|
+
- **The operator CLI is spelled differently**, and it shows in help text and
|
|
174
|
+
error messages: `python -m bantamkit.memory` here against `bantamkit-memory`
|
|
175
|
+
there. There is no third spelling — a pure-npm install has no Python in it, and
|
|
176
|
+
CPython does not install that console script.
|
|
177
|
+
- **`build_id` hashes the executing tree**, and the two runtimes are two trees, so
|
|
178
|
+
it differs by construction. `assets_digest` is identical, and that is the one
|
|
179
|
+
that carries meaning.
|
|
180
|
+
|
|
181
|
+
Each of these is recorded in the divergence table with a conformance case pinning
|
|
182
|
+
the wording, so the difference cannot drift unnoticed.
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git clone https://github.com/Ink01101011/bantamkit
|
|
188
|
+
cd bantamkit
|
|
189
|
+
python -m venv .venv && .venv/bin/pip install -e "runtime-py[dev,mcp]"
|
|
190
|
+
.venv/bin/python -m pytest runtime-py/tests -q
|
|
191
|
+
.venv/bin/ruff check runtime-py
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The cross-runtime gate needs Node:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
node tools/conformance/run.mjs --all
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Links
|
|
201
|
+
|
|
202
|
+
- Source: https://github.com/Ink01101011/bantamkit
|
|
203
|
+
- The Node distribution: https://www.npmjs.com/package/bantamkit-mcp
|
|
204
|
+
- Install notes: [`docs/install.md`](https://github.com/Ink01101011/bantamkit/blob/main/docs/install.md)
|
|
205
|
+
- What the two runtimes disagree about: [`docs/porting.md`](https://github.com/Ink01101011/bantamkit/blob/main/docs/porting.md)
|
|
206
|
+
|
|
207
|
+
MIT.
|