bantamkit-mcp 0.26.0

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ink01101011
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,398 @@
1
+ # bantamkit-mcp
2
+
3
+ The bantamkit MCP server as a pure-Node package: `npx bantamkit-mcp`, no Python, no
4
+ `uv`, no `pipx`, no interpreter bootstrap. It serves the same eleven tools, the same
5
+ `bantamkit_status` prompt and the same two resource templates as `runtime-py`'s server,
6
+ reads and writes the same memory store, and is checked against the Python server frame by
7
+ frame — 4500+ conformance cases, with every intentional difference written down as a
8
+ ruling. It installs **two** commands, not one — the server and `bantamkit-memory`, the
9
+ operator CLI for memory-store lifecycle; see *The second bin* below.
10
+
11
+ Every number on this page was measured on the machine that wrote it, with a command you
12
+ can rerun. Where something was not measured, it says so.
13
+
14
+ ## Install and run
15
+
16
+ ```jsonc
17
+ // .mcp.json — project scope
18
+ {
19
+ "mcpServers": {
20
+ "bantamkit": {
21
+ "command": "npx",
22
+ "args": ["-y", "bantamkit-mcp"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ That is the whole install. No `pip`, no venv, no `PYTHONPATH`. `runtime-ts/mcp.json.example`
29
+ in the repository is the annotated version, with the four forms and the two measured
30
+ questions that decide between them.
31
+
32
+ **Pass `-y`.** `npx` historically prompted before installing a package it had not seen,
33
+ and stdin here is the JSON-RPC channel: a prompt that consumed one frame looks like a
34
+ server that lost a request. npm 11.6.2 was measured sending that prompt to stderr rather
35
+ than reading stdin, but that is version-dependent and `-y` costs nothing.
36
+
37
+ **Pin the version.** `npx -y bantamkit-mcp` resolves `latest` and caches it, so two
38
+ machines can run different builds from one identical config line. See *Silent version
39
+ float* below — it is the failure this package makes easiest to hit and hardest to see.
40
+
41
+ ### Measured: what a cold start costs
42
+
43
+ `node tools/conformance/npx-cold-start.mjs` in the repo packs the tarball, installs it
44
+ from disk into a cache that has never seen it, and drives a real MCP handshake. On
45
+ node v25.2.1 / npm 11.6.2, macOS (darwin 25.5.0), Apple silicon:
46
+
47
+ | | cold cache | warm cache |
48
+ |---|---|---|
49
+ | wall to the first JSON-RPC frame | **3.90 s** and **5.09 s**, two runs | **1.14 s** and **1.11 s** |
50
+ | npm/npx bytes on stderr | 211 (an `npm notice` about npm itself) | 0 |
51
+
52
+ Both cold figures are reported rather than averaged, and the two in that column are two
53
+ runs from **one sitting on 2026-08-25** — the column is labelled "two runs" and one
54
+ execution cannot honestly fill it.
55
+
56
+ **The spread is bigger than a range would suggest, and it is the network.** Five cold
57
+ runs on that same day, same machine, same commit, measured **3.68 / 3.90 / 4.66 / 5.09 /
58
+ 9.66 s**. An earlier revision of this table read "4.13 s and 7.36 s" and called a cold
59
+ start "worth about 4-7 s here"; three of those five runs fall outside that range, so the
60
+ range is dropped rather than re-fitted. What a reader should take from the cold column is
61
+ an ORDER OF MAGNITUDE — seconds, dominated by the registry round trip, worse on a slower
62
+ link — and not a number to compare a future run against. The warm figure is the one that
63
+ is a property of this package, and it is stable across every run above.
64
+
65
+ - **92 packages** installed (top-level under `node_modules`, scope-aware); **111**
66
+ `package.json` **under `node_modules`**, which is the count of packages actually
67
+ installed:
68
+
69
+ ```
70
+ find "$BED/cold/cache/_npx/<hash>/node_modules" -type f -name package.json | wc -l # 111
71
+ ```
72
+
73
+ Counting from the `_npx/<hash>` directory instead gives **112**, and the extra file is
74
+ not a package: npx synthesises a `package.json` at that root holding the `file:` spec
75
+ and an `_npx.packages` array. The method is written down here because the 112 was read
76
+ once as a drift of one and it is not one — it is a different denominator. Reproduce
77
+ either with `node tools/conformance/npx-cold-start.mjs --keep`, which prints the scratch
78
+ path it leaves behind.
79
+ - **15.7 MB** of files under `$npm_config_cache/_npx/<hash>` (26 MB of allocated blocks by
80
+ `du`), **37.8 MB** for the whole cache including npm's content-addressable store (this
81
+ read 37.7 until 2026-08-25; all four cold runs that day printed 37.8).
82
+ - `bantamkit-mcp` itself is **0.9 MB** of that (1.2 MB allocated). The rest is
83
+ `@modelcontextprotocol/sdk@1.30.0`'s dependency tree, which pulls in `express`, `cors`,
84
+ `body-parser`, `ajv`, `eventsource`, `hono` and `express-rate-limit` — the SDK's HTTP
85
+ transport, none of which this stdio server uses. That is the SDK's shape, not a choice
86
+ this package makes; it declares exactly one runtime dependency.
87
+ - The tarball is **~266 KB**, 137 files: 50 in `dist/`, 84 in `assets/`, plus `LICENSE`,
88
+ `package.json` and this README. (The exact byte count moves whenever this file does;
89
+ the gate prints it, and `test/packaging.test.mjs` pins the file list.)
90
+
91
+ ### Measured: what happens when the registry is not reachable
92
+
93
+ A cold `npx` start needs the network. Measured with the registry pointed at a closed port
94
+ and at a blackholed address, driving the same handshake:
95
+
96
+ | registry | seconds before `npx` gave up | bytes the client saw on stdout |
97
+ |---|---|---|
98
+ | connection refused (`http://127.0.0.1:1/`) | **140.3 s** | **0** |
99
+ | packets dropped (`http://192.0.2.1:443/`) | **590.4 s** | **0** |
100
+
101
+ **Read that as: the failure is silence.** Nothing appears on the JSON-RPC channel for the
102
+ whole interval, and then the process exits 1. An MCP host does not see "no network"; it
103
+ sees a server that accepted the launch and never answered `initialize`, and it reports its
104
+ own handshake timeout. The npm error text goes to stderr, which most hosts do not surface.
105
+
106
+ A warm cache does not have this problem — `npx` runs the cached install without contacting
107
+ the registry — so this bites a new machine, a cleared cache, or a CI runner, which is
108
+ exactly when nobody is watching.
109
+
110
+ If offline operation matters, do not use `npx`. Run `npm i -g bantamkit-mcp` once and
111
+ point the config at the installed binary, or keep using `tools/bantamkit-mcp`.
112
+
113
+ ### Measured: `npx` is not on a login-less PATH
114
+
115
+ ```sh
116
+ $ env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin sh -c 'command -v npx'
117
+ $ echo $?
118
+ 1
119
+ ```
120
+
121
+ On the machine this was written on, `node`, `npm` and `npx` exist only on the
122
+ mise-injected PATH, and mise is activated from `~/.zshrc` — an **interactive** shell rc.
123
+ `launchctl getenv PATH` is unset, so a GUI-launched application inherits launchd's default
124
+ `/usr/bin:/bin:/usr/sbin:/sbin`, where none of the three is found.
125
+
126
+ **What a GUI-launched MCP host actually sees is `ENOENT` on `npx`.** Not a bantamkit
127
+ error, not a bad config — the host reports that it could not spawn the command, which is
128
+ the least informative message in the whole chain. This is the RB-P96 failure class
129
+ relocated: the sh launcher was guaranteed present because it was a file in the repository;
130
+ `npx` is guaranteed present only if the host's *process* environment has it.
131
+
132
+ Two ways out, both in `runtime-ts/mcp.json.example`:
133
+
134
+ ```jsonc
135
+ // absolute path to the npx you actually have
136
+ { "command": "/Users/you/.local/share/mise/installs/node/latest/bin/npx",
137
+ "args": ["-y", "bantamkit-mcp@0.25.0"] }
138
+ ```
139
+
140
+ ```jsonc
141
+ // or install once and skip npx entirely: npm i -g bantamkit-mcp
142
+ { "command": "/usr/local/bin/bantamkit-mcp", "args": [] }
143
+ ```
144
+
145
+ ## What `npx` gives up versus `tools/bantamkit-mcp`
146
+
147
+ The sh launcher is not obsolete. It guarantees things `npx` cannot, and this table is the
148
+ prep probe's, unsoftened.
149
+
150
+ | guarantee | `tools/bantamkit-mcp` | `npx bantamkit-mcp` |
151
+ |---|---|---|
152
+ | present in every checkout **and** every worktree | yes — it is a file in the tree | **gone.** Depends on the host's PATH, not on the project directory |
153
+ | runs *this* checkout's code | yes | **gone for developers too.** Testing a worktree needs an absolute `node <worktree>/dist/cli.js`, or the host silently runs the published build |
154
+ | code from the worktree, deps from the main checkout | yes (`PYTHONPATH` + `PYTHONSAFEPATH`) | **no Node analogue.** `NODE_PATH` is ignored by ESM and there is no `-P`. `npm link` and workspaces are a different failure surface, not the same one solved |
155
+ | stdin is the JSON-RPC channel | yes | yes, with `-y` |
156
+ | names the cause when dependencies are missing | yes | the analogous failure is *no network*, and it has no message at all — see the table above |
157
+ | starts without a network | yes | **no**, on a cold cache |
158
+ | `--which`, for diagnosing which endpoint answered | the flag exists | **not in the npx CLI** — but `tools/bantamkit-mcp-node --which` has it, see below |
159
+ | one config line, no clone, no venv | no | **yes.** This is the whole reason the package exists |
160
+
161
+ ### `--which` is a launcher flag, not a package flag
162
+
163
+ This section previously read "`--which` is deleted, not ported" and said "there is no
164
+ Node `--which`, and none is planned". Both are now false, and the second was made false
165
+ inside this repository: `tools/bantamkit-mcp-node` ships `--which`, and
166
+ `runtime-ts/test/launcher.test.mjs` runs it — including on a checkout that has never been
167
+ built, which is the case the Python flag's `find_spec` was chosen for. It prints
168
+ `checkout=`, `deps_root=`, `runtime=node`, `node=`, `entry=` (suffixed `(missing)` when
169
+ `dist/` is absent) and `sdk=`.
170
+
171
+ What is genuinely not ported is `--which` **on the published package**, and the reason is
172
+ the line above it in the table: `npx bantamkit-mcp` does not run a checkout, so
173
+ `checkout=` and `source=` have nothing to report. The question a reader has about an npx
174
+ endpoint is not *where did this resolve* but *which build answered*, and that is
175
+ `build_identity` — a tool on the wire rather than a flag on a launcher, and what the next
176
+ section is about.
177
+
178
+ The old wording came from a real defect, which is now closed. `tools/bantamkit-mcp:47`
179
+ used to name `tools/mcpreach/mcpreach.py` as `--which`'s consumer and `docs/mcp.md` used
180
+ to document a five-value exit-code interface for that program (`0` REACHABLE, `1`
181
+ UNREACHABLE, `2` FOREIGN, `3` UNDECLARED, `4` NO_ENV). **That program has never existed** —
182
+ `git log --all --diff-filter=A -- '*mcpreach*'` is empty across every ref in this
183
+ repository, and `docs/eval.md` records the decision not to ship it, the half-built checker
184
+ having "never been seen to fire". Both citations were rewritten on 2026-08-24 to name what
185
+ actually runs, and `runtime-py/tests/test_doc_commands_gate.py` is now red if any fenced
186
+ shell block in the repository names a `tools/` program that is not in the tree.
187
+
188
+ ## Silent version float, and the instrument for it
189
+
190
+ This is the one that will bite a team hardest.
191
+
192
+ `npx -y bantamkit-mcp` resolves the dist-tag `latest` and caches the result. Two people
193
+ with byte-identical `.mcp.json` files can be running different builds — one resolved last
194
+ week, one resolved this morning — and nothing in the config, the logs or the tool output
195
+ says so. The sh launcher could not do this: it ran the checkout you were standing in.
196
+
197
+ Call `build_identity`. Three fields answer it:
198
+
199
+ - **`runtime`** — `"node"` here, absent on the Python server. Its *presence* is the
200
+ discriminator, and it is folded into `build_id` so the two lineages cannot collide even
201
+ if their code digests agreed.
202
+ - **`assets_digest`** — sha256 over every byte of the asset pack, computed identically in
203
+ both runtimes and **verified equal**: `sha256:b03141bf…` over 83 files from Python and
204
+ from Node. This is the cross-runtime instrument; two machines disagreeing here are
205
+ serving different data.
206
+ - **`code_digest`** / **`build_id`** — over `dist/**/*.js` on this side and `*.py` on the
207
+ other, so they are *required* to differ across runtimes and required to match across two
208
+ installs of the same version. `build_id` differing between two teammates on the same
209
+ version string is the float, caught.
210
+
211
+ The output carries a `cross_runtime` sentence saying in plain words which fields are
212
+ comparable. `git_commit` is refused rather than guessed: an installed npm tarball carries
213
+ no repository, and reading a checkout's HEAD would describe the tree rather than the bytes
214
+ that were imported (RB-P84).
215
+
216
+ **Pin the version in the config** if you want this to be a non-issue:
217
+ `"args": ["-y", "bantamkit-mcp@0.25.0"]`.
218
+
219
+ ## The second bin: `bantamkit-memory`
220
+
221
+ `package.json` declares **two** bins, so an install puts two commands on the path. Packed
222
+ and installed into an empty scratch directory, that is what arrives:
223
+
224
+ ```console
225
+ $ npm install ./bantamkit-mcp-0.25.0.tgz
226
+ added 95 packages in 5s
227
+ $ ls -l node_modules/.bin/
228
+ bantamkit-mcp -> ../bantamkit-mcp/dist/cli.js
229
+ bantamkit-memory -> ../bantamkit-mcp/dist/memory/cli.js
230
+ ```
231
+
232
+ `bantamkit-mcp` is the server the `.mcp.json` line at the top launches, and its stdout is
233
+ the JSON-RPC wire — not a thing you run by hand. `bantamkit-memory` is the operator CLI
234
+ for memory-store lifecycle, and it prints reports. That is the whole reason it is a
235
+ second bin instead of a subcommand: lifecycle output on the server's stdout would corrupt
236
+ the transport, and `bantamkit-mcp`'s help is a byte-compared artifact against
237
+ `python -m bantamkit.mcpserver -h`, which a subparsers action would move.
238
+
239
+ Five subcommands, scoped to the writable **project** layer only — read-only grants and the
240
+ profile store are out of its reach by the code path, not by convention:
241
+
242
+ | subcommand | what it does |
243
+ |---|---|
244
+ | `status` | index size, budget, headroom, archive count |
245
+ | `lint` | exit 1 if the store is malformed or over budget |
246
+ | `compact` | archive the stalest facts until the index fits |
247
+ | `archived` | list what compaction has moved out |
248
+ | `restore NAME` | move an archived fact back |
249
+
250
+ Each takes `--store PATH` or `--start DIR`; with neither, it resolves the project store
251
+ the way `Memory.layered()` does. Exit codes are `0` success, `1` a failure the operator
252
+ must act on (over budget, a malformed fact, a refused restore), `2` a usage error — so
253
+ `lint` drops into a pre-commit hook or a CI job unchanged.
254
+
255
+ Driven off `node_modules/.bin/` from the install above, against a scratch five-fact store:
256
+
257
+ ```console
258
+ $ bantamkit-memory status --store store
259
+ store: store
260
+ facts: 5
261
+ index: 576 bytes
262
+ budget: 24000
263
+ headroom: 23424
264
+ archived: 0
265
+ $ bantamkit-memory lint --store store --budget 400
266
+ lint: FAIL — index is 576 bytes, budget is 400
267
+ try: bantamkit-memory compact --store store --budget 400
268
+ $ echo $?
269
+ 1
270
+ $ bantamkit-memory compact --store store --budget 400
271
+ compacted 3 fact(s)
272
+ index: 576 -> 236 bytes (budget 400, target 267, reserve 133, headroom 164)
273
+ archived -> store/archive
274
+ assets-pack-has-eighty-four-files (project, 133 bytes)
275
+ ci-runner-is-macos-only (project, 97 bytes)
276
+ conformance-runner-entrypoint (reference, 110 bytes)
277
+ restore one with: bantamkit-memory restore <name> --store store
278
+ $ bantamkit-memory archived --store store
279
+ archived facts: 3 (store/archive)
280
+ assets-pack-has-eighty-four-files
281
+ ci-runner-is-macos-only
282
+ conformance-runner-entrypoint
283
+ $ bantamkit-memory restore ci-runner-is-macos-only --store store
284
+ restored 'ci-runner-is-macos-only' — index now 333/24000 bytes
285
+ ```
286
+
287
+ `compact` printing every name that left is the point: `archive/` is a directory nothing
288
+ reads back on its own, so a compaction whose output is not shown is a silent deletion as
289
+ far as the operator is concerned.
290
+
291
+ Without installing, `npx -p bantamkit-mcp bantamkit-memory status` runs it out of the
292
+ registry. `-p` is not optional — the package name and this bin's name differ, and plain
293
+ `npx bantamkit-memory` would go looking for a package called `bantamkit-memory`. Measured
294
+ here against the local tarball rather than the registry, since this version is not
295
+ published:
296
+
297
+ ```console
298
+ $ npx -y -p ./bantamkit-mcp-0.25.0.tgz bantamkit-memory status --store npxstore
299
+ store: npxstore
300
+ facts: 0
301
+ index: 0 bytes
302
+ budget: 24000
303
+ headroom: 24000
304
+ archived: 0
305
+ ```
306
+
307
+ **The Python install does not provide this command**, which is why the two spellings
308
+ exist. Measured against the distribution in this repository's venv:
309
+
310
+ ```console
311
+ $ .venv/bin/python -c "from importlib.metadata import distribution; d=distribution('bantamkit'); print(sorted(e.name for e in d.entry_points if e.group=='console_scripts'))"
312
+ ['bantamkit-mcp']
313
+ $ env PATH="$PWD/.venv/bin:/usr/bin:/bin" sh -c 'command -v bantamkit-memory'
314
+ $ echo $?
315
+ 1
316
+ ```
317
+
318
+ One console script, and it is the server. The Python operator therefore types
319
+ `python -m bantamkit.memory`, and the two CLIs are identical bytes after substituting one
320
+ for the other — except the wrap, because argparse's hanging indent is
321
+ `len(prefix) + len(prog) + 1`. The reason there is no third spelling both installs could
322
+ use lives in one place, the `prog` row of `docs/porting.md`'s divergence table, and this
323
+ page does not restate it. `docs/memory.md`'s *The operator CLI* is the full reference;
324
+ `tools/conformance/suites/memorycli.mjs` is the gate that compares the two.
325
+
326
+ ## The default is layered — do not add `--store` by reflex
327
+
328
+ With **no arguments**, which is what the config above passes and what production runs, the
329
+ server binds `Memory.layered`: the project store discovered from the working directory,
330
+ plus the profile layer, plus any `extra_stores` from `.bantamkit/config.yaml`. Recall lines
331
+ are prefixed with their layer — `[project] `, `[extra:<name>]`, `[profile]`.
332
+
333
+ `--store <path>` binds one store and drops the tag. It is a debugging flag. A recall string
334
+ produced under `--store` is not a string the deployment ever emits, and the cold-start gate
335
+ asserts the layered form specifically for that reason.
336
+
337
+ ## The asset pack
338
+
339
+ The pack is language-agnostic data (tool manifests, schemas, skills, rubrics, contract
340
+ wording, eval fixtures) that lives at the **repository root**, one level above this
341
+ package, and is shared verbatim with `runtime-py`. npm cannot reach outside a package
342
+ directory, so `scripts/sync-assets.mjs` vendors it into `runtime-ts/assets/` on `prepack`;
343
+ that directory is generated and git-ignored.
344
+
345
+ `build_identity` hashes **every byte of the whole tree**, not just the 20 files the tools
346
+ read, so the pack ships whole — 83 files / 212,480 bytes — or `assets_digest` and
347
+ `build_id` change. `test/packaging.test.mjs` asserts that against what `npm pack` would
348
+ actually put in the tarball, never against the source tree.
349
+
350
+ `assetsRoot()` mirrors `runtime-py/src/bantamkit/assets.py` arm for arm:
351
+
352
+ 1. `$BANTAMKIT_ASSETS`, verbatim, with no existence check.
353
+ 2. `<package>/assets/` — one level above `dist/`. **This is the arm `npx` uses.**
354
+ 3. `<repo>/assets/` — two levels above `dist/`, for a dev checkout with nothing vendored.
355
+ 4. Otherwise `AssetNotFound: no assets directory found; set BANTAMKIT_ASSETS`.
356
+
357
+ ## Sharing a store with the Python server
358
+
359
+ Both servers read and write the same memory store, and byte-compatibility is the product,
360
+ not a nice-to-have. Two things to know:
361
+
362
+ - **Concurrent writes are safe for the memory store and lossy for the checkpoint, in both
363
+ runtimes.** `index.md` is derived from the `facts/` directory, so the last writer
364
+ re-enumerates everything and the index self-heals — measured over 8 trials of two
365
+ processes × 15 saves, zero disagreements between `facts/` and `index.md`. A checkpoint is
366
+ not derived that way: `shiftwork_clock_out` is read-modify-write and the write depends on
367
+ the document it read, so two concurrent clock-outs on the *same cursor unit* both answer
368
+ `ok` and one history entry is lost. Measured 10/10 trials on Node **and** 10/10 on the
369
+ Python reference — reference behaviour reproduced, not something the port introduced. The
370
+ cursor check serialises the normal case; only the same-unit collision loses.
371
+ - **Do not hand-edit a fact file's `name`, `description` or `type` to a bare non-string.**
372
+ `description: 2026` is a YAML integer, not the text `2026`. Python interpolates it and
373
+ carries on; this port refuses the whole store with `malformed fact file …`. Quote it:
374
+ `description: '2026'`. Every fact the tools *write* is quoted correctly, so this only
375
+ reaches you through an editor. See `src/memory/factfile.ts:257`.
376
+
377
+ ## Development
378
+
379
+ ```sh
380
+ npm install
381
+ npm run build # tsc -> dist/
382
+ npm test # node --test; `npm test -- packaging` selects one file
383
+ npm pack --dry-run # vendors the pack via prepack, lists the tarball
384
+ ```
385
+
386
+ From the repository root:
387
+
388
+ ```sh
389
+ node tools/conformance/run.mjs --all # Node vs Python, 4300+ cases
390
+ node tools/conformance/npx-cold-start.mjs # pack, cold npx, warm npx, PATH
391
+ node tools/conformance/npx-cold-start.mjs --offline # + the no-registry probe (~2.5 min)
392
+ ```
393
+
394
+ The conformance harness shells out to a CPython to produce the reference side. That is a
395
+ **development-time** dependency of the test tooling; nothing under `tools/` ships, and
396
+ `files: ["dist", "assets"]` is the whole published surface.
397
+
398
+ Runtime dependencies: exactly one, `@modelcontextprotocol/sdk`, pinned to `1.30.0`.
@@ -0,0 +1,90 @@
1
+ name: default
2
+ schema_instruction: "Return ONLY a JSON object matching this JSON Schema. No prose.\n"
3
+ schema_retry: "{error}\nReturn ONLY a JSON object matching the schema."
4
+ critique_feedback: "A reviewer scored your answer {score}/10 (needs >= {threshold}). Feedback: {feedback}\nIf the feedback says a value is missing, unverified, or absent from your evidence, that is a fact you never looked up: call your tools and read the source that has it. Do not reword, generalise, or hedge the previous answer to work around the gap. Then answer again."
5
+ parse_error: "output was not parseable JSON: {detail}"
6
+ validation_error: "JSON does not match schema at '{where}': {detail}"
7
+ json_answer_retry: "Your answer contains no JSON. Restate your final answer as ONLY the JSON requested by the task, with no prose around it."
8
+ loop_note: "(you have now received this exact result {count} times; it will not change. Do something different or give your final answer now)"
9
+ loop_warn: "(STOP calling tools. Give your final answer now, in exactly the format the task asked for.)"
10
+ evidence_line: "{name}({arguments}) -> {observation}"
11
+ evidence_no_observation: "(no observation)"
12
+ evidence_empty: "(no tool calls were made)"
13
+ # The document reader pair's observations (`document_list`, `document_read`). Every string a
14
+ # reader hands back to the model lives here and not in the harness, for the reason the layer
15
+ # rule gives: the mechanics measured in `docread.py` transfer across models, this wording is
16
+ # exactly what does not.
17
+ document_manifest_empty: "no documents are attached to this task"
18
+ document_manifest_part: "{document} ({kind}) part {index} \"{part}\": {rows} rows, numbered 0 to {last}"
19
+ document_manifest_header_row: " row 0 is the header: {row}"
20
+ document_manifest_first_row: " row 1 is the first data row: {row}"
21
+ document_manifest_last_row: " row {index} is the last data row: {row}"
22
+ # What the rendering left out, stated in the one place that says what exists. Measured need
23
+ # (J25, the user's real ~/Downloads, 2026-08-20): `step test.xlsx` is 18.62 MB and the manifest
24
+ # said `4 parts, 28 rows` — all 28 of those rows are empty lines and the file's actual content
25
+ # is 56 embedded PNGs. Every line here is a COUNT: an adjective ("some images") tells a model
26
+ # nothing it can decide with, and silence tells it something false.
27
+ document_manifest_package_media: "{document}: the file also holds {count} embedded file(s) ({what}) totalling {bytes} bytes, which no row can carry — this reader renders no image or embedded object"
28
+ document_manifest_omitted_media: " NOT in those rows: {count} embedded file(s), {bytes} bytes, anchored to this part"
29
+ document_manifest_omitted_blank: " {count} of those {rows} rows carry no cell value at all and render as an empty line"
30
+ document_manifest_omitted_format: " column(s) {where}: {count} cell(s) store a NUMBER under the date/time format {what} — this reader renders the stored serial number verbatim and does not convert it to a date; convert it with that format code if you need one"
31
+ # J25-D3, the PDF reader. Two silences a page-addressable reader can produce that a
32
+ # spreadsheet cannot: a page that rendered no row (a scan, or glyphs no font maps), and
33
+ # characters dropped because the file states no mapping for them. An empty page and a page
34
+ # nobody could read are different facts and the model must be able to tell them apart.
35
+ document_manifest_omitted_unread_page: " this part rendered NO row: {why}. It draws {count} image(s), {bytes} bytes, that no row can carry — no row means this reader recovered no text from the page, which is NOT the same as the page being empty, and there is no OCR here"
36
+ document_manifest_unread_no_operator: "the page ran no text-showing operator at all, so nothing on it was ever text"
37
+ document_manifest_unread_unmapped: "the page ran {show_ops} text-showing operator(s) and every one of the {unmapped} character(s) they showed came through a font that declares no character map, so this reader dropped them rather than guess at them"
38
+ document_manifest_unread_whitespace: "the page ran {show_ops} text-showing operator(s) and this reader mapped every one of the {vouched} character(s) they showed, and every one of those characters is WHITESPACE — the text was recovered and it carries nothing, which is a fact about the page and not a failure of this reader"
39
+ document_manifest_unread_no_character: "the page ran {show_ops} text-showing operator(s) that put no character on the page at all, neither one this reader could map nor one it had to drop"
40
+ document_manifest_omitted_unmapped: " {count} character(s) shown on this part are NOT in those rows: their codes came from font(s) with no /ToUnicode map ({what}), so the file stores indices into a font's own glyphs and says nowhere which character each glyph draws — this reader drops them rather than guess, because a guess here is indistinguishable from content"
41
+ document_manifest_omitted_other: " NOT in those rows: {count} {subject} ({what})"
42
+ document_page_header: "{document} \"{part}\" rows {start}-{end} of {rows}; each line below begins with its own row number"
43
+ document_page_next: "more rows follow: call document_read again with offset={next_offset}"
44
+ document_page_end: "that was the last row of \"{part}\""
45
+ document_page_truncated: "row {index} was too long for one page and was cut: {dropped} bytes dropped"
46
+ document_unknown: "error: no document named {name}; this task has: {available}"
47
+ document_offset_past_end: "error: offset {offset} is past the end of \"{part}\", which has {rows} rows numbered 0 to {last}"
48
+ document_error: "error: {detail}"
49
+ # `bantamkit_read` (the MCP reader, job43) reuses every `document_*` sentence above with the
50
+ # path standing in for `{document}`. Two sentences are genuinely its own: its continuation
51
+ # line names ITS tool, and a part it does not have is a fact about the file, not the task.
52
+ bantamkit_read_page_next: "more rows follow: call bantamkit_read again with offset={next_offset}"
53
+ bantamkit_read_unknown_part: "error: no part named \"{part}\" in {path}; it has: {available}"
54
+ # The `paste` arm's system message (`docs/eval-data/2026-08-20-document-read-bar.md` §10.2,
55
+ # PASTE_MAX_BYTES = 8621 under Amendment 1, 2026-08-20). The arm registers NO tools, so these four lines are the whole of
56
+ # what the model is told ABOUT the corpus; the rows themselves are docread's own rendering,
57
+ # unprefixed and unreformatted, so `paste` and `document_read` differ in DELIVERY and not in
58
+ # content. Clause 4 makes stating the paste's own completeness part of the contract, and
59
+ # `paste_truncated` says the missing rows are unreachable rather than leaving the model to
60
+ # hunt for a tool that is not there: a paste that lets a truncated head read as the whole
61
+ # sheet is a different, worse arm than the one the bar pre-registered.
62
+ document_paste_preamble: "The following document content is attached to this task. Fields in a row are separated by tabs, rows are given in order, and row 0 of each part is its header."
63
+ document_paste_part: "{document} ({kind}) part {index} \"{part}\": {rows} rows, numbered 0 to {last}; {shown} of them are shown below."
64
+ document_paste_complete: " rows 0 to {last_shown} are shown, which is every row of \"{part}\": this copy is COMPLETE."
65
+ document_paste_truncated: " rows 0 to {last_shown} are shown; rows {first_missing} to {last} of \"{part}\" are NOT shown and no tool is attached that can fetch them. This copy is PARTIAL."
66
+ document_paste_none: " no rows of \"{part}\" are shown and no tool is attached that can fetch them."
67
+ # What the model is told when a tool CALL cannot be made or fails, rather than when a tool
68
+ # answers with its own error (`document_unknown` and friends above are that). It lives here
69
+ # for the reason the layer rule gives and for one measured reason: before 2026-08-20 the
70
+ # dispatcher formatted this sentence itself and interpolated the raw exception, so a model
71
+ # that sent one extra argument was handed
72
+ # `_document_tools.<locals>.list_documents() got an unexpected keyword argument 'document'`
73
+ # — a Python qualname, model-facing, owned by no asset and pinned by no golden. The
74
+ # argument-shaped failures are intercepted before the handler is called now, so `{detail}`
75
+ # carries a handler's OWN sentence and no longer a signature fragment.
76
+ tool_failed: "error: {tool} failed: {detail}. fix the arguments and retry."
77
+ tool_arguments: "error: {tool} does not take the arguments it was given. it takes: {accepts}. fix the arguments and retry."
78
+ tool_arguments_none: "error: {tool} takes no arguments at all. call it with none and retry."
79
+ # 2026-08-20, `RB-P86`. The sibling of the two above, one layer in. `select_declared_arguments`
80
+ # drops keys the schema does not declare and type-checks nothing, so `llama3.2:3b` — which
81
+ # emits a JSON-Schema FRAGMENT as the value of a declared parameter,
82
+ # `{"document": {"description": "stock", "type": "string"}}` — got its dict straight through to
83
+ # `docs.get(name)` and the model read `unhashable type: 'dict'`, thirteen times over 432 graded
84
+ # runs. That is CPython's sentence about a hash table, not a tool's sentence about its own
85
+ # arguments, and a 3b that receives it stops calling tools and answers that it cannot access
86
+ # the workbook. What it is told now names the argument, the type the tool declares and the type
87
+ # it was sent, so the next turn has something to correct. One frame for one or many, because
88
+ # the count is not the thing the model has to act on.
89
+ tool_argument_types: "error: {tool} was called with the wrong type of argument. {problems}. fix the arguments and retry."
90
+ tool_argument_type: "{argument} must be type {expected}, not type {actual}"