harness-everything 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) 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 +47 -0
  5. package/PRIVACY.md +9 -0
  6. package/benchmarks/fixtures/system-one-holdout-reviews.json +18 -18
  7. package/benchmarks/fixtures/system-one-holdout.json +9 -9
  8. package/benchmarks/fixtures/system-one-intent-holdout-draft.json +2228 -0
  9. package/benchmarks/fixtures/system-one-intent-holdout.json +16509 -0
  10. package/benchmarks/fixtures/system-one-intent-reviews.json +2723 -0
  11. package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/README.md +75 -0
  12. package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/report.json +10832 -0
  13. package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/README.md +72 -0
  14. package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/report.json +10817 -0
  15. package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/README.md +63 -0
  16. package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/report.json +10817 -0
  17. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/README.md +64 -0
  18. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/report.json +15177 -0
  19. package/benchmarks/results/system-one/labeler-holdout-check-2026-09-24/report.json +1104 -0
  20. package/benchmarks/results/system-one/labeler-holdout-check-codex-2026-09-24/report.json +1104 -0
  21. package/benchmarks/results/system-one/labeler-holdout-check-codex-tier3-2026-09-24/report.json +1104 -0
  22. package/benchmarks/results/system-one/labeler-holdout-check-intent-2026-09-24/report.json +2976 -0
  23. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-005-steps.json +6125 -0
  24. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-haiku-sonnet-escalation.json +6344 -0
  25. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-scaled-margin-escalation.json +6206 -0
  26. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report.json +6103 -0
  27. package/benchmarks/results/system-one/laya-ft-2026-09-25/README.md +83 -0
  28. package/benchmarks/results/system-one/laya-ft-2026-09-25/report.json +71 -0
  29. package/benchmarks/results/system-one/laya-ft-2026-09-25/shadow-report.json +44 -0
  30. package/benchmarks/results/system-one/laya-zs-2026-09-25/README.md +68 -0
  31. package/benchmarks/results/system-one/laya-zs-2026-09-25/report.json +173 -0
  32. package/ci/mechanism-34-observations.test.js +550 -0
  33. package/ci/mechanism-system-one-collect.test.js +16 -0
  34. package/ci/mechanism-system-one-evaluation.test.js +17 -0
  35. package/ci/mechanism-system-one-intent.test.js +406 -0
  36. package/ci/mechanism-system-one-label.test.js +27 -0
  37. package/ci/mechanism-system-one-laya.test.js +45 -0
  38. package/ci/mechanism-system-one-ngram.test.js +161 -0
  39. package/ci/mechanism-system-one-relevance.test.js +112 -0
  40. package/ci/mechanism-system-one-resident.test.js +16 -0
  41. package/ci/mechanism-system-one-train.test.js +12 -0
  42. package/ci/system-one-laya-calibrate-test.py +65 -0
  43. package/ci/system-one-laya-export-test.py +155 -0
  44. package/ci/system-one-laya-label-test.py +201 -0
  45. package/ci/system-one-laya-perintent-test.py +60 -0
  46. package/ci/system-one-laya-provider-test.py +115 -0
  47. package/ci/system-one-laya-train-test.py +93 -0
  48. package/ci/system-one-ngram-parity.py +19 -0
  49. package/ci/system-one-train-test.py +6 -0
  50. package/docs/routing.md +25 -0
  51. package/docs/system-one-corpus.md +31 -3
  52. package/docs/system-one-intent-gates.md +102 -0
  53. package/docs/system-one-intent.md +173 -0
  54. package/docs/system-one-laya-training.md +38 -0
  55. package/docs/system-one-observations.md +206 -0
  56. package/docs/system-one-routing.md +102 -5
  57. package/docs/system-one-suggestion-gates.md +292 -0
  58. package/docs/system-one-training.md +69 -7
  59. package/harness-everything/SKILL.md +1 -1
  60. package/harness-everything/scripts/kernel-router.js +10 -0
  61. package/harness-everything/scripts/system-one/catalogs.js +35 -0
  62. package/harness-everything/scripts/system-one/contract.js +45 -1
  63. package/harness-everything/scripts/system-one/evaluate.js +118 -21
  64. package/harness-everything/scripts/system-one/intent.js +22 -0
  65. package/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  66. package/harness-everything/scripts/system-one/ngram.js +84 -0
  67. package/harness-everything/scripts/system-one/provider.js +24 -7
  68. package/harness-everything/scripts/system-one/resident.js +8 -3
  69. package/harness-everything/scripts/system-one/router.js +1 -1
  70. package/hooks/hooks.json +40 -0
  71. package/hooks/scripts/codex-user-prompt.js +39 -0
  72. package/hooks/scripts/lib/observations.js +581 -0
  73. package/hooks/scripts/observation-hook.js +25 -0
  74. package/opencode-plugin/plugin.json +1 -1
  75. package/package.json +1 -1
  76. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  77. package/plugins/harness-everything/hooks/hooks.json +35 -2
  78. package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +39 -0
  79. package/plugins/harness-everything/hooks/scripts/lib/observations.js +581 -0
  80. package/plugins/harness-everything/hooks/scripts/observation-hook.js +25 -0
  81. package/plugins/harness-everything/plugin.json +1 -1
  82. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  83. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +10 -0
  84. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/catalogs.js +35 -0
  85. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/contract.js +45 -1
  86. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +118 -21
  87. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/intent.js +22 -0
  88. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  89. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/ngram.js +84 -0
  90. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +24 -7
  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 +32 -13
  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 +192 -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 +181 -0
  109. package/scripts/system-one-train.py +14 -1
@@ -0,0 +1,201 @@
1
+ """Stdlib contract tests for the Laya dense intent labeler (#255 Phase 1+2).
2
+
3
+ No model dependency: backends are injected as stubs. Run with:
4
+ python3 ci/system-one-laya-label-test.py
5
+ """
6
+ import importlib.util
7
+ import json
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ sys.dont_write_bytecode = True # never leave __pycache__ in the source tree that packaging copies
14
+ path = Path(__file__).resolve().parents[1] / 'scripts/system-one-label-laya.py'
15
+ spec = importlib.util.spec_from_file_location('laya_labeler', path)
16
+ laya_labeler = importlib.util.module_from_spec(spec)
17
+ spec.loader.exec_module(laya_labeler)
18
+
19
+ INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
20
+ 'review', 'test', 'docs', 'plan', 'investigate']
21
+
22
+
23
+ class MappingTests(unittest.TestCase):
24
+ def test_intent_order_matches_owner_catalog(self):
25
+ # catalogs.js INTENT_OPTIONS order is the scorer output order; it must not drift.
26
+ self.assertEqual(laya_labeler.INTENTS, INTENTS)
27
+
28
+ def test_every_intent_has_one_noul_question(self):
29
+ questions = laya_labeler.build_questions()
30
+ self.assertEqual(list(questions), INTENTS, 'question order follows the catalog order')
31
+ for intent, q in questions.items():
32
+ self.assertEqual(q['type'], 'noul')
33
+ self.assertTrue(q['instructions'], intent)
34
+
35
+ def test_question_order_change_keeps_semantic_mapping(self):
36
+ a = laya_labeler.build_questions()
37
+ b = laya_labeler.build_questions()
38
+ self.assertEqual(a, b)
39
+ self.assertEqual(list(a), INTENTS)
40
+
41
+ def test_mapping_version_is_pinned(self):
42
+ self.assertRegex(laya_labeler.QUESTIONS_VERSION, r'^v\d+$')
43
+
44
+ def test_v2_keeps_intents_and_order_but_sharpens_text(self):
45
+ v1 = laya_labeler.build_questions('v1')
46
+ v2 = laya_labeler.build_questions('v2')
47
+ self.assertEqual(list(v2), INTENTS)
48
+ for intent, q in v2.items():
49
+ self.assertEqual(q['type'], 'noul')
50
+ self.assertTrue(q['instructions'], intent)
51
+ self.assertTrue(any(v2[i]['instructions'] != v1[i]['instructions'] for i in INTENTS),
52
+ 'v2 rewords at least one intent')
53
+ self.assertEqual(v2['git']['instructions'], v1['git']['instructions'],
54
+ 'unconfused intents keep v1 text')
55
+
56
+ def test_unknown_questions_version_fails(self):
57
+ with self.assertRaisesRegex(ValueError, 'unknown questions version'):
58
+ laya_labeler.build_questions('v9')
59
+
60
+
61
+ class ValidatorTests(unittest.TestCase):
62
+ def scores(self, **over):
63
+ base = {i: 0.0 for i in INTENTS}
64
+ base.update(over)
65
+ return base
66
+
67
+ def test_valid_scores_pass(self):
68
+ self.assertTrue(laya_labeler.validate_scores(self.scores(fix=0.9, test=0.4)))
69
+
70
+ def test_missing_or_unknown_key_fails(self):
71
+ bad = self.scores(fix=0.9)
72
+ del bad['fix']
73
+ with self.assertRaisesRegex(ValueError, 'keys'):
74
+ laya_labeler.validate_scores(bad)
75
+ bad2 = self.scores(fix=0.9)
76
+ bad2['nope'] = 0.1
77
+ with self.assertRaisesRegex(ValueError, 'keys'):
78
+ laya_labeler.validate_scores(bad2)
79
+
80
+ def test_out_of_range_or_nan_fails(self):
81
+ for v in (-0.1, 1.1, float('nan')):
82
+ with self.assertRaisesRegex(ValueError, 'range', msg=f'value={v}'):
83
+ laya_labeler.validate_scores(self.scores(fix=v))
84
+
85
+ def test_non_float_fails(self):
86
+ with self.assertRaisesRegex(ValueError, 'range'):
87
+ laya_labeler.validate_scores(self.scores(fix='0.9'))
88
+
89
+
90
+ class DeriveTests(unittest.TestCase):
91
+ def scores(self, **over):
92
+ base = {i: 0.0 for i in INTENTS}
93
+ base.update(over)
94
+ return base
95
+
96
+ def test_primary_and_secondary_from_bands(self):
97
+ out = laya_labeler.derive_label(self.scores(fix=0.9, test=0.5, docs=0.3))
98
+ self.assertEqual(out['gold'], 'fix')
99
+ self.assertEqual(out['secondary'], ['test'])
100
+
101
+ def test_low_max_abstains(self):
102
+ out = laya_labeler.derive_label(self.scores(explain=0.2))
103
+ self.assertIsNone(out['gold'])
104
+ self.assertEqual(out['secondary'], [])
105
+
106
+ def test_tie_breaks_by_catalog_order(self):
107
+ out = laya_labeler.derive_label(self.scores(fix=0.7, test=0.7))
108
+ self.assertEqual(out['gold'], 'fix')
109
+
110
+ def test_derived_labels_are_marked_bootstrap(self):
111
+ out = laya_labeler.derive_label(self.scores(fix=0.9))
112
+ self.assertTrue(out['derived'])
113
+
114
+
115
+ class GuardTests(unittest.TestCase):
116
+ def test_refuses_to_overwrite_sonnet_scores_file(self):
117
+ with self.assertRaisesRegex(ValueError, 'protected'):
118
+ laya_labeler.output_row({'id': 'x', 'text': 'fix it'}, {}, backend='stub',
119
+ out_name='labels-intent-scores.jsonl')
120
+
121
+ def test_output_row_shape_matches_trainer_contract(self):
122
+ scores = {i: (0.9 if i == 'fix' else 0.0) for i in INTENTS}
123
+ row = laya_labeler.output_row({'id': 'abc', 'text': 'fix it'}, scores,
124
+ backend='stub', out_name='labels-intent-laya-zs.jsonl',
125
+ routing='english')
126
+ self.assertEqual(row['id'], 'abc')
127
+ self.assertEqual(row['gold'], 'fix')
128
+ self.assertEqual(row['secondary'], [])
129
+ self.assertEqual(list(row['scores']), INTENTS)
130
+ self.assertEqual(row['labeler']['task'], 'intent')
131
+ self.assertEqual(row['labeler']['routing'], 'english')
132
+ self.assertEqual(row['labeler']['questions'], 'v1')
133
+ self.assertEqual(json.loads(json.dumps(row)), row)
134
+
135
+ def test_output_row_records_questions_version(self):
136
+ scores = {i: 0.1 for i in INTENTS}
137
+ row = laya_labeler.output_row({'id': 'abc', 'text': 'x'}, scores,
138
+ backend='stub', out_name='o.jsonl', questions_version='v2')
139
+ self.assertEqual(row['labeler']['questions'], 'v2')
140
+
141
+
142
+ class EndToEndTests(unittest.TestCase):
143
+ def test_stub_backend_labels_a_split(self):
144
+ prompts = [{'id': 'p1', 'split': 'validation', 'text': 'fix the crash'},
145
+ {'id': 'p2', 'split': 'train', 'text': 'commit and push'}]
146
+ calls = []
147
+
148
+ def stub(text):
149
+ calls.append(text)
150
+ return ({i: (0.8 if i == 'fix' else 0.05) for i in INTENTS}, 'english', 1.0)
151
+
152
+ with tempfile.TemporaryDirectory() as d:
153
+ data = Path(d) / 'data'
154
+ data.mkdir()
155
+ (data / 'prompts.jsonl').write_text(
156
+ '\n'.join(json.dumps(p) for p in prompts) + '\n', encoding='utf-8')
157
+ out = Path(d) / 'labels-intent-laya-zs.jsonl'
158
+ manifest = Path(d) / 'manifest.json'
159
+ summary = laya_labeler.label_split(
160
+ data, 'validation', out, manifest, backend_fn=stub, backend_name='stub')
161
+ self.assertEqual(calls, ['fix the crash'], 'only the requested split is labeled')
162
+ rows = [json.loads(line) for line in out.read_text(encoding='utf-8').strip().split('\n')]
163
+ self.assertEqual([r['id'] for r in rows], ['p1'])
164
+ self.assertEqual(rows[0]['gold'], 'fix')
165
+ self.assertEqual(summary['labeled'], 1)
166
+ man = json.loads(manifest.read_text(encoding='utf-8'))
167
+ self.assertEqual(man['backend'], 'stub')
168
+ self.assertEqual(man['split'], 'validation')
169
+
170
+ def test_resume_skips_done_ids(self):
171
+ calls = []
172
+
173
+ def stub(text):
174
+ calls.append(text)
175
+ return ({i: (0.8 if i == 'fix' else 0.05) for i in INTENTS}, 'english', 1.0)
176
+
177
+ with tempfile.TemporaryDirectory() as d:
178
+ data = Path(d) / 'data'
179
+ data.mkdir()
180
+ prompts = [{'id': f'p{i}', 'split': 'validation', 'text': f'text {i}'} for i in range(3)]
181
+ (data / 'prompts.jsonl').write_text(
182
+ '\n'.join(json.dumps(p) for p in prompts) + '\n', encoding='utf-8')
183
+ out = Path(d) / 'labels-intent-laya-zs.jsonl'
184
+ manifest = Path(d) / 'manifest.json'
185
+ laya_labeler.label_split(data, 'validation', out, manifest,
186
+ backend_fn=stub, backend_name='stub')
187
+ self.assertEqual(len(calls), 3)
188
+ laya_labeler.label_rows(
189
+ prompts, 'validation', 'sha', out, manifest, backend_fn=stub,
190
+ backend_name='stub', resume=True)
191
+ self.assertEqual(len(calls), 3, 'resume makes no new backend calls')
192
+ self.assertEqual(
193
+ [json.loads(line)['id'] for line in out.read_text(encoding='utf-8').splitlines()],
194
+ ['p0', 'p1', 'p2'], 'resume appends nothing when all ids are done')
195
+ man2 = json.loads(manifest.read_text(encoding='utf-8'))
196
+ self.assertEqual([man2['labeled'], man2['thisRun'], man2['resumed']], [3, 0, True])
197
+ self.assertEqual(man2['routing'], {'english': 3})
198
+
199
+
200
+ if __name__ == '__main__':
201
+ unittest.main()
@@ -0,0 +1,60 @@
1
+ """Stdlib tests for per-intent Bernoulli evaluation (#255 Phase 3).
2
+
3
+ Each intent is its own binary decision with its own threshold: no simplex,
4
+ no single winner. Run with:
5
+ python3 ci/system-one-laya-perintent-test.py
6
+ """
7
+ import importlib.util
8
+ import sys
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ sys.dont_write_bytecode = True # never leave __pycache__ in the source tree that packaging copies
13
+ path = Path(__file__).resolve().parents[1] / 'scripts/system-one-perintent-laya.py'
14
+ spec = importlib.util.spec_from_file_location('laya_perintent', path)
15
+ laya_perintent = importlib.util.module_from_spec(spec)
16
+ spec.loader.exec_module(laya_perintent)
17
+
18
+ INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
19
+ 'review', 'test', 'docs', 'plan', 'investigate']
20
+
21
+
22
+ class BinarizeTests(unittest.TestCase):
23
+ def test_band_boundary(self):
24
+ self.assertEqual(laya_perintent.binarize({'fix': 0.4, 'test': 0.39}), {'fix', 'test'} - {'test'})
25
+ self.assertEqual(laya_perintent.binarize({'fix': 0.4}), {'fix'})
26
+
27
+ def test_custom_positive_band(self):
28
+ self.assertEqual(laya_perintent.binarize({'fix': 0.5}, band=0.6), set())
29
+
30
+
31
+ class SweepTests(unittest.TestCase):
32
+ def test_separable_intent_gets_perfect_threshold(self):
33
+ scores = [0.1, 0.2, 0.8, 0.9]
34
+ truth = [False, False, True, True]
35
+ tau, p, r, f = laya_perintent.sweep_threshold(scores, truth)
36
+ self.assertEqual((p, r, f), (1.0, 1.0, 1.0))
37
+ self.assertTrue(0.2 < tau <= 0.8)
38
+
39
+ def test_all_negative_stays_silent(self):
40
+ tau, p, r, f = laya_perintent.sweep_threshold([0.1, 0.2, 0.3], [False, False, False])
41
+ self.assertEqual(r, 0.0)
42
+ self.assertTrue(tau > 0.3)
43
+
44
+
45
+ class MicroTests(unittest.TestCase):
46
+ def test_hand_computed_micro(self):
47
+ # totals: tp=1, fp=2, fn=2.
48
+ per = {'a': (1, 1, 1), 'b': (0, 1, 1)} # (tp, fp, fn)
49
+ m = laya_perintent.micro(per)
50
+ self.assertAlmostEqual(m['precision'], 1 / 3)
51
+ self.assertAlmostEqual(m['recall'], 1 / 3)
52
+ self.assertAlmostEqual(m['f1'], 1 / 3)
53
+
54
+ def test_empty_predictions_score_zero_not_nan(self):
55
+ m = laya_perintent.micro({'a': (0, 0, 2)})
56
+ self.assertEqual((m['precision'], m['recall'], m['f1']), (0.0, 0.0, 0.0))
57
+
58
+
59
+ if __name__ == '__main__':
60
+ unittest.main()
@@ -0,0 +1,115 @@
1
+ """Contract tests for the Laya fine-tuned provider adapter (#255 Phase 3).
2
+
3
+ The 13-option evaluator vector, unclassified ramp, limits and artifact
4
+ rules are all pinned here with a stub predictor: no model weights needed.
5
+ Run with:
6
+ python3 ci/system-one-laya-provider-test.py
7
+ """
8
+ import hashlib
9
+ import importlib.util
10
+ import json
11
+ import sys
12
+ import tempfile
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+ sys.dont_write_bytecode = True # never leave __pycache__ in the source tree that packaging copies
17
+ path = Path(__file__).resolve().parents[1] / 'harness-everything/scripts/system-one/laya_adapter.py'
18
+ spec = importlib.util.spec_from_file_location('laya_adapter', path)
19
+ laya_adapter = importlib.util.module_from_spec(spec)
20
+ spec.loader.exec_module(laya_adapter)
21
+
22
+ INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
23
+ 'review', 'test', 'docs', 'plan', 'investigate']
24
+ OPTIONS = [{'id': i, 'text': f'{i} text'} for i in INTENTS] + [{'id': 'unclassified', 'text': 'unclear'}]
25
+
26
+
27
+ def request(options=None, task='intent', context='fix it'):
28
+ return {'schemaVersion': 1, 'task': task, 'context': context,
29
+ 'options': options if options is not None else OPTIONS,
30
+ 'catalogHash': 'b' * 64, 'requestHash': 'a' * 64}
31
+
32
+
33
+ def stub_predict(probs):
34
+ def run(state, questions):
35
+ assert state and set(questions) == set(INTENTS)
36
+ return dict(probs)
37
+ return run
38
+
39
+
40
+ class ArtifactTests(unittest.TestCase):
41
+ def write_ckpt(self, root, weights=b'fake-weights-1', config=b'{"cfg":1}'):
42
+ ckpt = Path(root) / 'model.safetensors'
43
+ ckpt.write_bytes(weights)
44
+ (Path(root) / 'rl_agent_config.json').write_bytes(config)
45
+ return {'checkpoint': str(ckpt),
46
+ 'weightsSha256': hashlib.sha256(weights).hexdigest(),
47
+ 'configSha256': hashlib.sha256(config).hexdigest()}
48
+
49
+ def test_verify_ok_and_config_sibling_rule(self):
50
+ with tempfile.TemporaryDirectory() as d:
51
+ m = self.write_ckpt(d)
52
+ self.assertEqual(laya_adapter.verify_artifacts(m), Path(m['checkpoint']))
53
+
54
+ def test_hash_mismatch_and_bad_format_fail(self):
55
+ with tempfile.TemporaryDirectory() as d:
56
+ m = self.write_ckpt(d)
57
+ bad = dict(m, weightsSha256='0' * 64)
58
+ with self.assertRaisesRegex(ValueError, 'artifact-hash'):
59
+ laya_adapter.verify_artifacts(bad)
60
+ bad2 = dict(m, checkpoint=str(Path(d) / 'model.pt'))
61
+ with self.assertRaisesRegex(ValueError, 'checkpoint-format'):
62
+ laya_adapter.verify_artifacts(bad2)
63
+ bad3 = dict(m, checkpoint='relative/model.safetensors')
64
+ with self.assertRaisesRegex(ValueError, 'checkpoint-format'):
65
+ laya_adapter.verify_artifacts(bad3)
66
+
67
+
68
+ class ScorerTests(unittest.TestCase):
69
+ def probs(self, **over):
70
+ base = {i: 0.05 for i in INTENTS}
71
+ base.update(over)
72
+ return base
73
+
74
+ def test_scores_follow_request_option_order_raw(self):
75
+ score, _ = laya_adapter.make_scorer(stub_predict(self.probs(fix=0.8)))
76
+ shuffled = [OPTIONS[5], OPTIONS[0], *[o for o in OPTIONS if o['id'] not in ('feature', 'explain')]]
77
+ out = score(request(options=shuffled))
78
+ self.assertEqual(len(out), 13)
79
+ by_id = {o['id']: p for o, p in zip(shuffled, out)}
80
+ self.assertGreater(by_id['fix'], by_id['test'])
81
+ self.assertTrue(all(0.0 <= p <= 1.0 for p in out))
82
+ self.assertGreater(sum(out), 1.0, 'independent scores sum past one: no simplex')
83
+
84
+ def test_confident_max_leaves_no_unclassified_mass(self):
85
+ score, _ = laya_adapter.make_scorer(stub_predict(self.probs(fix=0.9)))
86
+ out = score(request())
87
+ self.assertEqual(out[-1], 0.0, 'max above the cutoff closes the abstain ramp')
88
+
89
+ def test_flat_lows_abstain_through_unclassified(self):
90
+ score, _ = laya_adapter.make_scorer(stub_predict(self.probs()))
91
+ out = score(request())
92
+ self.assertGreater(out[-1], 0.5, 'nothing actionable concentrates on unclassified')
93
+
94
+ def test_non_intent_task_and_missing_intent_fail(self):
95
+ score, _ = laya_adapter.make_scorer(stub_predict(self.probs()))
96
+ with self.assertRaisesRegex(ValueError, 'task'):
97
+ score(request(task='tier'))
98
+ with self.assertRaisesRegex(ValueError, 'options'):
99
+ score(request(options=[{'id': 'fix', 'text': 'x'}, {'id': 'unclassified', 'text': 'y'}]))
100
+
101
+
102
+ class LimitTests(unittest.TestCase):
103
+ def test_boundary_passes_and_overflow_fails(self):
104
+ score, config = laya_adapter.make_scorer(stub_predict({i: 0.1 for i in INTENTS}))
105
+ ok_ctx = 'x' * config['context_tokens']
106
+ laya_adapter.check_limits(request(context=ok_ctx), config)
107
+ with self.assertRaisesRegex(ValueError, 'input-too-long'):
108
+ laya_adapter.check_limits(request(context=ok_ctx + 'x'), config)
109
+ big_opt = [{'id': 'a', 'text': 'y' * (config['option_tokens'] + 1)}, {'id': 'b', 'text': 'z'}]
110
+ with self.assertRaisesRegex(ValueError, 'input-too-long'):
111
+ laya_adapter.check_limits(request(options=big_opt), config)
112
+
113
+
114
+ if __name__ == '__main__':
115
+ unittest.main()
@@ -0,0 +1,93 @@
1
+ """Contract tests for the single-device Laya trainer (#255 Phase 3).
2
+
3
+ Pure parts run on stdlib; torch parts skip when torch is missing
4
+ (same convention as S1-I08/S1-I13). Run with:
5
+ python3 ci/system-one-laya-train-test.py
6
+ """
7
+ import importlib.util
8
+ import sys
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ sys.dont_write_bytecode = True # never leave __pycache__ in the source tree that packaging copies
13
+ path = Path(__file__).resolve().parents[1] / 'scripts/system-one-train-laya.py'
14
+ spec = importlib.util.spec_from_file_location('laya_trainer', path)
15
+ laya_trainer = importlib.util.module_from_spec(spec)
16
+ spec.loader.exec_module(laya_trainer)
17
+
18
+ try:
19
+ import torch # noqa: F401
20
+ HAS_TORCH = True
21
+ except ImportError:
22
+ HAS_TORCH = False
23
+
24
+
25
+ class CalibSplitTests(unittest.TestCase):
26
+ def test_holdout_is_stable_disjoint_and_ten_percent(self):
27
+ items = [{'id': i} for i in range(1000)]
28
+ a_train, a_calib = laya_trainer.calib_split(items, seed=20260922, frac=0.1)
29
+ b_train, b_calib = laya_trainer.calib_split(items, seed=20260922, frac=0.1)
30
+ self.assertEqual(a_calib, b_calib, 'same seed replays the holdout')
31
+ self.assertEqual(len(a_calib), 100)
32
+ self.assertEqual(len(set(map(str, a_train)) & set(map(str, a_calib))), 0, 'disjoint')
33
+ c_train, c_calib = laya_trainer.calib_split(items, seed=1, frac=0.1)
34
+ self.assertNotEqual(a_calib, c_calib, 'seed matters')
35
+
36
+ def test_small_inputs_keep_at_least_one_calib(self):
37
+ _, calib = laya_trainer.calib_split([{'id': i} for i in range(5)], seed=7, frac=0.1)
38
+ self.assertEqual(len(calib), 1)
39
+
40
+ def test_prompt_groups_do_not_cross_split_and_calib_deduplicates_oversamples(self):
41
+ items = []
42
+ for prompt_id in range(20):
43
+ items.append({'promptId': str(prompt_id), 'intent': 'fix'})
44
+ items.append({'promptId': str(prompt_id), 'intent': 'refactor'})
45
+ items.append({'promptId': str(prompt_id), 'intent': 'refactor'}) # oversampled duplicate
46
+ train, calib = laya_trainer.calib_split(items, seed=20260922, frac=0.1)
47
+ train_prompts = {it['promptId'] for it in train}
48
+ calib_prompts = {it['promptId'] for it in calib}
49
+ self.assertFalse(train_prompts & calib_prompts, 'a prompt must stay wholly on one side')
50
+ calib_keys = [(it['promptId'], it['intent']) for it in calib]
51
+ self.assertEqual(len(calib_keys), len(set(calib_keys)),
52
+ 'oversampled duplicates must not overweight temperature fitting')
53
+ self.assertEqual(len(calib), 4, '10% of 40 de-duplicated prompt-intent identities')
54
+ self.assertEqual(sum(1 for it in train if it['intent'] == 'refactor'), 2 * len(train_prompts),
55
+ 'training keeps oversampled copies for non-calibration prompt groups')
56
+
57
+
58
+ class IntentWeightTests(unittest.TestCase):
59
+ def test_default_weights_are_one(self):
60
+ self.assertEqual(laya_trainer.intent_weights(['fix', 'test'], {}), [1.0, 1.0])
61
+
62
+ def test_table_applies_per_item_intent(self):
63
+ self.assertEqual(laya_trainer.intent_weights(['refactor', 'fix', 'refactor'], {'refactor': 2.0}),
64
+ [2.0, 1.0, 2.0])
65
+
66
+ def test_bad_table_fails(self):
67
+ with self.assertRaisesRegex(ValueError, 'intent-weights'):
68
+ laya_trainer.intent_weights(['fix'], {'nope': 2.0})
69
+ with self.assertRaisesRegex(ValueError, 'intent-weights'):
70
+ laya_trainer.intent_weights(['fix'], {'fix': 0})
71
+
72
+
73
+ @unittest.skipUnless(HAS_TORCH, 'torch not installed')
74
+ class TorchTests(unittest.TestCase):
75
+ def test_collate_shapes(self):
76
+ items = [{'ids': [1, 2, 3], 'markers': [0, 2], 'qtype': 2,
77
+ 'target': [0.3, 0.7], 'label': 1} for _ in range(4)]
78
+ batch = laya_trainer.collate(items, pad_id=0)
79
+ self.assertEqual(tuple(batch['input_ids'].shape), (4, 3))
80
+ self.assertEqual(tuple(batch['marker_pos'].shape), (4, 2))
81
+ self.assertTrue(bool((batch['target'].sum(-1) - 1.0 < 1e-6).all()))
82
+
83
+ def test_temperature_fit_is_finite_and_bounded(self):
84
+ import random
85
+ rng = random.Random(0)
86
+ sel = [([rng.uniform(-1, 1), rng.uniform(-1, 1)],
87
+ [0.4, 0.6]) for _ in range(30)]
88
+ t = laya_trainer.fit_one_temp(sel)
89
+ self.assertTrue(0.1 <= t <= 10.0)
90
+
91
+
92
+ if __name__ == '__main__':
93
+ unittest.main()
@@ -0,0 +1,19 @@
1
+ """Print the trainer's n-gram features for texts on stdin (JSON list) so the Node provider can be compared exactly."""
2
+ import importlib.util
3
+ import json
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ sys.dont_write_bytecode = True
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+ spec = importlib.util.spec_from_file_location('ngram_trainer', ROOT / 'scripts' / 'system-one-train-ngram.py')
10
+ trainer = importlib.util.module_from_spec(spec)
11
+ spec.loader.exec_module(trainer)
12
+ assert 'torch' not in sys.modules, 'importing the ngram trainer must not import torch'
13
+
14
+ request = json.loads(sys.stdin.read())
15
+ out = []
16
+ for text in request['texts']:
17
+ feats = trainer.featurize(text, request['nmax'], request['dim'])
18
+ out.append({str(k): v for k, v in sorted(feats.items())})
19
+ print(json.dumps({'hash': [trainer.fnv1a32(s) for s in request['hashes']], 'features': out}))
@@ -46,6 +46,12 @@ assert flat == list(range(len(lengths))), 'every example exactly once'
46
46
  assert all(len(b) * max(lengths[i] for i in b) <= 4096 or len(b) == 1 for b in batches), 'budget respected'
47
47
  assert batches == trainer.make_batches(lengths, token_budget=4096), 'deterministic'
48
48
 
49
+ weights = trainer.class_weights([0, 0, 0, 1, 2, 2], 4)
50
+ assert len(weights) == 4
51
+ assert weights[3] == 0.0, 'an absent class gets no weight'
52
+ assert weights[1] > weights[2] > weights[0], weights
53
+ assert abs(sum(w * n for w, n in zip(weights, [3, 1, 2, 0])) - 6) < 1e-9, 'weights average to 1 over the examples'
54
+
49
55
  with tempfile.TemporaryDirectory() as tmp:
50
56
  p = Path(tmp) / 'rows.jsonl'
51
57
  p.write_text('\n'.join(json.dumps(r) for r in prompts) + '\n', encoding='utf-8')
package/docs/routing.md CHANGED
@@ -64,6 +64,31 @@ Keep one short evidence item inline when that reads better; use nested bullets f
64
64
 
65
65
  This MUST is semantic rather than a hard runtime lock: hosts may observe/remind with different strength, but lack of a blocking hook does not downgrade the contract to optional advice.
66
66
 
67
+ ### Turn label line
68
+
69
+ End the final message of **every** turn with one line that rendered Markdown hides. This includes short answers, lookups, and turns that end with a question back to the user; unlike the Harness Status, it is not limited to non-trivial work:
70
+
71
+ ```
72
+ <!-- harness-label {"v":1,"validity":"actionable","contextDependent":false,"tier":"tier2","intents":["fix","test"],"workflow":"iterative-single","skills":["tdd"]} -->
73
+ ```
74
+
75
+ It states the agent's own judgement after the work, not a copy of the router's suggestion: `validity` (`actionable` or `invalid`), `contextDependent` (the prompt text alone would not have been enough), `tier` under tier rules v2, up to three `intents`, the `workflow` strategy and the `skills` actually used. The observation collector stores it as training data ([system-one-observations.md](system-one-observations.md)). A missing or malformed line is recorded with a reason code; it never blocks a turn or changes routing.
76
+
77
+ Tier values:
78
+
79
+ | Tier | Meaning |
80
+ | --- | --- |
81
+ | `tier1` | answer, lookup, status or Git operation; no file change |
82
+ | `tier2` | a bounded file change: code, config or docs |
83
+ | `tier3` | a new feature, a refactor, a redefinition, or cross-component work |
84
+ | `null` | only when `validity` is `invalid` |
85
+
86
+ The router prints this contract as the **last** section of its output, so it
87
+ is the nearest instruction when the turn ends. In live Claude Code runs the
88
+ label appeared on 1 of 4 turns while it sat inside the Harness Status contract
89
+ (which is scoped to non-trivial work), and on 5 of 6 turns as its own final
90
+ section.
91
+
67
92
  ## Suggested skills vs. selected workflow
68
93
 
69
94
  These are intentionally different concepts.
@@ -16,7 +16,7 @@ to, judged from the prompt alone?** The labels follow the tier model in
16
16
  | --- | --- | --- |
17
17
  | `tier1` | Something answered or done without changing project behavior: Git/GitHub housekeeping (commit, push, open a PR, sync, tag, post given results as a comment), a status check, reading a log or searching and reporting, the smallest doc fix (a typo, a link, an ignore entry), or a reply the assistant can give directly, including non-engineering chores and discussion questions | "commit these changes", "is issue 88 still open?", "find every repo still using the old logging package", "close the other apps" |
18
18
  | `tier2` | A bounded change or investigation: any code, configuration or CI edit (a rename, a value, a revert), running tests or a build, filing an issue, explaining code that must be read first, most bug fixes, a focused review or benchmark, planning or synthesis that produces documents (phase plans, an ADR, an overview), and answers that settle open decisions for work in progress | "rename tmp to buffer", "run npm test", "the CLI fails with this error, fix it", "1. agree 2. keep the old name" |
19
- | `tier3` | New or changed behavior delivered with tests, and anything larger: a new flag or format, a behavior change across producers and tests, a large refactor, a dependency upgrade with breakage, a migration, a repository-wide change, a multi-phase implementation, or delegation to several agents | "add a --verbose flag with tests", "migrate to ESM", "dispatch agents to audit every repo" |
19
+ | `tier3` | Work that needs several agents (sub-agents included); a new module or subsystem, a refactor, or a change to a framework or interface definition; or code or design work that spans several repositories or several workflows | "add a plugin system to the CLI", "refactor the parser into modules", "change the plugin interface", "dispatch agents to audit every repo" |
20
20
  | `null` | No actionable content: a bare continuation, "do what you said", a vague reaction, a question about what the assistant meant, or pasted status with no request | "go", "yes", "照剛剛說的改", "something is off here" |
21
21
 
22
22
  Rules:
@@ -31,6 +31,30 @@ Rules:
31
31
  reformat or extend the assistant's previous answer without changing files is
32
32
  `tier1`. A requested change to code or configuration, however small, is at
33
33
  least `tier2`. Only the smallest doc fixes listed in the table stay `tier1`.
34
+ The line between `tier2` and `tier3` is the owner's: `tier3` when the work
35
+ needs several agents or sub-agents, adds a feature, refactors, changes a
36
+ framework or interface definition, or spans several repositories or
37
+ workflows. Fixing, adjusting, investigating or reviewing existing behavior
38
+ within one workflow is `tier2`.
39
+ The owner settled four more lines from the third spot-check:
40
+ - **Git is `tier1` at any width.** Committing sub-repositories and then the
41
+ main repository is still housekeeping. "Several repositories" applies to
42
+ code or design work only.
43
+ - **Scope, not the word "add".** Adding an option, a function or a parser
44
+ inside one existing tool or component is `tier2`. A new module or
45
+ subsystem, or an addition that spans several components, is `tier3`.
46
+ A bug fix, even with a regression test, is `tier2`.
47
+ - **Repository-wide or cross-component work is `tier3`.** Replacing or
48
+ unifying a pattern across the repository (every `console.log`, all error
49
+ handling, every path in a report), upgrading a dependency and fixing what
50
+ breaks, a data migration, a CI matrix across platforms, or restructuring a
51
+ documentation tree is `tier3`, even when each edit is small.
52
+ - **A short reply follows the work in progress.** A reply that asks to
53
+ change an output (code, a document, an example) is `tier2`. Only a pure
54
+ question or a bare acknowledgement stays `tier1`.
55
+ - **Explicit agents or architecture make `tier3`.** A request for several
56
+ agents, a team discussion or a verifier pass, or one that redefines an
57
+ architecture, is `tier3` even when it is phrased as discussion.
34
58
  4. Explicit workflow words ("use fable", "no subagents") do not change the gold.
35
59
  Precedence is a router policy, not a label.
36
60
  5. Never derive gold from the lexical router or any model output. A reviewer may
@@ -102,6 +126,10 @@ a decision on its own.
102
126
  | `benchmarks/fixtures/system-one-holdout.json` | Evaluation corpus, rebuilt with `node scripts/system-one-corpus.js build <draft> <reviews> <corpus>` |
103
127
 
104
128
  The reviewed holdout has 215 cases (11 rejected): en 93 and zh-TW 122; gold
105
- `tier1` 47, `tier2` 97, `tier3` 51 and `null` 20. It meets the `reviewedHoldout`
106
- gate. It says nothing about model quality until a `harness-routing-v1`
129
+ `tier1` 48, `tier2` 104, `tier3` 43 and `null` 20. It meets the `reviewedHoldout`
130
+ gate. After the third training spot-check the owner moved nine cases to the new
131
+ lines: `h-long-git-ops-1` to `tier1`, and the `h-add-cli-flag-*`,
132
+ `h-hash-algorithm-support-1/2`, `h-fix-cli-error-log-3/4` and
133
+ `h-fix-failing-test-3` cases to `tier2`. Results recorded before that change
134
+ used 47 `tier1`, 97 `tier2` and 51 `tier3`. It says nothing about model quality until a `harness-routing-v1`
107
135
  checkpoint is evaluated on it.