@skill-harness/cli 0.25.0 → 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.
- package/dist/decision.js +48 -15
- package/package.json +4 -4
package/dist/decision.js
CHANGED
|
@@ -253,10 +253,20 @@ def ordinary(path):
|
|
|
253
253
|
def closed(value, fields, name):
|
|
254
254
|
require(isinstance(value, dict) and set(value) == set(fields), name + ' has unsupported or missing fields')
|
|
255
255
|
|
|
256
|
+
def jsonl_rows(path):
|
|
257
|
+
# JSONL records end only at LF. Unicode NEL/LS/PS are legal JSON string data.
|
|
258
|
+
lines = ordinary(path).decode('utf-8').split('\n')
|
|
259
|
+
if lines[-1] == '': lines.pop()
|
|
260
|
+
return [json.loads(line) for line in lines]
|
|
261
|
+
|
|
256
262
|
def export_data(root):
|
|
257
263
|
root = pathlib.Path(root).absolute()
|
|
258
264
|
raw = ordinary(root / 'export-manifest.json'); m = json.loads(raw)
|
|
259
|
-
require(m.get('schema') ==
|
|
265
|
+
require(m.get('schema') == 2 and m.get('kind') == 'decision-learning-export', 'Unsupported export schema; re-export with the current skill-harness')
|
|
266
|
+
normalization = m['inputNormalization']
|
|
267
|
+
closed(normalization, ['algorithm','runtime','nodeVersion','unicodeVersion'], 'Input normalization producer')
|
|
268
|
+
require(normalization['algorithm'] == 'ecmascript-nfkc-whitespace-trim-lower-sha256-v1' and normalization['runtime'] == 'node', 'Unsupported input normalization producer')
|
|
269
|
+
require(all(isinstance(normalization[key], str) and re.fullmatch(r'[0-9]+(?:\.[0-9]+){1,2}', normalization[key]) for key in ['nodeVersion','unicodeVersion']), 'Invalid input normalization producer version')
|
|
260
270
|
require(m.get('trainingExecuted') is False and m.get('providerPredictionsIncluded') is False, 'Unsupported source provenance')
|
|
261
271
|
require(set(m['files']) == {'train.jsonl','validation.jsonl','test.jsonl','train-lora.py','requirements-training.txt','training-config.example.json','TRAINING.md'}, 'Unexpected export files')
|
|
262
272
|
for name, ref in m['files'].items():
|
|
@@ -266,10 +276,10 @@ def export_data(root):
|
|
|
266
276
|
require(digest(ordinary(pathlib.Path(__file__).absolute())) == m['files']['train-lora.py']['sha256'], 'Run the exact exported training script')
|
|
267
277
|
groups, seen_inputs, seen_cases = {}, {}, set()
|
|
268
278
|
for split in ['train','validation','test']:
|
|
269
|
-
rows =
|
|
279
|
+
rows = jsonl_rows(root / (split+'.jsonl'))
|
|
270
280
|
require(len(rows) == m['counts'][split], 'Split count mismatch')
|
|
271
281
|
for row in rows:
|
|
272
|
-
closed(row, ['caseId','caseHash','taskGroup','lineageGroup','sessionId','fixtureOnly','input','question','answer','label','source'], 'Dataset row')
|
|
282
|
+
closed(row, ['caseId','caseHash','taskGroup','lineageGroup','sessionId','fixtureOnly','input','inputIdentity','question','answer','label','source'], 'Dataset row')
|
|
273
283
|
require(row['caseId'] not in seen_cases and type(row['answer']) is bool, 'Duplicate case or invalid label')
|
|
274
284
|
seen_cases.add(row['caseId'])
|
|
275
285
|
require(row['label']['caseId'] == row['caseId'] and row['label']['caseHash'] == row['caseHash'] and row['label']['value'] == row['answer'] and row['label']['kind'] in ['human','test'] and row['label']['independent'] is True, 'Label identity mismatch')
|
|
@@ -278,7 +288,15 @@ def export_data(root):
|
|
|
278
288
|
identity = key + ':' + row[key]
|
|
279
289
|
require(identity not in groups or groups[identity] == split, 'Related cases cross splits')
|
|
280
290
|
groups[identity] = split
|
|
281
|
-
|
|
291
|
+
identity = row['inputIdentity']
|
|
292
|
+
closed(identity, ['algorithm','inputSha256','normalizedSha256'], 'Input identity receipt')
|
|
293
|
+
require(identity['algorithm'] == normalization['algorithm'], 'Input identity algorithm mismatch')
|
|
294
|
+
require(all(isinstance(identity[key], str) and re.fullmatch(r'[0-9a-f]{64}', identity[key]) for key in ['inputSha256','normalizedSha256']), 'Invalid input identity digest')
|
|
295
|
+
require(isinstance(row['input'], str) and digest(row['input'].encode('utf-8')) == identity['inputSha256'], 'Input identity byte mismatch')
|
|
296
|
+
# This is producer evidence bound by the exact row/file/manifest hashes.
|
|
297
|
+
# Recomputing with Python Unicode tables can disagree with the Node producer.
|
|
298
|
+
# The receipt is not an independent proof that normalization was correct.
|
|
299
|
+
normalized = identity['normalizedSha256']
|
|
282
300
|
require(normalized not in seen_inputs or seen_inputs[normalized] == split, 'Duplicate input crosses splits')
|
|
283
301
|
seen_inputs[normalized] = split
|
|
284
302
|
if m.get('trainingEligible') is True: require(row['fixtureOnly'] is False and row['sessionId'] is not None, 'Fixture/anonymous data cannot be training eligible')
|
|
@@ -353,8 +371,7 @@ def main():
|
|
|
353
371
|
model = get_peft_model(model, LoraConfig(task_type=TaskType.CAUSAL_LM, r=t['loraRank'], lora_alpha=t['loraAlpha'], target_modules=t['targetModules'], lora_dropout=0.0, bias='none'))
|
|
354
372
|
def rows(split):
|
|
355
373
|
data = []
|
|
356
|
-
for
|
|
357
|
-
row = json.loads(line)
|
|
374
|
+
for row in jsonl_rows(root / (split + '.jsonl')):
|
|
358
375
|
require(row['fixtureOnly'] is False and type(row['answer']) is bool and row['label']['kind'] in ['human','test'] and row['label']['independent'] is True, 'Only independently labeled reviewed real data may train')
|
|
359
376
|
prompt = 'Question: ' + row['question'] + '\nEvidence:\n' + row['input'] + '\nAnswer (true or false): '
|
|
360
377
|
prefix = tokenizer.encode(prompt, add_special_tokens=True)
|
|
@@ -384,6 +401,8 @@ var TRAINING_DOC = `# Local LoRA workflow
|
|
|
384
401
|
|
|
385
402
|
This export contains independent labels, never JEV predictions. A fixture demonstration is not training eligible. No training or installation has been performed by exporting these files.
|
|
386
403
|
|
|
404
|
+
Export schema 2 records the producer's normalized-input SHA-256 receipt in every row, the exact input UTF-8 SHA-256, and the Node/Unicode versions and normalization algorithm in the manifest. One JavaScript normalizer validates the split and writes the receipt. Python checks receipt shape, raw-input binding, duplicate identities and exact file hashes; it does not reinterpret those identities with its own Unicode tables. These are trusted producer receipts, not independent proofs of correct normalization. Separate training approval still pins the exact export manifest. Invalid Unicode scalar input is refused by the producer. Re-export older schema-1 packets with the current CLI; do not modify their bound script, rows or manifest in place.
|
|
405
|
+
|
|
387
406
|
1. Check the intact export offline: python3 train-lora.py --export /absolute/export --check-export-only.
|
|
388
407
|
2. For reviewed real data only, select a base model, immutable full revision, license and an existing canonical local weight directory. Pin every file by SHA-256 in a separate copy of training-config.example.json. Select architecture-specific LoRA target modules. Record an explicit eligibility review bound to the exact export-manifest.json digest. Storage consent alone is insufficient.
|
|
389
408
|
3. Use an isolated Python 3.12 environment with the exact requirements-training.txt versions. Resolve dependencies separately, retain the complete resolved environment/wheel hashes, and assess the host memory requirements. The template pins direct versions; it is not a platform-complete dependency lock or proof of hardware support.
|
|
@@ -459,6 +478,11 @@ function consentFor(consents, sessionId) {
|
|
|
459
478
|
check(c.decision === "granted", "session storage declined; advice remains allowed");
|
|
460
479
|
return c;
|
|
461
480
|
}
|
|
481
|
+
var INPUT_NORMALIZATION_ALGORITHM = "ecmascript-nfkc-whitespace-trim-lower-sha256-v1";
|
|
482
|
+
function inputIdentity(input) {
|
|
483
|
+
check(!/[\uD800-\uDFFF]/u.test(input), "learning input must contain valid Unicode scalar values");
|
|
484
|
+
return { algorithm: INPUT_NORMALIZATION_ALGORITHM, inputSha256: learningDigest(input), normalizedSha256: learningDigest(input.normalize("NFKC").replace(/\s+/gu, " ").trim().toLowerCase()) };
|
|
485
|
+
}
|
|
462
486
|
var ENTRY_KEYS = ["caseId", "caseHash", "taskGroup", "lineageGroup", "split", "sessionId", "fixtureOnly", "decisionTimeReviewed", "redactionReviewed", "rights", "exportApproved", "trainingApproved", "reviewer"];
|
|
463
487
|
function parseEntry(c, e) {
|
|
464
488
|
keys(e, ENTRY_KEYS, "experiment entry");
|
|
@@ -490,7 +514,7 @@ function parseExperiment(cases, manifest) {
|
|
|
490
514
|
bind(groups, "task:" + e.taskGroup, e.split, "task group");
|
|
491
515
|
bind(groups, "lineage:" + e.lineageGroup, e.split, "lineage group");
|
|
492
516
|
if (e.sessionId !== null) bind(groups, "session:" + e.sessionId, e.split, "session");
|
|
493
|
-
bind(duplicates,
|
|
517
|
+
bind(duplicates, inputIdentity(cases[i].input).normalizedSha256, e.split, "duplicate input");
|
|
494
518
|
bind(duplicates, "source:" + cases[i].source.sha256 + ":" + cases[i].source.recordId, e.split, "source record");
|
|
495
519
|
});
|
|
496
520
|
return { ...structuredClone(manifest), entries };
|
|
@@ -660,7 +684,7 @@ async function prepareExport({ cases, labels, manifest, consents, labelEvidence,
|
|
|
660
684
|
excluded.push({ caseId: c.id, reasons });
|
|
661
685
|
continue;
|
|
662
686
|
}
|
|
663
|
-
const row = { caseId: c.id, caseHash: c.hash, taskGroup: e.taskGroup, lineageGroup: e.lineageGroup, sessionId: e.sessionId, fixtureOnly: e.fixtureOnly, input: c.input, question: c.question, answer: label.label.value, label: { ...label.label }, source: { ...c.source } };
|
|
687
|
+
const row = { caseId: c.id, caseHash: c.hash, taskGroup: e.taskGroup, lineageGroup: e.lineageGroup, sessionId: e.sessionId, fixtureOnly: e.fixtureOnly, input: c.input, inputIdentity: inputIdentity(c.input), question: c.question, answer: label.label.value, label: { ...label.label }, source: { ...c.source } };
|
|
664
688
|
splitRows[e.split].push(row);
|
|
665
689
|
included.push(e);
|
|
666
690
|
}
|
|
@@ -668,16 +692,16 @@ async function prepareExport({ cases, labels, manifest, consents, labelEvidence,
|
|
|
668
692
|
const trainingEligible = mode === "reviewed-data" && excluded.length === 0 && Object.values(splitRows).every((rows) => rows.length > 0) && included.every((e) => !e.fixtureOnly && e.trainingApproved && e.rights === "local-training");
|
|
669
693
|
const files = { ...trainingAssets() };
|
|
670
694
|
for (const [split, rows] of Object.entries(splitRows)) files[`${split}.jsonl`] = rows.map((r) => JSON.stringify(r) + "\n").join("");
|
|
671
|
-
const metadata2 = { schema:
|
|
695
|
+
const metadata2 = { schema: 2, kind: "decision-learning-export", inputNormalization: { algorithm: INPUT_NORMALIZATION_ALGORITHM, runtime: "node", nodeVersion: process.versions.node, unicodeVersion: process.versions.unicode }, mode, experimentId: manifest.id, experimentHash: learningDigest(manifest), trainingEligible, trainingExecuted: false, providerPredictionsIncluded: false, counts: Object.fromEntries(Object.entries(splitRows).map(([s, r]) => [s, r.length])), excluded, review: { frozenAt: manifest.frozenAt, eligibilityRecords: included, independentLabelReceipts: reviewed.reviewed.map((r) => ({ ...r.reference, recordedAt: r.recordedAt })), sessionConsents: [...new Set(included.filter((e) => e.sessionId !== null).map((e) => e.sessionId))].map((sessionId) => {
|
|
672
696
|
const c = consentFor(consents, sessionId);
|
|
673
697
|
return { sessionId, sha256: learningDigest(c), decision: c.decision, interactionId: c.interactionId, recordedAt: c.recordedAt };
|
|
674
698
|
}) }, files: Object.fromEntries(Object.entries(files).map(([name, s]) => [name, { sha256: learningDigest(s), bytes: Buffer.byteLength(s) }])) };
|
|
675
699
|
files["export-manifest.json"] = JSON.stringify(metadata2, null, 2) + "\n";
|
|
676
|
-
return { schema:
|
|
700
|
+
return { schema: 2, kind: "prepared-decision-learning-export", metadata: metadata2, files };
|
|
677
701
|
}
|
|
678
702
|
async function writePreparedExport({ prepared, directory }) {
|
|
679
703
|
keys(prepared, ["schema", "kind", "metadata", "files"], "prepared export");
|
|
680
|
-
check(prepared.schema ===
|
|
704
|
+
check(prepared.schema === 2 && prepared.metadata?.schema === 2 && prepared.kind === "prepared-decision-learning-export", "unsupported prepared export");
|
|
681
705
|
text(directory, "output directory", 4096);
|
|
682
706
|
check(isAbsolute(directory), "output directory must be absolute");
|
|
683
707
|
const allowed = ["train.jsonl", "validation.jsonl", "test.jsonl", "export-manifest.json", "train-lora.py", "training-config.example.json", "requirements-training.txt", "TRAINING.md"];
|
|
@@ -1529,8 +1553,9 @@ function normalize2(payload) {
|
|
|
1529
1553
|
}
|
|
1530
1554
|
function deadline(signal) {
|
|
1531
1555
|
return new Promise((_, reject) => {
|
|
1532
|
-
|
|
1533
|
-
signal.
|
|
1556
|
+
const fail = () => reject(signal.reason instanceof Error ? signal.reason : new Error("provider request timed out"));
|
|
1557
|
+
if (signal.aborted) return fail();
|
|
1558
|
+
signal.addEventListener("abort", fail, { once: true });
|
|
1534
1559
|
});
|
|
1535
1560
|
}
|
|
1536
1561
|
async function readBounded(response, timedOut) {
|
|
@@ -1566,7 +1591,7 @@ async function readBounded(response, timedOut) {
|
|
|
1566
1591
|
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1567
1592
|
}
|
|
1568
1593
|
function failure(startedAt, error) {
|
|
1569
|
-
const publicErrors = ["invalid provider response", "provider request timed out", "provider response exceeded limit"];
|
|
1594
|
+
const publicErrors = ["invalid provider response", "provider request timed out", "provider response exceeded limit", "provider request cancelled"];
|
|
1570
1595
|
const message = error instanceof Error && publicErrors.includes(error.message) ? error.message : "provider request failed";
|
|
1571
1596
|
return {
|
|
1572
1597
|
status: "error",
|
|
@@ -1581,6 +1606,7 @@ async function callProvider(provider, model, example, options = {}) {
|
|
|
1581
1606
|
const startedAt = performance.now();
|
|
1582
1607
|
let controller;
|
|
1583
1608
|
let timer;
|
|
1609
|
+
let cancel;
|
|
1584
1610
|
try {
|
|
1585
1611
|
const request = makeRequest(provider, model, example);
|
|
1586
1612
|
requiredString(options.apiKey, "apiKey", 16 * 1024);
|
|
@@ -1591,8 +1617,14 @@ async function callProvider(provider, model, example, options = {}) {
|
|
|
1591
1617
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5) {
|
|
1592
1618
|
throw new TypeError("invalid timeout");
|
|
1593
1619
|
}
|
|
1620
|
+
if (options.signal !== void 0 && !(options.signal instanceof AbortSignal)) {
|
|
1621
|
+
throw new TypeError("signal must be an AbortSignal");
|
|
1622
|
+
}
|
|
1623
|
+
if (options.signal?.aborted) throw new Error("provider request cancelled");
|
|
1594
1624
|
controller = new AbortController();
|
|
1595
|
-
|
|
1625
|
+
cancel = () => controller.abort(new Error("provider request cancelled"));
|
|
1626
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
1627
|
+
timer = setTimeout(() => controller.abort(new Error("provider request timed out")), timeoutMs);
|
|
1596
1628
|
const timedOut = deadline(controller.signal);
|
|
1597
1629
|
const response = await Promise.race([
|
|
1598
1630
|
(options.fetchImpl ?? fetch)(request.url, {
|
|
@@ -1617,6 +1649,7 @@ async function callProvider(provider, model, example, options = {}) {
|
|
|
1617
1649
|
return failure(startedAt, error);
|
|
1618
1650
|
} finally {
|
|
1619
1651
|
if (timer !== void 0) clearTimeout(timer);
|
|
1652
|
+
if (cancel) options.signal?.removeEventListener("abort", cancel);
|
|
1620
1653
|
controller?.abort();
|
|
1621
1654
|
}
|
|
1622
1655
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "skill-harness CLI
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "skill-harness CLI \u2014 run, grade, review, and lint agent-skill scenarios on the pi harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/cli.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepack": "node ../../scripts/release-pack.mjs --guard"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@skill-harness/core": "0.
|
|
48
|
-
"@skill-harness/adapters": "0.
|
|
47
|
+
"@skill-harness/core": "0.26.0",
|
|
48
|
+
"@skill-harness/adapters": "0.26.0"
|
|
49
49
|
}
|
|
50
50
|
}
|