bantamkit-mcp 0.31.0 → 0.32.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.
- package/dist/memory/component.js +66 -5
- package/dist/shiftwork.d.ts +4 -1
- package/dist/shiftwork.js +37 -3
- package/package.json +1 -1
package/dist/memory/component.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* lines carry a `[project] ` tag that the `--store` form never emits. A port validated
|
|
26
26
|
* against only one of them ships a string the deployment never produces.
|
|
27
27
|
*/
|
|
28
|
+
import { realpathSync } from 'node:fs';
|
|
29
|
+
import { resolve } from 'node:path';
|
|
28
30
|
import { BantamError } from '../errors.js';
|
|
29
31
|
import { pyStrip } from './factfile.js';
|
|
30
32
|
import { countFacts, loadGrants, MEMORY_DIR_ENV, PROJECT_STORE, resolveProjectStore, } from './layers.js';
|
|
@@ -60,6 +62,55 @@ export function normalizeName(name) {
|
|
|
60
62
|
export function profileStore() {
|
|
61
63
|
return pyJoin(pyHome(), ...PROJECT_STORE);
|
|
62
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether two paths name ONE directory, symlinks and `/var` vs `/private/var` included.
|
|
67
|
+
*
|
|
68
|
+
* Port of `_same_directory` (`runtime-py/src/bantamkit/memory/component.py`, J47-1,
|
|
69
|
+
* 0844ccb). Realpath, not string equality, and that distinction is measured rather than
|
|
70
|
+
* tidy: on macOS the walk up from a cwd under `/var` returns `/private/var/...` while
|
|
71
|
+
* `pyHome()` returns `/var/...`, so two spellings of one directory compare unequal as
|
|
72
|
+
* strings. The Stop hook's `samePath` (`tools/hooks/bantamkit-hook.mjs`) compares the same
|
|
73
|
+
* two roots the same way and for the same reason — it carried the identical defect, and
|
|
74
|
+
* J47-7 (`a109f99`) brought it to the same `realpathSync.native`, following this function
|
|
75
|
+
* rather than leading it. When the resolution itself
|
|
76
|
+
* fails, an absolute-path comparison is the honest fallback: it can only under-report a
|
|
77
|
+
* match, never invent one.
|
|
78
|
+
*
|
|
79
|
+
* AMENDED 2026-09-11 (J47-3B). The paragraph above stands as the reason this function
|
|
80
|
+
* resolves rather than compares strings, and the fallback's behaviour is unchanged. Two
|
|
81
|
+
* things it says are now known to be wrong about `fs.realpathSync` in particular, and
|
|
82
|
+
* measured: (1) it is NOT the twin of `os.path.realpath`. `realpathSync` hands its argument
|
|
83
|
+
* to `path.resolve` before it resolves anything, and `path.resolve` pops `..` LEXICALLY —
|
|
84
|
+
* before the symlink in front of it is followed. `os.path.realpath` and the kernel pop it
|
|
85
|
+
* AFTER. So a `HOME` spelled `<bed>/link/..`, where `link -> <bed>/deep/real`, is
|
|
86
|
+
* `<bed>/deep` to the reference and `<bed>` to Node: `realpathSync` then threw ENOENT on a
|
|
87
|
+
* directory that is there, the fallback under-reported, and `layered` bound one directory as
|
|
88
|
+
* two layers — the 2026-09-10 self-merge, still live on this side until now. (2) "it can
|
|
89
|
+
* only under-report a match, never invent one" was offered as reassurance, and
|
|
90
|
+
* under-reporting is the DANGEROUS direction here: a missed match IS the duplicate layer.
|
|
91
|
+
*
|
|
92
|
+
* The mechanism is now `realpathSync.native` — libuv's `uv_fs_realpath`, i.e. the platform's
|
|
93
|
+
* own `realpath(3)` / `GetFinalPathNameByHandle` — chosen because it pops `..` in the
|
|
94
|
+
* kernel's order and therefore agrees with `os.path.realpath` byte for byte on that bed,
|
|
95
|
+
* because it keeps the macOS `/var` vs `/private/var` resolution the original was written
|
|
96
|
+
* for, and because it adds no path arithmetic of this port's own. `statSync` identity
|
|
97
|
+
* (`dev` + `ino`) was the other candidate and was not taken: `ino` is not dependable on
|
|
98
|
+
* every Windows filesystem, and this runtime has to answer the same on Windows.
|
|
99
|
+
*
|
|
100
|
+
* The fallback stays `resolve(a) === resolve(b)`, and the under-report it can still produce
|
|
101
|
+
* is provably harmless where `layered` calls this: the native call throws only for a path
|
|
102
|
+
* that is not on disk, and `binding.path` always is — `layered` constructs that store, which
|
|
103
|
+
* creates it, before the guard runs — so a throwing side is a directory that does not exist,
|
|
104
|
+
* and that cannot be the same directory as one that does.
|
|
105
|
+
*/
|
|
106
|
+
function sameDirectory(a, b) {
|
|
107
|
+
try {
|
|
108
|
+
return realpathSync.native(a) === realpathSync.native(b);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return resolve(a) === resolve(b);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
63
114
|
/** The name a layer answers under: the PROJECT directory, not the store directory. */
|
|
64
115
|
export function layerLabel(root) {
|
|
65
116
|
if (pyName(pyParent(root)) === '.bantamkit')
|
|
@@ -110,11 +161,21 @@ export class Memory {
|
|
|
110
161
|
false,
|
|
111
162
|
]);
|
|
112
163
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
164
|
+
// ONE DIRECTORY IS ONE LAYER. The walk above starts at the cwd and climbs, so a
|
|
165
|
+
// session with no `.bantamkit` anywhere above it resolves `~/.bantamkit/memory` —
|
|
166
|
+
// the profile store — as its PROJECT store. Binding that directory a second time
|
|
167
|
+
// gave `dream` the same store twice: every fact collided with itself, was merged
|
|
168
|
+
// into itself, and the "profile copy" that was archived was the same file. It
|
|
169
|
+
// archived 20 of 20 of the user's real facts on 2026-09-10. Such a session has one
|
|
170
|
+
// layer, and `dreamOutcome` already has a true thing to say about that.
|
|
171
|
+
const profileRoot = profileStore();
|
|
172
|
+
if (!sameDirectory(profileRoot, binding.path)) {
|
|
173
|
+
mem.layers.push([
|
|
174
|
+
'profile',
|
|
175
|
+
new MemoryStore(profileRoot, { k, create: false, ...(today ? { today } : {}) }),
|
|
176
|
+
false,
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
118
179
|
return mem;
|
|
119
180
|
}
|
|
120
181
|
/** The labels, in order. `_layers` is private in the reference too; this is for the tests. */
|
package/dist/shiftwork.d.ts
CHANGED
|
@@ -37,7 +37,10 @@ export declare function clockIn(checkpoint: string): PyValue;
|
|
|
37
37
|
* `unitId` must name the cursor unit — the contract is execute-the-cursor (driver parity),
|
|
38
38
|
* never pick-a-unit. When `job.roles` names the unit's role, `accounting.model` must be one
|
|
39
39
|
* of that role's models, spelled exactly; a wrong or missing model is refused here, before
|
|
40
|
-
* any mutation and before the accounting line.
|
|
40
|
+
* any mutation and before the accounting line. Extended 2026-09-11 (job47): so is a
|
|
41
|
+
* `job.roles` value for that role that is not a list of model identifiers — an unreadable
|
|
42
|
+
* declaration allows no model, and it is refused in the same place, by the same structured
|
|
43
|
+
* return, writing nothing. Mutations: set the unit's status, advance
|
|
41
44
|
* `plan.cursor` to the first non-terminal unit in PLAN order (`depends_on` is ignored),
|
|
42
45
|
* shallow-merge `handoffPatch` into `handoff`, push `historyEntry` onto the 5-entry ring.
|
|
43
46
|
*
|
package/dist/shiftwork.js
CHANGED
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
* therefore pinned as per-side literals in each runtime's own tests. The check runs at
|
|
40
40
|
* validation time — after the cursor check, before the first mutation, and so before the
|
|
41
41
|
* log-then-commit pair below — because a refusal taken after the append would leave an
|
|
42
|
-
* orphan accounting line claiming a model that was rejected.
|
|
42
|
+
* orphan accounting line claiming a model that was rejected. J47-5: a role the map DOES
|
|
43
|
+
* name whose value is not a list of model identifiers is refused there too — an unreadable
|
|
44
|
+
* declaration allows no model, and the refusal is the same structured one, taken in the
|
|
45
|
+
* same place. That makes three refusal strings, not two, and the third is likewise a
|
|
46
|
+
* per-side literal.
|
|
43
47
|
*
|
|
44
48
|
* WHAT IS NOT PORTED, DELIBERATELY. The `depends_on` field is ignored on cursor advance
|
|
45
49
|
* (v1-linear, the Python module's own ruling), there is no lock (the MCP topology has one
|
|
@@ -252,6 +256,25 @@ function pyFormat(value) {
|
|
|
252
256
|
* string and the sentence says so. Reachable, and therefore measured: `BANTAMKIT_ASSETS` is
|
|
253
257
|
* honoured by both runtimes, so the tests and the conformance corpus drive this branch
|
|
254
258
|
* through a pack whose schema has lost `minItems`.
|
|
259
|
+
*
|
|
260
|
+
* AND A DECLARATION THIS CODE CANNOT READ IS NOT A LICENCE (J47-4, ported here as J47-5).
|
|
261
|
+
* The same ruling, one step further out: a role the map names is constrained by what it
|
|
262
|
+
* NAMES, and a value that is not a list of model identifiers names nothing, so it allows
|
|
263
|
+
* nothing. This port failed open five ways and mangled the declaration two more. MEASURED
|
|
264
|
+
* end to end under a pack whose `job.roles.additionalProperties` is `true`, with
|
|
265
|
+
* `model: 'haiku'`: a string, a dict, a number, a null and a bool all came back
|
|
266
|
+
* `{"result":"ok"}` with the status set, the cursor advanced and an accounting line
|
|
267
|
+
* written — the clock-out COMPLETED — while `[5]` and `['ok', null]` refused with the
|
|
268
|
+
* unreadable value rendered into the sentence as `5` and `ok, None`. A list holding a
|
|
269
|
+
* non-string is the shape a bare kind test cannot catch, which is why the property is
|
|
270
|
+
* LIST OF STRINGS and not merely list; the reference raised `TypeError: sequence item 0`
|
|
271
|
+
* on exactly that input. Fail CLOSED, through the same structured `_error` return, saying
|
|
272
|
+
* only what is true — the declaration cannot be read, therefore no model is allowed —
|
|
273
|
+
* naming no type (a Python type name would not port: `int` against `number`) and
|
|
274
|
+
* rendering no part of the value. ONE sentence for both accounting shapes: which model
|
|
275
|
+
* was reported cannot matter when the declaration that would judge it is unreadable.
|
|
276
|
+
* `[]` is untouched by this — an empty list IS a list of model identifiers and keeps the
|
|
277
|
+
* sentence J46-10 pinned.
|
|
255
278
|
*/
|
|
256
279
|
function modelRefusal(document, unitId, unit, accounting) {
|
|
257
280
|
const role = text(field(unit, 'role'));
|
|
@@ -259,8 +282,16 @@ function modelRefusal(document, unitId, unit, accounting) {
|
|
|
259
282
|
const allowed = roles === undefined || roles.t !== 'dict' ? undefined : roles.v.get(role);
|
|
260
283
|
// `role not in roles`. The `t !== 'list'` arm is a TYPE guard and not a policy: the schema
|
|
261
284
|
// pins the value to an array, so the only way past it is a document no read would accept.
|
|
262
|
-
|
|
285
|
+
// AMENDED 2026-09-11 (J47-5): the two lines above stand as written and are now FALSE as a
|
|
286
|
+
// description of this code. A document no read would accept is exactly what a shared asset
|
|
287
|
+
// drifting produces, the arm answered `unconstrained` for every such document and the
|
|
288
|
+
// clock-out completed, and `t === 'list'` was never the property anyway — `['ok', null]`
|
|
289
|
+
// passed the kind test and mangled `None` into the sentence. The arm below is a POLICY.
|
|
290
|
+
if (allowed === undefined)
|
|
263
291
|
return null;
|
|
292
|
+
if (allowed.t !== 'list' || !allowed.v.every((model) => model.t === 'str')) {
|
|
293
|
+
return `unit ${unitId} in role ${role} cannot clock out: job.roles.${role} is not a list of model identifiers, so it allows no model`;
|
|
294
|
+
}
|
|
264
295
|
const names = allowed.v.map((model) => pyFormat(model)).join(', ');
|
|
265
296
|
const offered = asPatch(accounting).get('model');
|
|
266
297
|
if (offered === undefined || offered.t === 'null') {
|
|
@@ -280,7 +311,10 @@ function modelRefusal(document, unitId, unit, accounting) {
|
|
|
280
311
|
* `unitId` must name the cursor unit — the contract is execute-the-cursor (driver parity),
|
|
281
312
|
* never pick-a-unit. When `job.roles` names the unit's role, `accounting.model` must be one
|
|
282
313
|
* of that role's models, spelled exactly; a wrong or missing model is refused here, before
|
|
283
|
-
* any mutation and before the accounting line.
|
|
314
|
+
* any mutation and before the accounting line. Extended 2026-09-11 (job47): so is a
|
|
315
|
+
* `job.roles` value for that role that is not a list of model identifiers — an unreadable
|
|
316
|
+
* declaration allows no model, and it is refused in the same place, by the same structured
|
|
317
|
+
* return, writing nothing. Mutations: set the unit's status, advance
|
|
284
318
|
* `plan.cursor` to the first non-terminal unit in PLAN order (`depends_on` is ignored),
|
|
285
319
|
* shallow-merge `handoffPatch` into `handoff`, push `historyEntry` onto the 5-entry ring.
|
|
286
320
|
*
|