bantamkit-mcp 0.32.1 → 0.34.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/README.md +196 -23
- package/assets/schemas/shiftwork-checkpoint.json +5 -1
- package/assets/tools/bantamkit_read.json +1 -3
- package/assets/tools/repo_map.json +1 -3
- package/assets/tools/shiftwork_clock_in.json +1 -1
- package/assets/tools/shiftwork_clock_out.json +38 -3
- package/assets/tools/skill_audit.json +1 -1
- package/assets/tools/token_ledger.json +1 -1
- package/dist/cli.js +5 -4
- package/dist/eventlog.js +29 -5
- package/dist/hostinstall.d.ts +39 -7
- package/dist/hostinstall.js +105 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/pyargs.js +5 -0
- package/dist/mcp/server.d.ts +20 -8
- package/dist/mcp/server.js +47 -14
- package/dist/memory/cli.js +1 -1
- package/dist/memory/store.d.ts +47 -0
- package/dist/memory/store.js +67 -1
- package/dist/npminstall.d.ts +54 -0
- package/dist/npminstall.js +141 -0
- package/dist/selfupdate.d.ts +39 -38
- package/dist/selfupdate.js +62 -108
- package/dist/shiftwork.d.ts +19 -2
- package/dist/shiftwork.js +268 -10
- package/package.json +1 -1
package/dist/shiftwork.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* * the rewritten checkpoint — `json.dumps(document, indent=2) + "\n"`, `.tmp` then
|
|
11
11
|
* `replace()`;
|
|
12
12
|
* * the `.log.jsonl` line — `json.dumps(record, sort_keys=True) + "\n"`, append mode,
|
|
13
|
-
*
|
|
13
|
+
* read back by exactly ONE thing since job50/F6 — `briefed()`, which looks at the `unit`
|
|
14
|
+
* and `event` keys and nothing else — so a wrong line is still never noticed by the
|
|
15
|
+
* code that wrote it;
|
|
14
16
|
* * the returned dict, which the MCP surface hands straight to the model.
|
|
15
17
|
*
|
|
16
18
|
* THE ensure_ascii RULING. `json.dumps` defaults to `ensure_ascii=True` and
|
|
@@ -51,15 +53,33 @@
|
|
|
51
53
|
* accounting line is appended BEFORE the atomic rename, so a partial failure can lose the
|
|
52
54
|
* commit but never the accounting.
|
|
53
55
|
*/
|
|
54
|
-
import { loadSchema } from './assets.js';
|
|
56
|
+
import { AssetNotFound, loadSchema, loadToolAsset } from './assets.js';
|
|
55
57
|
import { schemaError } from './contract.js';
|
|
56
|
-
import { pyAppendText, pyJoin, PyOSError, pyReplace, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, pyReadText, } from './memory/pyfs.js';
|
|
58
|
+
import { pyAppendText, pyJoin, PyOSError, pyReplace, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, pyReadText, PyUnicodeDecodeError, } from './memory/pyfs.js';
|
|
57
59
|
import { dumpJson, fromJs, parseJson, PyJSONDecodeError, reprValue } from './pyjson.js';
|
|
60
|
+
import { checkSchema, PyJsonSchemaUnsupported } from './pyjsonschema.js';
|
|
58
61
|
export const SCHEMA_NAME = 'shiftwork-checkpoint';
|
|
62
|
+
/** The accounting line's shape lives on the TOOL asset, not in a second copy here (F5). */
|
|
63
|
+
const TOOL_ASSET = 'shiftwork_clock_out';
|
|
64
|
+
/**
|
|
65
|
+
* J50-9A/9B: the one sentence for a pack that cannot supply the accounting shape. FIXED —
|
|
66
|
+
* no path, no exception text, no unit — read out of `ACCOUNTING_SHAPE_UNREADABLE` in the
|
|
67
|
+
* Python module byte for byte, so a differential case can compare the two; which of the
|
|
68
|
+
* shapes failed is not said, because the property is the same for all of them (see
|
|
69
|
+
* `accountingSchema`).
|
|
70
|
+
*/
|
|
71
|
+
export const ACCOUNTING_SHAPE_UNREADABLE = "cannot clock out: the shiftwork_clock_out tool asset cannot be read as the " +
|
|
72
|
+
"accounting line's shape, so it allows no accounting line";
|
|
59
73
|
/** The driver's SUCCESS test. */
|
|
60
74
|
export const TERMINAL_UNIT_STATUS = new Set(['done', 'dropped']);
|
|
61
75
|
/** The schema's `maxItems` — older entries fall off the ring. */
|
|
62
76
|
export const HISTORY_RING_SIZE = 5;
|
|
77
|
+
/**
|
|
78
|
+
* F6: the `event` value of the ledger line `clockIn` appends when it issues a brief. The word
|
|
79
|
+
* is the register's own — `clock_in` answers `result: "brief"` — so the ledger names the
|
|
80
|
+
* thing it recorded with the same word the wire used. `BRIEF_EVENT` in `shiftwork.py`.
|
|
81
|
+
*/
|
|
82
|
+
export const BRIEF_EVENT = 'brief';
|
|
63
83
|
// ------------------------------------------------------------------- tagged-value sugar
|
|
64
84
|
const str = (v) => ({ t: 'str', v });
|
|
65
85
|
const int = (v) => ({ t: 'int', v: BigInt(v) });
|
|
@@ -161,6 +181,100 @@ export function timestamp(now) {
|
|
|
161
181
|
return (`${pad(when.getUTCFullYear(), 4)}-${pad(when.getUTCMonth() + 1)}-${pad(when.getUTCDate())}` +
|
|
162
182
|
`T${pad(when.getUTCHours())}:${pad(when.getUTCMinutes())}:${pad(when.getUTCSeconds())}Z`);
|
|
163
183
|
}
|
|
184
|
+
// ------------------------------------------------------------------------ F6: the ledger
|
|
185
|
+
/**
|
|
186
|
+
* `Path(str(path) + ".log.jsonl")` — string concatenation on the NORMALIZED path, not
|
|
187
|
+
* `with_suffix`, so `cp.json` gets `cp.json.log.jsonl` and not `cp.log.jsonl`.
|
|
188
|
+
*/
|
|
189
|
+
function logPath(path) {
|
|
190
|
+
return `${path}.log.jsonl`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `_record_brief`: append the brief line — best effort, NEVER throws for a store that
|
|
194
|
+
* refuses writes, never changes the brief.
|
|
195
|
+
*
|
|
196
|
+
* One line per call. A unit clocked in twice before it clocks out (a relaunch after a
|
|
197
|
+
* crashed subagent) leaves two brief lines, and a reader should conclude exactly that. The
|
|
198
|
+
* ledger records events, not state.
|
|
199
|
+
*
|
|
200
|
+
* THE NARROWNESS IS THE PORT OF `except OSError`. `pyAppendText` converts every filesystem
|
|
201
|
+
* failure into a `PyOSError` — a read-only directory, a directory where the file belongs, a
|
|
202
|
+
* full disk — and that is the ONLY class swallowed here. Anything else out of this block (a
|
|
203
|
+
* `TypeError` from a record that could not be dumped, a bug in this function) is not an
|
|
204
|
+
* OSError in Python and is rethrown here, because hiding it would leave every unit
|
|
205
|
+
* `briefed: false` forever with no red anywhere. The same `instanceof PyOSError` test the
|
|
206
|
+
* two write legs of `clockOut` already use, for the same reason.
|
|
207
|
+
*/
|
|
208
|
+
function recordBrief(log, unitId, role, now) {
|
|
209
|
+
const record = new Map([
|
|
210
|
+
['event', str(BRIEF_EVENT)],
|
|
211
|
+
['ts', str(timestamp(now))],
|
|
212
|
+
['unit', str(unitId)],
|
|
213
|
+
['role', role],
|
|
214
|
+
]);
|
|
215
|
+
try {
|
|
216
|
+
pyAppendText(log, `${dumpJson({ t: 'dict', v: record }, { sortKeys: true })}\n`);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
if (!(e instanceof PyOSError))
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* `str.splitlines()` — the boundaries CPython splits on, which are more than `\n`. The
|
|
225
|
+
* CRLF and bare CR forms are already folded by `pyReadText` (universal newlines, as
|
|
226
|
+
* `Path.read_text` folds them), so they do not need to be here, but nothing is lost by it.
|
|
227
|
+
*/
|
|
228
|
+
const SPLITLINES = /\r\n|[\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029]/;
|
|
229
|
+
/**
|
|
230
|
+
* `_briefed`: was a brief issued for `unitId` since its last clock-out? Read off the ledger.
|
|
231
|
+
*
|
|
232
|
+
* Walk the ledger in order, tracking only lines that name this unit: a brief line sets the
|
|
233
|
+
* flag, an accounting line (any line that is not a brief event) clears it. The answer is the
|
|
234
|
+
* flag at the end — so a clock-out CONSUMES the brief: brief, blocked, re-run without a
|
|
235
|
+
* clock_in reads `false`; brief, blocked, clock_in, done reads `true`. Detectable false
|
|
236
|
+
* negatives (a retried clock-out's duplicate line) beat undetectable false positives (an
|
|
237
|
+
* inline re-run marked briefed because a brief was issued once, ever).
|
|
238
|
+
*
|
|
239
|
+
* Reads the ledger the way a reader must: a missing or unreadable log is `false`, a line
|
|
240
|
+
* that is not JSON or not an object is skipped, never fatal. `record.get("unit") != unit_id`
|
|
241
|
+
* is a Python `!=`, so a `unit` that is not a string never equals the id; and the read is
|
|
242
|
+
* `except OSError` — a ledger that does not DECODE as UTF-8 raises out of Python's
|
|
243
|
+
* `clock_out` (`UnicodeDecodeError` is a `ValueError`), so `PyUnicodeDecodeError` is
|
|
244
|
+
* rethrown here for the same reason `recordBrief` rethrows: the port does not swallow what
|
|
245
|
+
* the reference does not.
|
|
246
|
+
*/
|
|
247
|
+
function briefed(log, unitId) {
|
|
248
|
+
let content;
|
|
249
|
+
try {
|
|
250
|
+
content = pyReadText(log);
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
if (!(e instanceof PyOSError))
|
|
254
|
+
throw e;
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
let flag = false;
|
|
258
|
+
for (const line of content.split(SPLITLINES)) {
|
|
259
|
+
let record;
|
|
260
|
+
try {
|
|
261
|
+
record = parseJson(line);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
if (!(e instanceof PyJSONDecodeError))
|
|
265
|
+
throw e;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (record.t !== 'dict')
|
|
269
|
+
continue;
|
|
270
|
+
const unit = record.v.get('unit');
|
|
271
|
+
if (unit === undefined || unit.t !== 'str' || unit.v !== unitId)
|
|
272
|
+
continue;
|
|
273
|
+
const event = record.v.get('event');
|
|
274
|
+
flag = event !== undefined && event.t === 'str' && event.v === BRIEF_EVENT;
|
|
275
|
+
}
|
|
276
|
+
return flag;
|
|
277
|
+
}
|
|
164
278
|
// ----------------------------------------------------------------------------- clock_in
|
|
165
279
|
/**
|
|
166
280
|
* Validate the checkpoint and return the cursor unit's brief, or a refusal.
|
|
@@ -175,7 +289,7 @@ export function timestamp(now) {
|
|
|
175
289
|
* all-terminal test before the cursor lookup — so an empty plan reports success rather than
|
|
176
290
|
* escalating on its dangling cursor, because `all([])` is true.
|
|
177
291
|
*/
|
|
178
|
-
export function clockIn(checkpoint) {
|
|
292
|
+
export function clockIn(checkpoint, options = {}) {
|
|
179
293
|
const { document, refusal } = readValid(pyJoin(checkpoint));
|
|
180
294
|
if (refusal !== null)
|
|
181
295
|
return refusal;
|
|
@@ -204,6 +318,10 @@ export function clockIn(checkpoint) {
|
|
|
204
318
|
['reason', str(`cursor ${cursor} names no unit`)],
|
|
205
319
|
]);
|
|
206
320
|
}
|
|
321
|
+
// F6: the brief is issued, so say so in the ledger — best effort, and only on this branch:
|
|
322
|
+
// a refusal above issued nothing and therefore records nothing. `pyJoin` is the same
|
|
323
|
+
// normalisation `clockOut` applies before it builds ITS log path, so the two land in one file.
|
|
324
|
+
recordBrief(logPath(pyJoin(checkpoint)), cursor, field(unit, 'role'), options.now ?? Date.now() / 1000);
|
|
207
325
|
return dict([
|
|
208
326
|
['result', str('brief')],
|
|
209
327
|
['unit', unit],
|
|
@@ -305,6 +423,137 @@ function modelRefusal(document, unitId, unit, accounting) {
|
|
|
305
423
|
}
|
|
306
424
|
return null;
|
|
307
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* The accounting line's shape, read off the clock_out TOOL asset — never a second copy.
|
|
428
|
+
*
|
|
429
|
+
* J50-7 put the definition on `parameters.properties.accounting` of the tool manifest,
|
|
430
|
+
* which is what the wire advertises; but the MCP call path enforces only the argument
|
|
431
|
+
* kind (`dict`), so a host is held to what it was promised only if this function reads
|
|
432
|
+
* the same file. The declared value is `anyOf: [object, null]`. The null arm is the
|
|
433
|
+
* caller's (a null line is legal and is not an audit record), so only the object arm is
|
|
434
|
+
* validated here, wrapped under the key `accounting` so the pointed error names
|
|
435
|
+
* `accounting` / `accounting/<key>` exactly the way `checkpoint invalid:` names a path —
|
|
436
|
+
* one renderer for every schema refusal this module makes, not a second one.
|
|
437
|
+
*
|
|
438
|
+
* Returns null when the pack CANNOT SUPPLY the shape (J50-9A, ported J50-9B). Reading the
|
|
439
|
+
* asset gave this module a dependency the roles gate never had, and a `BANTAMKIT_ASSETS`
|
|
440
|
+
* pack trimmed to `schemas/` — the very pack both runtimes' roles tests build — made
|
|
441
|
+
* `clockOut` die on `AssetNotFound` where the module promises a structured refusal. The
|
|
442
|
+
* caller turns null into `ACCOUNTING_SHAPE_UNREADABLE`, fail CLOSED, per J47-4: a
|
|
443
|
+
* declaration this code cannot read is not a licence. MISSING AND MALFORMED ARE ONE CASE,
|
|
444
|
+
* not two, because the property is one — the line cannot be checked, so it is not allowed —
|
|
445
|
+
* and a second sentence would either render an exception message the two runtimes spell
|
|
446
|
+
* differently, or split one property into per-shape cases that can each be missed. The
|
|
447
|
+
* shapes folded in, each MEASURED to throw out of `clockOut` before this existed: no
|
|
448
|
+
* `tools/` dir or no file (`AssetNotFound`); bytes that are not UTF-8
|
|
449
|
+
* (`PyUnicodeDecodeError`) or not JSON (`SyntaxError`); a manifest without
|
|
450
|
+
* `parameters.properties.accounting.anyOf` at each step of that path (`TypeError`, five
|
|
451
|
+
* ways); an `anyOf` with no object arm (a hand-thrown `Error`); and an object arm the
|
|
452
|
+
* validator refuses as a schema (`PyJsonSchemaUnsupported`, checked here so
|
|
453
|
+
* `accountingRefusal` only ever validates a schema).
|
|
454
|
+
*/
|
|
455
|
+
function accountingSchema() {
|
|
456
|
+
let manifest;
|
|
457
|
+
try {
|
|
458
|
+
manifest = loadToolAsset(TOOL_ASSET);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
// `except (AssetNotFound, OSError, ValueError)`: no `tools/` dir or no file; a file the
|
|
462
|
+
// OS will not hand over; bytes that are not UTF-8 (`UnicodeDecodeError`, a `ValueError`
|
|
463
|
+
// there and `PyUnicodeDecodeError` here); text that is not JSON (`JSONDecodeError` there,
|
|
464
|
+
// `SyntaxError` out of `JSON.parse` here). Anything else is a defect and still escapes.
|
|
465
|
+
if (e instanceof AssetNotFound ||
|
|
466
|
+
e instanceof PyOSError ||
|
|
467
|
+
e instanceof PyUnicodeDecodeError ||
|
|
468
|
+
e instanceof SyntaxError) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
throw e;
|
|
472
|
+
}
|
|
473
|
+
// `isinstance(declared, dict) and key in declared`, at each step of the path. A JSON
|
|
474
|
+
// array is not a dict, and neither is `null` — `typeof null === 'object'` is not a policy.
|
|
475
|
+
let declared = manifest;
|
|
476
|
+
for (const key of ['parameters', 'properties', 'accounting', 'anyOf']) {
|
|
477
|
+
if (!isDict(declared) || !(key in declared))
|
|
478
|
+
return null;
|
|
479
|
+
declared = declared[key];
|
|
480
|
+
}
|
|
481
|
+
if (!Array.isArray(declared))
|
|
482
|
+
return null;
|
|
483
|
+
// `next((a for a in declared if isinstance(a, dict) and a.get("type") == "object"), None)`
|
|
484
|
+
const arm = declared.find((a) => isDict(a) && a.type === 'object');
|
|
485
|
+
if (arm === undefined)
|
|
486
|
+
return null;
|
|
487
|
+
const schema = { type: 'object', properties: { accounting: arm } };
|
|
488
|
+
// `check_schema`, taken HERE so `accountingRefusal` only ever validates a schema. The
|
|
489
|
+
// port's `validate` runs the same check first and would throw the same class out of
|
|
490
|
+
// `schemaError`; catching it there would make one property two sites.
|
|
491
|
+
try {
|
|
492
|
+
checkSchema(fromJs(schema));
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
if (e instanceof PyJsonSchemaUnsupported)
|
|
496
|
+
return null;
|
|
497
|
+
throw e;
|
|
498
|
+
}
|
|
499
|
+
return schema;
|
|
500
|
+
}
|
|
501
|
+
/** `isinstance(x, dict)` for a value that came out of `JSON.parse`. */
|
|
502
|
+
function isDict(value) {
|
|
503
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* F5 (job50): an accounting line that does not fit its declared shape is refused.
|
|
507
|
+
*
|
|
508
|
+
* Returns the refusal sentence, or null when the clock-out may proceed. The sentence is
|
|
509
|
+
* a fixed frame around the SAME `schemaError` rendering the checkpoint refusals use
|
|
510
|
+
* (`checkpoint invalid: …`, `refused to write: …`), over the same `{"accounting": …}`
|
|
511
|
+
* wrapper the reference builds, so the differential compares like with like. The frame
|
|
512
|
+
* is J50-8's, read out of `_accounting_refusal` and reproduced to the character:
|
|
513
|
+
* `unit {unit_id} in role {role} reported an accounting line the schema refuses: {problem}`.
|
|
514
|
+
*
|
|
515
|
+
* Checked BEFORE anything is written: no accounting line, no cursor advance, the
|
|
516
|
+
* checkpoint byte-unchanged. Same posture as `modelRefusal`, same place in `clockOut`.
|
|
517
|
+
*
|
|
518
|
+
* THREE RULINGS, decided in Python and binding here:
|
|
519
|
+
*
|
|
520
|
+
* * ORDER. The roles gate runs first and keeps its sentence; this check runs second. A
|
|
521
|
+
* non-string `model` under a role `job.roles` names gets the ROLES sentence (`reported
|
|
522
|
+
* model 5, which job.roles.implementer does not allow…`); the schema's `model: string`
|
|
523
|
+
* fires only when the roles gate is silent (no map, or a role the map omits).
|
|
524
|
+
* * NULL STAYS LEGAL. `accounting: null` is not validated — the container is not
|
|
525
|
+
* required. In Node that is three spellings of nothing: the JS `null`, an omitted
|
|
526
|
+
* argument (`undefined`), and the tagged `{t: 'null'}` the MCP server hands over.
|
|
527
|
+
* * ODD KEYS PASS. `additionalProperties: true` — a key the schema does not name is
|
|
528
|
+
* written verbatim. Only the NAMED keys have a type, and only `tokens` and
|
|
529
|
+
* `duration_ms` are required.
|
|
530
|
+
* * NO SHAPE, NO LINE (J50-9A). When the pack cannot supply the shape the line is
|
|
531
|
+
* refused with `ACCOUNTING_SHAPE_UNREADABLE`, in this same place, writing nothing —
|
|
532
|
+
* not skipped. Skipping is the fail-open this file has already had once (job47's
|
|
533
|
+
* `modelRefusal` returning unconstrained on an unreadable declaration). A null line
|
|
534
|
+
* is still not validated, so a pack with no `tools/` can clock out a unit that
|
|
535
|
+
* reports no accounting.
|
|
536
|
+
*
|
|
537
|
+
* One measured fact reproduced rather than reasoned about: under 2020-12 semantics a float
|
|
538
|
+
* that is a whole number (`1234.0`) IS an `integer`; `12.5`, `"1234"`, `true` and `-1` are
|
|
539
|
+
* not. `pyjsonschema` already carries that rule (`instance.t === 'float' &&
|
|
540
|
+
* Number.isInteger(instance.v)`), and the float only survives to reach it on the
|
|
541
|
+
* parsed-from-bytes route — a JS-object `1234.0` is `1234` before this code sees it.
|
|
542
|
+
*/
|
|
543
|
+
function accountingRefusal(unitId, unit, accounting) {
|
|
544
|
+
if (accounting === null || accounting === undefined)
|
|
545
|
+
return null;
|
|
546
|
+
const line = asPyValue(accounting);
|
|
547
|
+
if (line.t === 'null')
|
|
548
|
+
return null;
|
|
549
|
+
const schema = accountingSchema();
|
|
550
|
+
if (schema === null)
|
|
551
|
+
return ACCOUNTING_SHAPE_UNREADABLE;
|
|
552
|
+
const problem = schemaError(dumpJson(dict([['accounting', line]])), schema);
|
|
553
|
+
if (problem === null)
|
|
554
|
+
return null;
|
|
555
|
+
return `unit ${unitId} in role ${text(field(unit, 'role'))} reported an accounting line the schema refuses: ${problem}`;
|
|
556
|
+
}
|
|
308
557
|
/**
|
|
309
558
|
* Apply the cursor unit's result, validate the WHOLE mutated document, write atomically.
|
|
310
559
|
*
|
|
@@ -314,7 +563,10 @@ function modelRefusal(document, unitId, unit, accounting) {
|
|
|
314
563
|
* any mutation and before the accounting line. Extended 2026-09-11 (job47): so is a
|
|
315
564
|
* `job.roles` value for that role that is not a list of model identifiers — an unreadable
|
|
316
565
|
* declaration allows no model, and it is refused in the same place, by the same structured
|
|
317
|
-
* return, writing nothing.
|
|
566
|
+
* return, writing nothing. Extended 2026-09-13 (job50/F5): and so is an accounting line
|
|
567
|
+
* that does not fit the shape the `shiftwork_clock_out` asset declares — checked after the
|
|
568
|
+
* roles gate, before any mutation, same structured return, nothing written; a null
|
|
569
|
+
* accounting is not validated and stays legal. Mutations: set the unit's status, advance
|
|
318
570
|
* `plan.cursor` to the first non-terminal unit in PLAN order (`depends_on` is ignored),
|
|
319
571
|
* shallow-merge `handoffPatch` into `handoff`, push `historyEntry` onto the 5-entry ring.
|
|
320
572
|
*
|
|
@@ -344,6 +596,9 @@ export function clockOut(checkpoint, unitId, status, handoffPatch, historyEntry,
|
|
|
344
596
|
const wrongModel = modelRefusal(doc, unitId, unit, accounting);
|
|
345
597
|
if (wrongModel !== null)
|
|
346
598
|
return errorResult(wrongModel);
|
|
599
|
+
const badLine = accountingRefusal(unitId, unit, accounting);
|
|
600
|
+
if (badLine !== null)
|
|
601
|
+
return errorResult(badLine);
|
|
347
602
|
unit.v.set('status', str(status));
|
|
348
603
|
const remaining = subList(plan, 'units').v.filter((u) => !TERMINAL_UNIT_STATUS.has(text(u.v.get('status'))));
|
|
349
604
|
plan.v.set('cursor', remaining.length > 0 ? field(remaining[0], 'id') : str(unitId));
|
|
@@ -369,11 +624,14 @@ export function clockOut(checkpoint, unitId, status, handoffPatch, historyEntry,
|
|
|
369
624
|
]);
|
|
370
625
|
for (const [key, value] of asPatch(accounting))
|
|
371
626
|
record.set(key, value);
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
|
|
627
|
+
const log = logPath(path);
|
|
628
|
+
// F6: `briefed` is MEASURED off the ledger, AFTER the orchestrator's keys are merged, so the
|
|
629
|
+
// runtime's answer wins over a self-reported one — the order is load-bearing. It is written
|
|
630
|
+
// on every line, `accounting: null` included: the base shape (ts/unit/role/status) is the
|
|
631
|
+
// runtime's, and so is this field. Never a refusal.
|
|
632
|
+
record.set('briefed', { t: 'bool', v: briefed(log, unitId) });
|
|
375
633
|
try {
|
|
376
|
-
pyAppendText(
|
|
634
|
+
pyAppendText(log, `${dumpJson({ t: 'dict', v: record }, { sortKeys: true })}\n`);
|
|
377
635
|
}
|
|
378
636
|
catch (e) {
|
|
379
637
|
if (!(e instanceof PyOSError))
|
|
@@ -403,7 +661,7 @@ export function clockOut(checkpoint, unitId, status, handoffPatch, historyEntry,
|
|
|
403
661
|
['unit', str(unitId)],
|
|
404
662
|
['status', str(status)],
|
|
405
663
|
['cursor', field(plan, 'cursor')],
|
|
406
|
-
['log', str(
|
|
664
|
+
['log', str(log)],
|
|
407
665
|
]);
|
|
408
666
|
}
|
|
409
667
|
// ------------------------------------------------------------------------------- status
|