harness-everything 0.29.0 → 0.30.1

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 (108) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +1 -0
  4. package/CHANGELOG.md +42 -0
  5. package/PRIVACY.md +9 -0
  6. package/benchmarks/fixtures/system-one-intent-holdout-draft.json +2228 -0
  7. package/benchmarks/fixtures/system-one-intent-holdout.json +16509 -0
  8. package/benchmarks/fixtures/system-one-intent-reviews.json +2723 -0
  9. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/README.md +64 -0
  10. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/report.json +15177 -0
  11. package/benchmarks/results/system-one/labeler-holdout-check-intent-2026-09-24/report.json +2976 -0
  12. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-005-steps.json +6125 -0
  13. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-haiku-sonnet-escalation.json +6344 -0
  14. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-scaled-margin-escalation.json +6206 -0
  15. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report.json +6103 -0
  16. package/benchmarks/results/system-one/laya-ft-2026-09-25/README.md +83 -0
  17. package/benchmarks/results/system-one/laya-ft-2026-09-25/report.json +71 -0
  18. package/benchmarks/results/system-one/laya-ft-2026-09-25/shadow-report.json +44 -0
  19. package/benchmarks/results/system-one/laya-zs-2026-09-25/README.md +68 -0
  20. package/benchmarks/results/system-one/laya-zs-2026-09-25/report.json +173 -0
  21. package/ci/fable-mode-test.js +16 -0
  22. package/ci/mechanism-34-observations.test.js +550 -0
  23. package/ci/mechanism-system-one-collect.test.js +16 -0
  24. package/ci/mechanism-system-one-intent.test.js +406 -0
  25. package/ci/mechanism-system-one-laya.test.js +45 -0
  26. package/ci/mechanism-system-one-relevance.test.js +112 -0
  27. package/ci/mechanism-system-one-resident.test.js +16 -0
  28. package/ci/system-one-laya-calibrate-test.py +65 -0
  29. package/ci/system-one-laya-export-test.py +155 -0
  30. package/ci/system-one-laya-label-test.py +201 -0
  31. package/ci/system-one-laya-perintent-test.py +60 -0
  32. package/ci/system-one-laya-provider-test.py +115 -0
  33. package/ci/system-one-laya-train-test.py +93 -0
  34. package/docs/routing.md +25 -0
  35. package/docs/system-one-intent-gates.md +102 -0
  36. package/docs/system-one-intent.md +173 -0
  37. package/docs/system-one-laya-training.md +38 -0
  38. package/docs/system-one-observations.md +206 -0
  39. package/docs/system-one-routing.md +25 -10
  40. package/docs/system-one-suggestion-gates.md +292 -0
  41. package/docs/system-one-training.md +56 -6
  42. package/docs/workflows/fable-mode.md +7 -0
  43. package/fable-mode/SKILL.md +1 -1
  44. package/fable-mode/agents/fable-orchestrator.md +4 -4
  45. package/fable-mode/agents/fable-worker-haiku.md +4 -4
  46. package/fable-mode/agents/fable-worker-sonnet.md +4 -4
  47. package/fable-mode/execution-guardrails/SKILL.md +5 -3
  48. package/fable-mode/execution-guardrails/references/guardrail-rules.md +12 -8
  49. package/fable-mode/fable-haiku/SKILL.md +1 -1
  50. package/fable-mode/fable-opus/SKILL.md +1 -1
  51. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  52. package/harness-everything/SKILL.md +1 -1
  53. package/harness-everything/scripts/kernel-router.js +10 -0
  54. package/harness-everything/scripts/system-one/catalogs.js +35 -0
  55. package/harness-everything/scripts/system-one/contract.js +45 -1
  56. package/harness-everything/scripts/system-one/evaluate.js +115 -19
  57. package/harness-everything/scripts/system-one/intent.js +22 -0
  58. package/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  59. package/harness-everything/scripts/system-one/provider.js +10 -3
  60. package/harness-everything/scripts/system-one/resident.js +8 -3
  61. package/harness-everything/scripts/system-one/router.js +1 -1
  62. package/hooks/hooks.json +40 -0
  63. package/hooks/scripts/codex-user-prompt.js +39 -0
  64. package/hooks/scripts/lib/observations.js +581 -0
  65. package/hooks/scripts/observation-hook.js +25 -0
  66. package/opencode-plugin/plugin.json +1 -1
  67. package/package.json +1 -1
  68. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  69. package/plugins/harness-everything/hooks/hooks.json +35 -2
  70. package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +39 -0
  71. package/plugins/harness-everything/hooks/scripts/lib/observations.js +581 -0
  72. package/plugins/harness-everything/hooks/scripts/observation-hook.js +25 -0
  73. package/plugins/harness-everything/plugin.json +1 -1
  74. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  75. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +4 -4
  76. package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-haiku.md +4 -4
  77. package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-sonnet.md +4 -4
  78. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +5 -3
  79. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/references/guardrail-rules.md +12 -8
  80. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  81. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  82. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  83. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  84. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +10 -0
  85. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/catalogs.js +35 -0
  86. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/contract.js +45 -1
  87. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +115 -19
  88. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/intent.js +22 -0
  89. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  90. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +10 -3
  91. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/resident.js +8 -3
  92. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/router.js +1 -1
  93. package/sbom.cdx.json +7 -7
  94. package/scripts/evaluate-system-one.js +17 -5
  95. package/scripts/system-one-calibrate-laya.py +165 -0
  96. package/scripts/system-one-calibrate.js +42 -13
  97. package/scripts/system-one-collect.js +7 -3
  98. package/scripts/system-one-compare-laya.py +156 -0
  99. package/scripts/system-one-corpus.js +22 -15
  100. package/scripts/system-one-eval-laya.js +112 -0
  101. package/scripts/system-one-export-laya.py +197 -0
  102. package/scripts/system-one-label-laya.py +243 -0
  103. package/scripts/system-one-label.js +163 -37
  104. package/scripts/system-one-observations-export.js +106 -0
  105. package/scripts/system-one-observations-review.js +86 -0
  106. package/scripts/system-one-perintent-laya.py +134 -0
  107. package/scripts/system-one-train-laya.py +344 -0
  108. package/scripts/system-one-train-ngram.py +66 -14
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- // Assembles the System One tier holdout from a draft plus human review decisions.
4
- // See docs/system-one-corpus.md. A decision counts only for the exact prompt text it reviewed.
3
+ // Assembles a System One holdout (tier or intent stage) from a draft plus human review decisions.
4
+ // See docs/system-one-corpus.md and docs/system-one-intent.md. A decision counts only for the exact prompt text it reviewed.
5
5
  const fs = require('node:fs');
6
6
  const { createHash } = require('node:crypto');
7
7
  const { createRequest } = require('../harness-everything/scripts/system-one/contract');
8
- const { TIER_OPTIONS } = require('../harness-everything/scripts/system-one/router');
8
+ const { CATALOGS, MULTI, goldLabels, validSecondary } = require('../harness-everything/scripts/system-one/catalogs');
9
9
  const { validateCorpus } = require('../harness-everything/scripts/system-one/evaluate');
10
10
 
11
11
  const SOURCES = ['synthetic:authored', 'derived:local-history'];
12
12
  const LANGUAGES = ['en', 'zh-TW'];
13
- const GOLD = ['tier1', 'tier2', 'tier3', null];
14
13
  const REVIEWER = 'human:repository-owner';
15
14
  // Drafts are committed publicly: no email addresses or user home paths.
16
15
  const PRIVATE = [/[\w.+-]+@[\w-]+\.[\w.-]+/, /[A-Za-z]:\\Users\\/i, /\/home\/[^/\s]+/, /\/Users\/[^/\s]+/];
@@ -20,7 +19,8 @@ const nonempty = v => typeof v === 'string' && v.trim().length > 0;
20
19
  const fail = code => { throw new TypeError(code); };
21
20
  const promptHash = text => createHash('sha256').update(text, 'utf8').digest('hex');
22
21
 
23
- function validateDraft(draft) {
22
+ function validateDraft(draft, task = 'tier') {
23
+ const GOLD = goldLabels(task);
24
24
  if (!exact(draft, ['schemaVersion', 'cases']) || draft.schemaVersion !== 1 || !Array.isArray(draft.cases) || !draft.cases.length) fail('invalid-draft');
25
25
  const ids = new Set(); const prompts = new Set();
26
26
  for (const c of draft.cases) {
@@ -33,24 +33,27 @@ function validateDraft(draft) {
33
33
  return true;
34
34
  }
35
35
 
36
- function validateReviews(reviews) {
36
+ function validateReviews(reviews, task = 'tier') {
37
+ const GOLD = goldLabels(task);
37
38
  if (!exact(reviews, ['schemaVersion', 'decisions']) || reviews.schemaVersion !== 1 || !Array.isArray(reviews.decisions)) fail('invalid-reviews');
38
39
  const ids = new Set();
39
40
  for (const d of reviews.decisions) {
40
- if (!exact(d, ['id', 'promptHash', 'decision', 'gold', 'reviewer']) || !nonempty(d.id) || ids.has(d.id)
41
+ const multi = MULTI.includes(task);
42
+ if (!exact(d, ['id', 'promptHash', 'decision', 'gold', ...(multi ? ['secondary'] : []), 'reviewer']) || !nonempty(d.id) || ids.has(d.id)
41
43
  || typeof d.promptHash !== 'string' || !/^[0-9a-f]{64}$/.test(d.promptHash)
42
44
  || !['accept', 'relabel', 'reject'].includes(d.decision) || !GOLD.includes(d.gold) || d.reviewer !== REVIEWER
45
+ || !validSecondary(task, d.gold, d.secondary)
43
46
  || (d.decision === 'reject' && d.gold !== null)) fail('invalid-reviews');
44
47
  ids.add(d.id);
45
48
  }
46
49
  return true;
47
50
  }
48
51
 
49
- function build(draft, reviews) {
50
- validateDraft(draft);
52
+ function build(draft, reviews, task = 'tier') {
53
+ validateDraft(draft, task);
51
54
  const byId = new Map();
52
55
  if (reviews !== null) {
53
- validateReviews(reviews);
56
+ validateReviews(reviews, task);
54
57
  for (const d of reviews.decisions) {
55
58
  const c = draft.cases.find(item => item.id === d.id);
56
59
  if (!c) fail('unknown-review-case');
@@ -60,7 +63,8 @@ function build(draft, reviews) {
60
63
  }
61
64
  }
62
65
  const summary = { cases: 0, reviewed: 0, rejected: 0, stale: 0, unreviewed: 0,
63
- byLanguage: { en: 0, 'zh-TW': 0 }, byGold: { tier1: 0, tier2: 0, tier3: 0, unclassified: 0 }, reviewedHoldoutGate: false };
66
+ byLanguage: { en: 0, 'zh-TW': 0 }, byGold: Object.fromEntries(CATALOGS[task].map(o => [o.id, 0])),
67
+ ...(MULTI.includes(task) ? { bySecondary: {} } : {}), reviewedHoldoutGate: false };
64
68
  const cases = [];
65
69
  for (const c of draft.cases) {
66
70
  const d = byId.get(c.id);
@@ -70,10 +74,13 @@ function build(draft, reviews) {
70
74
  const reviewed = Boolean(current);
71
75
  if (reviewed) summary.reviewed += 1; else if (!d) summary.unreviewed += 1;
72
76
  const gold = reviewed ? d.gold : c.proposedGold;
77
+ // Drafts propose only the primary intent; secondary intents come from the reviewer.
78
+ const secondary = MULTI.includes(task) ? { secondary: reviewed ? [...d.secondary] : [] } : {};
73
79
  cases.push({ id: c.id, family: c.family, split: 'holdout', language: c.language, source: c.source, reviewed,
74
- request: createRequest('tier', c.prompt, TIER_OPTIONS), gold });
80
+ request: createRequest(task, c.prompt, CATALOGS[task]), gold, ...secondary });
75
81
  summary.byLanguage[c.language] += 1;
76
82
  summary.byGold[gold === null ? 'unclassified' : gold] += 1;
83
+ for (const s of secondary.secondary || []) summary.bySecondary[s] = (summary.bySecondary[s] || 0) + 1;
77
84
  }
78
85
  summary.cases = cases.length;
79
86
  const corpus = { schemaVersion: 1, cases };
@@ -85,14 +92,14 @@ function build(draft, reviews) {
85
92
 
86
93
  if (require.main === module) {
87
94
  try {
88
- const [op, draftPath, reviewsPath, outPath, extra] = process.argv.slice(2);
95
+ const [op, draftPath, reviewsPath, outPath, task = 'tier', extra] = process.argv.slice(2);
89
96
  if (op !== 'build' || !draftPath || !reviewsPath || !outPath || extra) throw new Error('usage');
90
97
  const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
91
- const { corpus, summary } = build(read(draftPath), reviewsPath === '-' ? null : read(reviewsPath));
98
+ const { corpus, summary } = build(read(draftPath), reviewsPath === '-' ? null : read(reviewsPath), task);
92
99
  fs.writeFileSync(outPath, `${JSON.stringify(corpus, null, 2)}\n`);
93
100
  console.log(JSON.stringify(summary));
94
101
  } catch (_) {
95
- console.error('Usage: system-one-corpus.js build <draft.json> <reviews.json|-> <corpus.json> (invalid draft, reviews, or path)');
102
+ console.error('Usage: system-one-corpus.js build <draft.json> <reviews.json|-> <corpus.json> [tier|intent] (invalid draft, reviews, or path)');
96
103
  process.exitCode = 1;
97
104
  }
98
105
  }
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Research evaluation driver for the fine-tuned Laya bridge (#255 Phase 3).
4
+ // It does NOT touch provider.js/resident.js (still cua-only): it starts the
5
+ // laya resident server itself, scores cases through it, and feeds the
6
+ // production decide()/evaluate() machinery. Usage:
7
+ // score <cases.json> <manifest.json> <records.json> [workdir]
8
+ // report <corpus.json> <records.json> <report.json> [--secondary-threshold T]
9
+ // cases.json: { cases: [{ id, baseline, request }] } (request = holdout-style,
10
+ // context + 13 catalog options). records.json matches evaluate() input.
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const net = require('node:net');
14
+ const { spawn } = require('node:child_process');
15
+ const { performance } = require('node:perf_hooks');
16
+ const { evaluate } = require('../harness-everything/scripts/system-one/evaluate');
17
+ const { decide } = require('../harness-everything/scripts/system-one/contract');
18
+ const { decisionPolicy } = require('../harness-everything/scripts/system-one/provider');
19
+
20
+ const ADAPTER = path.join(__dirname, '..', 'harness-everything', 'scripts', 'system-one', 'laya_adapter.py');
21
+ const CALL_TIMEOUT_MS = 30000;
22
+
23
+ function send(port, token, message) {
24
+ return new Promise(resolve => {
25
+ const socket = net.connect({ port, host: '127.0.0.1' });
26
+ const chunks = [];
27
+ const done = value => { socket.destroy(); resolve(value); };
28
+ const timer = setTimeout(() => done({ ok: false, reason: 'timeout' }), CALL_TIMEOUT_MS);
29
+ socket.on('connect', () => socket.end(`${JSON.stringify({ token, ...message })}\n`));
30
+ socket.on('data', c => chunks.push(c));
31
+ socket.on('end', () => { clearTimeout(timer);
32
+ try { done(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
33
+ catch (_) { done({ ok: false, reason: 'bad-reply' }); } });
34
+ socket.on('error', () => { clearTimeout(timer); done({ ok: false, reason: 'io' }); });
35
+ });
36
+ }
37
+
38
+ async function withServer(python, manifest, workdir, fn) {
39
+ const manifestJson = JSON.stringify(manifest);
40
+ const state = path.join(workdir, 'laya-research-state.json');
41
+ const lock = path.join(workdir, 'laya-research-state.starting');
42
+ for (const f of [state, lock]) try { fs.unlinkSync(f); } catch (_) { /* fresh */ }
43
+ const child = spawn(python, [ADAPTER, '--serve', manifestJson, state, lock], { stdio: 'ignore' });
44
+ const deadline = Date.now() + 120000;
45
+ let info = null;
46
+ while (Date.now() < deadline) {
47
+ try { info = JSON.parse(fs.readFileSync(state, 'utf8')); if (info.port && info.token) break; } catch (_) { /* starting */ }
48
+ if (child.exitCode !== null) throw new Error(`server exited during startup (code ${child.exitCode})`);
49
+ await new Promise(r => setTimeout(r, 200));
50
+ info = null;
51
+ }
52
+ if (!info) { child.kill('SIGKILL'); throw new Error('server did not become ready'); }
53
+ try {
54
+ return await fn(info);
55
+ } finally {
56
+ try { await send(info.port, info.token, { op: 'shutdown' }); } catch (_) { /* exiting */ }
57
+ child.kill('SIGKILL');
58
+ }
59
+ }
60
+
61
+ async function score(casesPath, manifestPath, recordsPath, workdir) {
62
+ const { cases } = JSON.parse(fs.readFileSync(casesPath, 'utf8'));
63
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
64
+ const policy = decisionPolicy(manifest.acceptance);
65
+ const records = await withServer(manifest.python, manifest, workdir, async info => {
66
+ const out = [];
67
+ for (const c of cases) {
68
+ const runs = [];
69
+ for (let i = 0; i < 2; i++) {
70
+ const start = performance.now();
71
+ const reply = await send(info.port, info.token, { op: 'score', request: c.request });
72
+ const latencyMs = performance.now() - start;
73
+ let decision;
74
+ let scores = null;
75
+ if (reply.ok) {
76
+ decision = decide(c.request, reply.response, policy);
77
+ if (decision.selectedId === 'unclassified') {
78
+ decision = { ...decision, status: 'abstain', reason: 'unclassified', selectedId: null };
79
+ }
80
+ if (['accepted', 'abstain'].includes(decision.status)) scores = reply.response.scores.map(s => s.probability);
81
+ } else {
82
+ decision = { status: 'unavailable', reason: reply.reason };
83
+ }
84
+ runs.push({ decision, scores, latencyMs, coldStart: false });
85
+ }
86
+ out.push({ id: c.id, baseline: c.baseline, runs });
87
+ }
88
+ return out;
89
+ });
90
+ fs.writeFileSync(recordsPath, `${JSON.stringify(records)}\n`);
91
+ console.log(JSON.stringify({ cases: records.length }));
92
+ }
93
+
94
+ function report(corpusPath, recordsPath, reportPath, secondaryThreshold) {
95
+ const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
96
+ const records = JSON.parse(fs.readFileSync(recordsPath, 'utf8'));
97
+ const result = evaluate(corpus, records, null, { secondaryThreshold });
98
+ fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`);
99
+ console.log(JSON.stringify({ cases: result.cases, coverage: result.model.coverage,
100
+ acceptedPrecision: result.model.acceptedPrecision, rolloutReady: result.rolloutReady }));
101
+ }
102
+
103
+ async function main() {
104
+ const [mode, a, b, c, d] = process.argv.slice(2);
105
+ if (mode === 'score' && a && b && c) return score(a, b, c, d || fs.mkdtempSync('/tmp/laya-eval-'));
106
+ if (mode === 'report' && a && b && c) {
107
+ const t = d === undefined ? undefined : Number((d.match(/^--secondary-threshold=(.+)$/) || [])[1]);
108
+ return report(a, b, c, t);
109
+ }
110
+ throw new Error('usage: system-one-eval-laya.js (score <cases> <manifest> <records> [workdir] | report <corpus> <records> <report> [--secondary-threshold=T])');
111
+ }
112
+ main().catch(err => { console.error(`laya research eval failed: ${err.message}`); process.exitCode = 1; });
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env python3
2
+ """Export soft-target RLCD training items for Laya fine-tuning (#255 Phase 3).
3
+
4
+ Each train prompt with teacher relevance scores becomes 12 `noul` items, one
5
+ per owner intent (catalog order, v1 wording shared with system-one-label-laya).
6
+ Teacher relevance p maps to gold probabilities {true: p, false: 1-p}, matching
7
+ the upstream fine-tune notebook (LocalLLaMA/typed-decisions, noul target
8
+ [false, true], qtype 2, label = argmax).
9
+
10
+ Output is reviewable jsonl (one item per line); the training host converts it
11
+ to train_items.pt with torch.save (never commit .pt). Sequence building goes
12
+ through laya.common.build_sequence so tokenization matches inference exactly.
13
+ Stdlib only at import time; `laya` is imported lazily inside the default
14
+ builder so contract tests run without model weights.
15
+ """
16
+ import argparse
17
+ import hashlib
18
+ import importlib.util
19
+ import json
20
+ import random
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ HERE = Path(__file__).resolve()
25
+ _label_spec = importlib.util.spec_from_file_location(
26
+ 'laya_labeler', HERE.parent / 'system-one-label-laya.py')
27
+ _label = importlib.util.module_from_spec(_label_spec)
28
+ _label_spec.loader.exec_module(_label)
29
+ INTENTS = _label.INTENTS
30
+ QUESTIONS_VERSION = _label.QUESTIONS_VERSION
31
+ build_questions = _label.build_questions
32
+
33
+ # laya.common.QTYPES = {"choice": 0, "score": 1, "noul": 2}.
34
+ QTYPE_NOUL = 2
35
+
36
+
37
+ def build_item(state_text, instructions, probs, builder):
38
+ """One upstream-format training item from gold {true, false} probabilities."""
39
+ pf, pt = float(probs['false']), float(probs['true'])
40
+ if not (pf >= 0 and pt >= 0 and abs(pf + pt - 1.0) < 1e-6):
41
+ raise ValueError(f'probabilities must sum to 1, got {probs!r}')
42
+ built = builder(state_text, instructions, {'true': pt, 'false': pf})
43
+ if built is None:
44
+ return None
45
+ target = [built['target'][0], built['target'][1]]
46
+ if abs(sum(target) - 1.0) >= 1e-6:
47
+ raise ValueError(f'builder target must sum to 1, got {target!r}')
48
+ return {'ids': built['ids'], 'markers': built['markers'], 'qtype': QTYPE_NOUL,
49
+ 'target': target, 'label': target.index(max(target))}
50
+
51
+
52
+ def parse_oversample(spec):
53
+ """Parse 'intent:factor,...' into {intent: factor}; factor 1 is rejected."""
54
+ table = {}
55
+ for part in (spec or '').split(','):
56
+ if not part.strip():
57
+ continue
58
+ try:
59
+ intent, factor = part.split(':')
60
+ factor = int(factor)
61
+ except ValueError:
62
+ raise ValueError(f'oversample: bad entry {part!r}, want intent:factor')
63
+ if intent not in INTENTS or factor < 2:
64
+ raise ValueError(f'oversample: bad entry {part!r}, want known intent and factor >= 2')
65
+ table[intent] = factor
66
+ return table
67
+
68
+
69
+ def default_builder_factory(checkpoint='convaiinnovations/laya', subfolder='multilingual'):
70
+ """Real sequence builder through laya.common (lazy import)."""
71
+ from huggingface_hub import snapshot_download # noqa: PLC0415
72
+ from transformers import AutoTokenizer # noqa: PLC0415
73
+ from laya.agent import _fix_tokenizer_config # noqa: PLC0415
74
+ from laya.common import build_sequence, render_options # noqa: PLC0415
75
+
76
+ model_dir = Path(snapshot_download(checkpoint))
77
+ revision = model_dir.name
78
+ base = model_dir / subfolder if subfolder else model_dir
79
+ _fix_tokenizer_config(str(base))
80
+ tok = AutoTokenizer.from_pretrained(str(base / 'tokenizer'))
81
+ cfg = json.loads((base / 'rl_agent_config.json').read_text(encoding='utf-8'))
82
+ questions = build_questions()
83
+
84
+ def builder(state_text, instructions, probs):
85
+ question = {'t': 'noul', 'ins': instructions, 'crit': {}}
86
+ seq, markers = build_sequence(tok, state_text, question, cfg['max_len'], cfg['head_max_len'])
87
+ if len(markers) != len(render_options(question)):
88
+ return None
89
+ return {'ids': seq, 'markers': markers,
90
+ 'target': [probs['false'], probs['true']]}
91
+
92
+ provenance = {'checkpoint': checkpoint, 'revision': revision, 'subfolder': subfolder or 'root',
93
+ 'questions': QUESTIONS_VERSION,
94
+ 'tokenizerDir': str(base / 'tokenizer')}
95
+ return builder, provenance
96
+
97
+
98
+ def export(data_dir, split, out_path, manifest_path, builder, seed=0, forbid_path=None,
99
+ teacher_file='labels-intent-scores.jsonl', provenance=None, oversample=None):
100
+ """Export one prompts.jsonl split to upstream-format items."""
101
+ if split not in ('train', 'validation'):
102
+ raise ValueError(f'split must be train or validation, got {split!r}')
103
+ oversample = dict(oversample or {})
104
+ for intent, factor in oversample.items():
105
+ if intent not in INTENTS or not isinstance(factor, int) or factor < 2:
106
+ raise ValueError(f'oversample: bad entry {intent!r}:{factor!r}')
107
+ data_dir, out_path, manifest_path = Path(data_dir), Path(out_path), Path(manifest_path)
108
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
109
+ teacher_raw = (data_dir / teacher_file).read_text(encoding='utf-8')
110
+ teacher = {r['id']: r for r in
111
+ (json.loads(line) for line in teacher_raw.splitlines() if line.strip())}
112
+ rows = sorted((r for r in (json.loads(line) for line in prompts_raw.splitlines() if line.strip())
113
+ if r.get('split') == split), key=lambda r: r['id'])
114
+ if forbid_path is not None:
115
+ forbidden = set(json.loads(Path(forbid_path).read_text(encoding='utf-8')))
116
+ clash = sorted({r['id'] for r in rows} & forbidden)
117
+ if clash:
118
+ raise ValueError(f'overlap with forbidden ids: {clash[:5]} (n={len(clash)})')
119
+ if seed:
120
+ random.Random(seed).shuffle(rows)
121
+ questions = build_questions()
122
+ items, skipped_no_teacher, skipped_markers = 0, 0, 0
123
+ with out_path.open('w', encoding='utf-8') as fh:
124
+ for prompt in rows:
125
+ trow = teacher.get(prompt['id'])
126
+ if trow is None:
127
+ skipped_no_teacher += 1
128
+ continue
129
+ for intent in INTENTS:
130
+ p = float(trow['scores'].get(intent, 0.0))
131
+ repeat = oversample.get(intent, 1) if p >= 0.4 else 1
132
+ item = build_item(prompt['text'], questions[intent]['instructions'],
133
+ {'true': p, 'false': 1.0 - p}, builder)
134
+ if item is None:
135
+ skipped_markers += repeat
136
+ continue
137
+ line = json.dumps({'promptId': prompt['id'], 'intent': intent, **item}) + '\n'
138
+ for _ in range(repeat):
139
+ fh.write(line)
140
+ items += repeat
141
+ manifest = {'split': split, 'seed': seed, 'questions': QUESTIONS_VERSION,
142
+ 'builder': provenance or {'builder': 'injected'},
143
+ 'oversample': oversample,
144
+ 'items': items, 'prompts': len(rows),
145
+ 'skippedNoTeacher': skipped_no_teacher, 'skippedMarkers': skipped_markers,
146
+ 'promptsSha256': hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
147
+ 'teacherSha256': hashlib.sha256(teacher_raw.encode('utf-8')).hexdigest(),
148
+ 'teacherFile': teacher_file,
149
+ 'outputSha256': hashlib.sha256(out_path.read_bytes()).hexdigest()}
150
+ manifest_path.write_text(json.dumps(manifest, indent=2), encoding='utf-8')
151
+ return {'items': items, 'skippedNoTeacher': skipped_no_teacher,
152
+ 'skippedMarkers': skipped_markers, 'manifest': manifest}
153
+
154
+
155
+ def main(argv=None):
156
+ parser = argparse.ArgumentParser(description=__doc__)
157
+ parser.add_argument('--data-dir', required=True, help='private corpus dir')
158
+ parser.add_argument('--split', choices=['train', 'validation'], default='train')
159
+ parser.add_argument('--out', required=True, help='output items jsonl')
160
+ parser.add_argument('--manifest', required=True, help='provenance manifest json')
161
+ parser.add_argument('--checkpoint', default='convaiinnovations/laya')
162
+ parser.add_argument('--subfolder', default='multilingual')
163
+ parser.add_argument('--seed', type=int, default=0, help='0 keeps id order; nonzero shuffles')
164
+ parser.add_argument('--forbid', default=None, help='json list of forbidden ids (overlap guard)')
165
+ parser.add_argument('--teacher', default='labels-intent-scores.jsonl')
166
+ parser.add_argument('--oversample', default='', help='intent:factor,... to repeat positive intent items')
167
+ parser.add_argument('--limit', type=int, default=0, help='export only the first N prompts (0 = all)')
168
+ args = parser.parse_args(argv)
169
+ oversample = parse_oversample(args.oversample)
170
+ builder, provenance = default_builder_factory(args.checkpoint, args.subfolder)
171
+ print(json.dumps({'builder': provenance}))
172
+ # --limit applies before export by staging a filtered view; export() keeps split semantics.
173
+ if args.limit > 0:
174
+ import tempfile # noqa: PLC0415
175
+ data_dir = Path(args.data_dir)
176
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
177
+ rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
178
+ todo = sorted((r for r in rows if r.get('split') == args.split), key=lambda r: r['id'])[:args.limit]
179
+ with tempfile.TemporaryDirectory() as tmp:
180
+ view = Path(tmp) / 'view'
181
+ view.mkdir()
182
+ (view / 'prompts.jsonl').write_text(
183
+ '\n'.join(json.dumps(r) for r in todo) + '\n', encoding='utf-8')
184
+ (view / args.teacher).write_text((data_dir / args.teacher).read_text(encoding='utf-8'), encoding='utf-8')
185
+ summary = export(view, args.split, args.out, args.manifest, builder,
186
+ seed=args.seed, forbid_path=args.forbid, teacher_file=args.teacher,
187
+ provenance=provenance, oversample=oversample)
188
+ else:
189
+ summary = export(args.data_dir, args.split, args.out, args.manifest, builder,
190
+ seed=args.seed, forbid_path=args.forbid, teacher_file=args.teacher,
191
+ provenance=provenance, oversample=oversample)
192
+ print(json.dumps({k: v for k, v in summary.items() if k != 'manifest'}))
193
+ return 0
194
+
195
+
196
+ if __name__ == '__main__':
197
+ sys.exit(main())
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ """Zero-shot dense intent labeling with Laya (#255 Phase 1+2).
3
+
4
+ Maps each of the 12 owner intents (catalogs.js order, frozen) to one Laya
5
+ `noul` question and records the yes-probability as the per-intent relevance
6
+ score. Output rows match the `labels-intent-scores.jsonl` trainer contract
7
+ {id, gold, secondary, scores}, but MUST be written to a separate file: the
8
+ sonnet-scored labels are never overwritten. Derived gold/secondary are
9
+ mechanical bootstrap only (labeler.derived=true), never owner truth.
10
+
11
+ Stdlib only at import time; `laya` / `laya_mlx` are imported lazily inside
12
+ the backend constructors so contract tests run without model weights.
13
+ """
14
+ import argparse
15
+ import hashlib
16
+ import json
17
+ import math
18
+ import statistics
19
+ import sys
20
+ import time
21
+ from pathlib import Path
22
+
23
+ # Owner catalog order (harness-everything/scripts/system-one/catalogs.js).
24
+ # This order is the scorer output order; changing it invalidates weights.
25
+ INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
26
+ 'review', 'test', 'docs', 'plan', 'investigate']
27
+
28
+ QUESTIONS_VERSION = 'v1'
29
+
30
+ # The sonnet-scored labels file. The labeler refuses to write to it.
31
+ PROTECTED_OUTPUT = 'labels-intent-scores.jsonl'
32
+
33
+ _PROPOSITIONS = {
34
+ 'explain': 'The request asks for an explanation of how something works or an answer to a question.',
35
+ 'discuss': 'The request asks to weigh options, give an opinion, or settle a decision.',
36
+ 'git': 'The request asks for Git or GitHub housekeeping: commit, push, branch, or pull request.',
37
+ 'fix': 'The request asks to repair wrong behavior: a bug, crash, failing test, or build.',
38
+ 'edit': 'The request asks to change an existing value, setting, text, or behavior.',
39
+ 'feature': 'The request asks to add new behavior: a command, option, format, or script.',
40
+ 'refactor': 'The request asks to restructure or unify existing code without adding new behavior.',
41
+ 'review': 'The request asks to evaluate an existing artifact against a standard.',
42
+ 'test': 'The request asks to run tests or builds, or to write tests as the main deliverable.',
43
+ 'docs': 'The request asks to write or edit documentation as the main deliverable.',
44
+ 'plan': 'The request asks to produce a plan, specification, or breakdown before implementation.',
45
+ 'investigate': 'The request asks to find facts or a cause and report back without changing anything.',
46
+ }
47
+
48
+ # v2 adds contrastive exclusions aimed at the measured top-2 co-firing pairs
49
+ # (edit/refactor, fix/investigate, investigate/review/explain, docs/edit,
50
+ # feature/edit, plan/discuss). Same 12 intents, same semantics: exclusions
51
+ # only sharpen boundaries, they never add or remove a category.
52
+ _PROPOSITIONS_V2 = {
53
+ 'explain': 'The request asks for an explanation of how something works or an answer to a question. Not investigating an unknown cause (investigate).',
54
+ 'discuss': 'The request asks to weigh options, give an opinion, or settle a decision. Not producing an implementation plan or breakdown (plan).',
55
+ 'git': 'The request asks for Git or GitHub housekeeping: commit, push, branch, or pull request.',
56
+ 'fix': 'The request asks to repair wrong behavior: a bug, crash, failing test, or build. Not merely finding the cause (investigate); only when a repair is requested.',
57
+ 'edit': 'The request asks to change an existing value, setting, text, or behavior. Not a bug repair (fix), not a restructuring (refactor), not new behavior (feature), not documentation as the deliverable (docs).',
58
+ 'feature': 'The request asks to add new behavior: a command, option, format, or script. Not modifying existing behavior (edit).',
59
+ 'refactor': 'The request asks to restructure or unify existing code without adding new behavior. Not a value change (edit), not a bug repair (fix).',
60
+ 'review': 'The request asks to evaluate an existing artifact against a standard. Not fact-finding (investigate).',
61
+ 'test': 'The request asks to run tests or builds, or to write tests as the main deliverable.',
62
+ 'docs': 'The request asks to write or edit documentation as the main deliverable. Not incidental text edits alongside other work (edit).',
63
+ 'plan': 'The request asks to produce a plan, specification, or breakdown before implementation. Not weighing options in discussion (discuss).',
64
+ 'investigate': 'The request asks to find facts or a cause and report back without changing anything. Not repairing (fix), not judging quality (review), not explaining how things work (explain).',
65
+ }
66
+
67
+ _QUESTIONS = {'v1': _PROPOSITIONS, 'v2': _PROPOSITIONS_V2}
68
+
69
+
70
+ def build_questions(version='v1'):
71
+ """One `noul` question per intent, in catalog order."""
72
+ if version not in _QUESTIONS:
73
+ raise ValueError(f'unknown questions version: {version!r}')
74
+ table = _QUESTIONS[version]
75
+ return {intent: {'type': 'noul', 'instructions': table[intent]} for intent in INTENTS}
76
+
77
+
78
+ def validate_scores(scores):
79
+ """True when scores cover exactly the 12 intents with finite 0..1 floats."""
80
+ if not isinstance(scores, dict) or set(scores) != set(INTENTS):
81
+ raise ValueError(f'scores keys must be exactly the 12 intents, got {sorted(scores) if isinstance(scores, dict) else type(scores)}')
82
+ for intent in INTENTS:
83
+ v = scores[intent]
84
+ if not isinstance(v, float) or not math.isfinite(v) or not 0.0 <= v <= 1.0:
85
+ raise ValueError(f'scores[{intent!r}] out of range: {v!r}')
86
+ return True
87
+
88
+
89
+ def derive_label(scores):
90
+ """Mechanical {gold, secondary} from bands (bootstrap only, derived=true).
91
+
92
+ Mirrors the scoredLabel bands: secondary is every other intent at 0.4+;
93
+ a max at or below 0.2 abstains to null. Ties break by catalog order.
94
+ """
95
+ validate_scores(scores)
96
+ top = max(scores[intent] for intent in INTENTS)
97
+ if top <= 0.2:
98
+ return {'gold': None, 'secondary': [], 'derived': True}
99
+ gold = max(INTENTS, key=lambda i: (scores[i], -INTENTS.index(i)))
100
+ secondary = sorted((i for i in INTENTS if i != gold and scores[i] >= 0.4),
101
+ key=lambda i: (-scores[i], INTENTS.index(i)))
102
+ return {'gold': gold, 'secondary': secondary, 'derived': True}
103
+
104
+
105
+ def output_row(prompt, scores, backend, out_name, routing=None, questions_version='v1'):
106
+ """One trainer-contract row for a labeled prompt."""
107
+ if Path(out_name).name == PROTECTED_OUTPUT:
108
+ raise ValueError(f'protected output: refusing to overwrite {PROTECTED_OUTPUT}')
109
+ derived = derive_label(scores)
110
+ labeler = {'task': 'intent', 'target': 'scores', 'backend': backend,
111
+ 'questions': questions_version, 'derived': True}
112
+ if routing is not None:
113
+ labeler['routing'] = routing
114
+ return {'id': prompt['id'], 'gold': derived['gold'], 'secondary': derived['secondary'],
115
+ 'scores': {intent: scores[intent] for intent in INTENTS},
116
+ 'labeler': labeler}
117
+
118
+
119
+ def torch_backend(questions_version='v1'):
120
+ """Official PyTorch Laya backend (lazy import). Returns (fn, provenance)."""
121
+ import laya # noqa: PLC0415
122
+ router = laya.Router()
123
+ questions = build_questions(questions_version)
124
+
125
+ def run(text):
126
+ started = time.monotonic()
127
+ result = router.predict(text, questions)
128
+ elapsed_ms = (time.monotonic() - started) * 1000.0
129
+ answers = result['answers']
130
+ scores = {intent: float(answers[intent]['noul']) for intent in INTENTS}
131
+ return scores, result['routing']['model'], elapsed_ms
132
+
133
+ provenance = {'package': 'laya', 'version': laya.__version__, 'questions': questions_version}
134
+ return run, provenance
135
+
136
+
137
+ def mlx_backend(questions_version='v1'):
138
+ """Native MLX Laya port backend (lazy import, inference only)."""
139
+ import laya_mlx # noqa: PLC0415
140
+ from laya_mlx import Router # noqa: PLC0415
141
+ router = Router()
142
+ questions = build_questions(questions_version)
143
+
144
+ def run(text):
145
+ started = time.monotonic()
146
+ result = router.predict(text, questions)
147
+ elapsed_ms = (time.monotonic() - started) * 1000.0
148
+ answers = result['answers']
149
+ scores = {intent: float(answers[intent]['noul']) for intent in INTENTS}
150
+ return scores, result['routing']['model'], elapsed_ms
151
+
152
+ provenance = {'package': 'laya-mlx', 'version': laya_mlx.__version__, 'questions': questions_version}
153
+ return run, provenance
154
+
155
+
156
+ def label_rows(rows, split, input_sha, out_path, manifest_path, backend_fn, backend_name, resume=False,
157
+ questions_version='v1'):
158
+ """Label explicit rows; returns a summary dict."""
159
+ out_path, manifest_path = Path(out_path), Path(manifest_path)
160
+ if out_path.name == PROTECTED_OUTPUT:
161
+ raise ValueError(f'protected output: refusing to overwrite {PROTECTED_OUTPUT}')
162
+ done = set()
163
+ if resume and out_path.exists():
164
+ for line in out_path.read_text(encoding='utf-8').splitlines():
165
+ if line.strip():
166
+ done.add(json.loads(line)['id'])
167
+ rows = [r for r in rows if r['id'] not in done]
168
+ routing, latencies, this_run = {}, [], 0
169
+ with out_path.open('a' if (resume and done) else 'w', encoding='utf-8') as fh:
170
+ for prompt in rows:
171
+ scores, routed, elapsed_ms = backend_fn(prompt['text'])
172
+ fh.write(json.dumps(output_row(prompt, scores, backend_name, out_path.name, routed,
173
+ questions_version)) + '\n')
174
+ routing[routed] = routing.get(routed, 0) + 1
175
+ latencies.append(elapsed_ms)
176
+ this_run += 1
177
+ total, full_routing = this_run, routing
178
+ if resume:
179
+ # The file is the source of truth: recount everything for cumulative totals.
180
+ full_routing = {}
181
+ total = 0
182
+ for line in out_path.read_text(encoding='utf-8').splitlines():
183
+ if not line.strip():
184
+ continue
185
+ row = json.loads(line)
186
+ routed = (row.get('labeler') or {}).get('routing', 'unknown')
187
+ full_routing[routed] = full_routing.get(routed, 0) + 1
188
+ total += 1
189
+ manifest = {'backend': backend_name, 'split': split, 'questions': questions_version,
190
+ 'labeled': total, 'thisRun': this_run, 'resumed': bool(resume and done),
191
+ 'inputSha256': input_sha, 'routing': full_routing,
192
+ 'latencyMs': {'p50': statistics.median(latencies) if latencies else 0.0,
193
+ 'p95': statistics.quantiles(latencies, n=100)[94] if len(latencies) >= 100 else (max(latencies) if latencies else 0.0)}}
194
+ manifest_path.write_text(json.dumps(manifest, indent=2), encoding='utf-8')
195
+ return manifest
196
+
197
+
198
+ def label_split(data_dir, split, out_path, manifest_path, backend_fn, backend_name, questions_version='v1'):
199
+ """Label one prompts.jsonl split; returns a summary dict."""
200
+ if split not in ('train', 'validation'):
201
+ raise ValueError(f'split must be train or validation, got {split!r}')
202
+ data_dir = Path(data_dir)
203
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
204
+ rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
205
+ todo = [r for r in rows if r.get('split') == split]
206
+ return label_rows(todo, split, hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
207
+ out_path, manifest_path, backend_fn, backend_name,
208
+ questions_version=questions_version)
209
+
210
+
211
+ def main(argv=None):
212
+ parser = argparse.ArgumentParser(description=__doc__)
213
+ parser.add_argument('--data-dir', required=True, help='private corpus dir (holds prompts.jsonl)')
214
+ parser.add_argument('--split', choices=['train', 'validation'], default='validation')
215
+ parser.add_argument('--out', required=True, help='output jsonl (never labels-intent-scores.jsonl)')
216
+ parser.add_argument('--manifest', required=True, help='provenance manifest json')
217
+ parser.add_argument('--backend', choices=['torch', 'mlx'], required=True)
218
+ parser.add_argument('--questions', choices=['v1', 'v2'], default='v1')
219
+ parser.add_argument('--limit', type=int, default=0, help='label only the first N rows (0 = all)')
220
+ parser.add_argument('--resume', action='store_true', help='skip ids already present in --out')
221
+ args = parser.parse_args(argv)
222
+ run, provenance = (torch_backend(args.questions) if args.backend == 'torch'
223
+ else mlx_backend(args.questions))
224
+ print(json.dumps({'backend': args.backend, 'provenance': provenance}))
225
+
226
+ data_dir = Path(args.data_dir)
227
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
228
+ rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
229
+ todo = [r for r in rows if r.get('split') == args.split]
230
+ if args.limit > 0:
231
+ todo = todo[:args.limit]
232
+ summary = label_rows(todo, args.split,
233
+ hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
234
+ args.out, args.manifest, run, args.backend, resume=args.resume,
235
+ questions_version=args.questions)
236
+ summary['limit'] = args.limit
237
+ Path(args.manifest).write_text(json.dumps(summary, indent=2), encoding='utf-8')
238
+ print(json.dumps(summary))
239
+ return 0
240
+
241
+
242
+ if __name__ == '__main__':
243
+ sys.exit(main())