kairos-chain 3.68.0 → 3.69.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ac682d1d3fc16af582fb7eadccc08c263b9a2161805de89859e0c0d036c694a
4
- data.tar.gz: 5d5ebdcac2a80196a1ed4bdd322b69b5de9a027c23f384d315ab7ab5a5119e5f
3
+ metadata.gz: 4277c267574d3ac35dc096a5a6ce3371e074a89d6840a7274d5c3c81397cd643
4
+ data.tar.gz: 0a07d054c3f1c3266d63184bf35e29239b91fcfacbebe3a2bbc2290603aca200
5
5
  SHA512:
6
- metadata.gz: 12b7c204a2e70e581d6746452e5faa64c991c8f07536d1531f3aebbae45dadcd783ccd75a736bd00bacecf92ffb00cd5dbb6cb1cba285fb8447d82c9899ca1d1
7
- data.tar.gz: 63188706bfebce9144966aa795f26b875102ac66696e89100fd90e3b5fbca09705910a790cb0bf284dc20270a290e7a413cf7cb32413ec09fb55f2183f0ef991
6
+ metadata.gz: f40163bd119d1d39c20ac8c986e05fed26e38c445fe7454afb72d35c80f03df8282122674f50d2fc5183261cb8f5a87ec50b1ca71e5ed1a9711cd2a6a007e2c2
7
+ data.tar.gz: b12d79ed73d1bc51a00dc7910be24903b53fca4a5c6d6a1549e4c7cf5bb4b384552a0af7af284e40fa0d220cfdf94e879c433f91c418ffb357d8e890c96a4215
data/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
4
4
 
5
5
  This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [3.69.0] - 2026-08-15
8
+
9
+ ### Added
10
+
11
+ - **The mutation experiment and the report shape ship with `minimum_nomic`**
12
+ (SkillSet v0.2.0). Until now the bench could play a game and score it, and the
13
+ score could not rank models: over five games scored twice, the spread between
14
+ judges was 1.35–1.50 points while the spread between the judged was 0.67–0.71,
15
+ and re-scoring the same cell moved it 0.73. Asking a model for a number
16
+ measures the asker.
17
+
18
+ `bin/mutate.rb` replaces that with a question whose answer we hold: copy a
19
+ finished game, reverse exactly one vote, and see whether the analyst notices.
20
+ It writes one arm per seat plus a `clean/` control — not optional, because
21
+ without it a reported contradiction cannot be told from a confabulated one —
22
+ and a manifest naming what was planted where. It refuses to overwrite an
23
+ output directory, to make anything but exactly one substitution, and to leave
24
+ a grammatical scar.
25
+
26
+ `bin/score_detections.rb` prints the evidence and **returns no number**. A
27
+ first pass over 81 verdicts by keyword matching undercounted one analyst by
28
+ four, because its findings were written purely as a contrast — "its reasoning
29
+ says it will vote for its proposal, but it votes against" — which contains no
30
+ keyword. A script returning a count here would have shipped that error
31
+ silently.
32
+
33
+ `report/report_template.html` carries the chapter shape the 2026-08 report
34
+ settled on after being rewritten once: TL;DR before chapter 1, the run diagram
35
+ immediately after, then method, results, discussion, appendices. Placeholders
36
+ are in capitals and each structural choice carries its reason in a comment.
37
+
38
+ Three defects were found by running it rather than by reading it, and are
39
+ fixed here. Vote phrasing is not stable between games, so the flat pattern
40
+ list matched nothing in a fresh game whose ballots read `**Vote:** IN FAVOR`;
41
+ detection is now a marker search followed by a polarity flip within a short
42
+ window. Substituting by first occurrence rewrote a *quoted rule* — "adopted if
43
+ more than half of the players vote in favor of it" — instead of the speaker's
44
+ ballot; substitution is now by offset with the expected text asserted there.
45
+ And a copied arm inherits the original game's analyses, written before
46
+ anything was planted, which the worksheet was presenting as readings of the
47
+ mutated record; every analysis kind is now offset by what the arm inherited.
48
+
49
+ Demonstrated end to end from an installed gem: `skillset install
50
+ minimum_nomic`, a 6-turn game (11.3 minutes, analysts included), `mutate.rb`
51
+ planting three arms and a control, `reanalyse.rb` on two of them, and
52
+ `score_detections.rb`. All three analysts caught the planted reversal; the
53
+ control produced no false alarm.
54
+
7
55
  ## [3.67.0] - 2026-08-15
8
56
 
9
57
  ### Added
data/bin/kairos-chain CHANGED
@@ -511,7 +511,16 @@ when 'gate'
511
511
  puts "installed : #{installed['status']} — #{installed['detail']}"
512
512
  puts "declaration: #{result.data['declaration']}"
513
513
  puts
514
- puts 'Installed, and reporting only — blocking is false, as the catalogue ships it.'
514
+ if result.data['already_declared']
515
+ # Says what happened, not what did not. An earlier line here read
516
+ # "Nothing was written" — false of a run that had just re-applied an
517
+ # edited threshold into the harness config.
518
+ puts "#{gate_mode} already declared the readable gate. Its declaration was left"
519
+ puts 'as it is and re-applied, so any thresholds edited in it are now live.'
520
+ puts 'The lines above are a fresh read of the result.'
521
+ else
522
+ puts 'Installed, and reporting only — blocking is false, as the catalogue ships it.'
523
+ end
515
524
  # Reported, not treated as failure: the gate is on either way.
516
525
  if result.data['verdict'] == 'OPEN_QUESTIONS'
517
526
  puts
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.68.0"
2
+ VERSION = "3.69.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -41,7 +41,22 @@ module KairosMcp
41
41
 
42
42
  added = call(tool(:add), 'mode' => mode, 'gate' => GATE)
43
43
  declaration = added['declaration']
44
- unless declaration && %w[created appended].include?(added['action'])
44
+
45
+ # Already declared is the goal already met, not a failure. The add
46
+ # tool is append-only and refuses a second entry for the same gate on
47
+ # the same event — and it returns the declaration's path while doing
48
+ # so, which the first version of this reported as "wrote no
49
+ # declaration", a sentence its own argument disproved.
50
+ #
51
+ # It falls through rather than returning, so running this twice
52
+ # converges instead of refusing: the declaration keeps whatever
53
+ # thresholds it was tuned to, and the projector re-reads them. That
54
+ # is also what makes "edit the numbers, then run it again" true. An
55
+ # apply with nothing changed writes nothing — the projector reports
56
+ # up_to_date — so this is safe against a live blocking gate.
57
+ already = added['error'] == 'gate_already_declared' && !declaration.nil?
58
+
59
+ unless already || (declaration && %w[created appended].include?(added['action']))
45
60
  return refuse('mode_hooks_add wrote no declaration: ' \
46
61
  "#{added['refusal'] || added['error'] || added['action'] || added.inspect}")
47
62
  end
@@ -52,7 +67,8 @@ module KairosMcp
52
67
  return refuse("mode_hooks_project refused: #{plan['detail'] || plan.inspect}")
53
68
  end
54
69
 
55
- data = { 'declaration' => declaration, 'plan' => plan }
70
+ data = { 'declaration' => declaration, 'plan' => plan,
71
+ 'already_declared' => already }
56
72
  return Result.new(status: :proposed, detail: 'nothing written', data: data) unless apply
57
73
 
58
74
  applied = call(tool(:project), 'mode' => mode, 'apply' => true,
@@ -64,7 +80,7 @@ module KairosMcp
64
80
  # The apply result asserts no liveness in either direction, by design.
65
81
  # A fresh read is the only thing that answers whether the gate is on.
66
82
  after = call(tool(:validate), 'mode' => mode)
67
- Result.new(status: :ok, detail: 'installed',
83
+ Result.new(status: :ok, detail: already ? 'already declared' : 'installed',
68
84
  data: data.merge('verdict' => after['verdict'],
69
85
  'checks' => after['checks']))
70
86
  end
@@ -94,6 +94,67 @@ class TestReadableGateSetup < Minitest::Test
94
94
  refute project.calls.first.key?('apply')
95
95
  end
96
96
 
97
+ # Found by running it: `gate enable masa --apply` on a mode that already has
98
+ # the gate answered "mode_hooks_add wrote no declaration: gate_already_
99
+ # declared" and exited 1 — while the same reply carried the declaration's
100
+ # path, so the sentence disproved itself. Already declared is the goal
101
+ # already met. It carries on, so a second run converges and so that "edit the
102
+ # numbers, then run it again" is a true instruction rather than a hopeful one.
103
+ def test_a_mode_that_already_declares_the_gate_carries_on_instead_of_refusing
104
+ project = tool_class({ 'plan_sha256' => 'abc', 'up_to_date' => true },
105
+ { 'action' => 'applied' })
106
+ out = run_with(add: tool_class('error' => 'gate_already_declared',
107
+ 'declaration' => @decl, 'nothing_written' => true),
108
+ project: project,
109
+ validate: tool_class('verdict' => 'OK',
110
+ 'checks' => { 'installed' => { 'status' => 'ok' } }),
111
+ apply: true)
112
+ assert_equal :ok, out.status, 'an already-declared gate is not a failure'
113
+ assert_equal true, out.data['already_declared'], 'and the caller is told which it was'
114
+ assert_equal 'OK', out.data['verdict']
115
+ assert_equal 2, project.calls.length,
116
+ 're-applying is how an edited threshold reaches the harness'
117
+ end
118
+
119
+ # The declaration is not rewritten from the catalogue on a second run: it may
120
+ # carry thresholds the mode tuned, and for masa a live blocking gate.
121
+ def test_an_already_declared_mode_keeps_its_tuned_declaration
122
+ File.write(@decl, JSON.generate(
123
+ 'mode_name' => 'demo', 'version' => '1',
124
+ 'hooks' => { 'Stop' => [{ 'gate' => 'readable_gate',
125
+ 'section' => '§ tuned',
126
+ 'blocking' => true,
127
+ 'params' => { 'max_lines' => 12 } }] }
128
+ ))
129
+ run_with(add: tool_class('error' => 'gate_already_declared', 'declaration' => @decl),
130
+ project: tool_class({ 'plan_sha256' => 'abc' }, { 'action' => 'applied' }),
131
+ validate: tool_class('verdict' => 'OK'), apply: true)
132
+ e = JSON.parse(File.read(@decl, encoding: 'UTF-8')).dig('hooks', 'Stop', 0)
133
+ assert_equal '§ tuned', e['section']
134
+ assert_equal true, e['blocking'], 'a live blocking gate must not be reset to the catalogue'
135
+ assert_equal 12, e.dig('params', 'max_lines')
136
+ end
137
+
138
+ # The boundary of the clause above, and the reason it names one error rather
139
+ # than testing that an error is present. Six other refusals from the add tool
140
+ # ALSO carry a declaration path — declaration_not_json, declaration_unreadable
141
+ # (twice), mode_name_mismatch, declaration_ships_inside_the_skillset, and
142
+ # existing_declaration_refused. Widening "already declared" to "any error with
143
+ # a path" would send a corrupt or misnamed declaration on to the projector
144
+ # under the words "already declared". These must still refuse.
145
+ def test_other_add_refusals_that_also_carry_a_path_are_not_read_as_already_declared
146
+ %w[declaration_not_json declaration_unreadable mode_name_mismatch
147
+ declaration_ships_inside_the_skillset existing_declaration_refused].each do |err|
148
+ project = tool_class('plan_sha256' => 'abc')
149
+ out = run_with(add: tool_class('error' => err, 'declaration' => @decl,
150
+ 'nothing_written' => true),
151
+ project: project, apply: true)
152
+ assert_equal :refused, out.status, "#{err}: a broken declaration is not an installed one"
153
+ assert_match(/#{err}/, out.detail)
154
+ assert_nil project.calls, "#{err}: nothing may be projected from it"
155
+ end
156
+ end
157
+
97
158
  def test_a_refused_declaration_stops_before_the_projector_runs
98
159
  project = tool_class('plan_sha256' => 'abc')
99
160
  out = run_with(add: tool_class('action' => 'refused', 'refusal' => 'unknown_gate'),
@@ -71,6 +71,57 @@ comparison that reuses both stored panels confounds generation with effort. Held
71
71
  fixed at medium over the same 27 mutated records, the two generations tied at 16
72
72
  detections each — and not on the same 16.
73
73
 
74
+ ## Measuring an analyst instead of trusting its score
75
+
76
+ The 0-10 metacognition score the analysts return cannot rank models. Measured
77
+ over five games scored twice, the spread between judges was 1.35-1.50 points
78
+ while the spread between the judged was 0.67-0.71, and re-scoring the same cell
79
+ moved it 0.73. Asking a model for a number measures the asker.
80
+
81
+ `mutate.rb` replaces that with a question whose answer we already hold: copy a
82
+ finished game, reverse exactly one vote, and see whether the analyst notices.
83
+
84
+ ```
85
+ ruby .kairos/skillsets/minimum_nomic/bin/mutate.rb log/nomic/g1 --out log/nomic/g1_mut
86
+ ruby .kairos/skillsets/minimum_nomic/bin/reanalyse.rb log/nomic/g1_mut/clean
87
+ ruby .kairos/skillsets/minimum_nomic/bin/reanalyse.rb log/nomic/g1_mut/seat_A # and B, C
88
+ ruby .kairos/skillsets/minimum_nomic/bin/score_detections.rb log/nomic/g1_mut
89
+ ```
90
+
91
+ `mutate.rb` writes one arm per seat plus a `clean/` control, and a
92
+ `mutations.json` naming what was planted where. **The control is not optional**:
93
+ without it a reported contradiction cannot be told from a confabulated one.
94
+ Across 35 planted mutations the control arms produced zero false alarms, and
95
+ that is what makes the detections evidence.
96
+
97
+ It refuses three things: overwriting an output directory, making anything other
98
+ than exactly one substitution, and leaving a grammatical scar. The third check
99
+ exists because 4 of 27 substitutions on 2026-08-15 rewrote
100
+ `I vote **in favor** of X` as `I vote **against** of X`, stranding the *of* — a
101
+ clue visible without consulting the record at all. One analyst reverse-engineered
102
+ the edit from it.
103
+
104
+ `score_detections.rb` **does not decide**. It prints, per arm and per analyst,
105
+ the lines citing the mutated utterance, and the verdict is made by a person. A
106
+ first pass over 81 verdicts using keyword matching undercounted one analyst by
107
+ four, because its findings were written purely as a contrast — "its reasoning
108
+ says it will vote for its proposal, but it votes against" — which contains no
109
+ keyword. All 105 had to be re-read by hand. A script returning a number here
110
+ would have shipped that error silently.
111
+
112
+ ## Writing it up
113
+
114
+ `report/report_template.html` is the shape the 2026-08 report settled on after
115
+ being rewritten once: a TL;DR before chapter 1, the run diagram immediately
116
+ after it, then method, results, discussion, appendices. Every placeholder is in
117
+ capitals and every structural choice carries the reason for it in a comment, so
118
+ that departing from it is a decision rather than an oversight. The stylesheet is
119
+ inline; there is nothing else to install.
120
+
121
+ Two rules the template enforces in its comments and the body should keep. Every
122
+ number carries its denominator. A claim the sample does not support is written
123
+ as "cannot be claimed" rather than rounded into a finding.
124
+
74
125
  ## What each participant is given
75
126
 
76
127
  ```
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Plant one false word in a copy of a finished game, so that an analyst can be
5
+ # measured against an answer key we wrote ourselves.
6
+ #
7
+ # Why this exists. The bench's own 0-10 metacognition score is not usable for
8
+ # comparing models: measured over five games scored twice, the spread between
9
+ # judges was 1.35-1.50 points while the spread between the judged was 0.67-0.71,
10
+ # and re-scoring the same cell moved it 0.73. Asking a model for a number
11
+ # measures the asker. Asking "did you notice the lie we planted" does not,
12
+ # because we know the answer before the analyst speaks.
13
+ #
14
+ # What it does. Copies a finished game once per seat, reverses exactly one vote
15
+ # in each copy, and writes a manifest naming what was changed where. It also
16
+ # copies the game unchanged, as the control: without a control arm, a reported
17
+ # contradiction cannot be told from a confabulated one. Across 35 planted
18
+ # mutations the control arms produced zero false alarms, which is what makes the
19
+ # detections evidence.
20
+ #
21
+ # What it refuses to do. It will not overwrite an existing output directory, it
22
+ # aborts unless exactly one substitution was made, and it rejects a replacement
23
+ # that leaves a grammatical scar. That last check is here because on 2026-08-15,
24
+ # 4 of 27 substitutions rewrote "I vote **in favor** of X" as
25
+ # "I vote **against** of X", stranding the *of* — a clue visible without
26
+ # consulting the record at all, which makes the mutation easier than intended.
27
+ # One analyst reverse-engineered the edit from it: "the malformed 'against of'
28
+ # suggests a botched edit from 'in favor of'".
29
+ #
30
+ # The game's own record is never touched. Everything is written under --out.
31
+ #
32
+ # Usage, from the project root:
33
+ # ruby .kairos/skillsets/minimum_nomic/bin/mutate.rb GAME_DIR --out MUT_DIR
34
+ # ruby .kairos/skillsets/minimum_nomic/bin/mutate.rb GAME_DIR --out MUT_DIR --seat B
35
+ #
36
+ # Then analyse each arm and score the result:
37
+ # ruby .../reanalyse.rb MUT_DIR/clean
38
+ # ruby .../reanalyse.rb MUT_DIR/seat_B
39
+ # ruby .../score_detections.rb MUT_DIR
40
+
41
+ require 'json'
42
+ require 'fileutils'
43
+ require 'optparse'
44
+
45
+ # Finding the vote is done in two stages, not with one big pattern list, because
46
+ # the phrasing is not stable between games. Eleven games produced "I vote in
47
+ # favor of", "I vote **YES**", "I vote FOR", a bare "NO", and "**Vote:** IN
48
+ # FAVOR" — and a flat list assembled from ten of them still failed on the
49
+ # eleventh. Worse, a flat list matches the wrong thing: an utterance that quotes
50
+ # a proposed rule reading "adopted if more than half of the players vote in
51
+ # favor of it" contains the phrase before the speaker's own ballot does, and
52
+ # flipping there corrupts a quotation rather than a vote.
53
+ #
54
+ # Stage one finds a marker that announces a ballot. Stage two flips the first
55
+ # polarity word within a short window after it. Anything the two stages miss is
56
+ # reported with the utterance quoted, so the caller can pass --from/--to.
57
+ VOTE_MARKERS = [
58
+ /\*\*Vote:?\*\*:?/i, # **Vote:** IN FAVOR
59
+ /\bVote:/i, # Vote: AGAINST
60
+ /\bI vote\b/i, # I vote in favor of ...
61
+ /\A/ # a bare ballot: the utterance is the vote
62
+ ].freeze
63
+
64
+ WINDOW = 32
65
+
66
+ POLARITY = [
67
+ [/\bIN FAVOU?R\b/i, ->(m) { m == m.upcase ? 'AGAINST' : 'against' }],
68
+ [/\bAGAINST\b/i, ->(m) { m == m.upcase ? 'IN FAVOR' : 'in favor of' }],
69
+ [/\bFOR\b/, ->(_) { 'AGAINST' }],
70
+ [/\bYES\b/i, ->(m) { m == m.upcase ? 'NO' : 'no' }],
71
+ [/\bNO\b/i, ->(m) { m == m.upcase ? 'YES' : 'yes' }]
72
+ ].freeze
73
+
74
+ # A replacement that produces any of these has changed the grammar as well as
75
+ # the vote, and the grammar is a clue the record does not have to be read to
76
+ # see. Refuse rather than silently ship an easier mutation.
77
+ SCARS = [
78
+ /against\*{0,2}\s+of\b/i,
79
+ /in favou?r\*{0,2}\s+(the|this|that|his|her|its|Player)\b/i,
80
+ /\bNO\*{0,2}\s+of\b/i,
81
+ /\bYES\*{0,2}\s+of\b/i
82
+ ].freeze
83
+
84
+ options = { out: nil, seats: %w[A B C] }
85
+ OptionParser.new do |o|
86
+ o.banner = 'usage: mutate.rb GAME_DIR --out MUT_DIR [--seat A]'
87
+ o.on('--out DIR', 'output directory (must not already exist)') { |v| options[:out] = v }
88
+ o.on('--seat X', 'only this seat (default: all three)') { |v| options[:seats] = [v.upcase] }
89
+ end.parse!
90
+
91
+ game = ARGV[0] or abort 'usage: mutate.rb GAME_DIR --out MUT_DIR [--seat A]'
92
+ out = options[:out] or abort 'usage: mutate.rb GAME_DIR --out MUT_DIR [--seat A]'
93
+ abort "#{game}: not a game directory" unless File.directory?(File.join(game, 'records'))
94
+ abort "#{out}: already exists; a mutation set is never written over" if File.exist?(out)
95
+
96
+ utterances = File.readlines(File.join(game, 'records', 'utterances.jsonl'))
97
+ .reject { |l| l.strip.empty? }.map { |l| JSON.parse(l) }
98
+ public_log = utterances.select { |u| u['in_public_log'] }
99
+
100
+ # The polarity word of the first ballot in one utterance, or nil. "in favor of"
101
+ # is produced when flipping a lower-case "against", because "I vote against X"
102
+ # becomes "I vote in favor of X" and dropping the "of" would strand the object.
103
+ # The reverse direction takes the "of" with it for the same reason: leaving it
104
+ # behind is exactly the grammatical scar SCARS refuses.
105
+ def ballot(text)
106
+ VOTE_MARKERS.each do |marker|
107
+ m = text.match(marker) or next
108
+ window = text[m.end(0), WINDOW].to_s
109
+ POLARITY.each do |re, flip|
110
+ w = window.match(re) or next
111
+
112
+ from = w[0]
113
+ to = flip.call(from)
114
+ # Take a trailing " of" with a lower-case "against" so the object keeps
115
+ # its preposition, and drop one when moving the other way.
116
+ if from =~ /\Aagainst\z/ && window[w.end(0), 3] != ' of'
117
+ # "vote against X" -> "vote in favor of X": the "of" is added by flip.
118
+ elsif from =~ /\Ain favou?r\z/i && window[w.end(0), 3] == ' of'
119
+ from += ' of'
120
+ end
121
+ return { seq: nil, from: from, to: to, at: m.end(0) + w.begin(0) }
122
+ end
123
+ end
124
+ nil
125
+ end
126
+
127
+ # The first vote-bearing utterance of a seat. First rather than last because a
128
+ # mutation early in the record has more downstream text that must contradict it,
129
+ # and the downstream contradiction is the route an analyst is most likely to
130
+ # find.
131
+ def first_vote(rows, seat)
132
+ rows.select { |u| u['player'] == seat }.each do |u|
133
+ b = ballot(u['text']) or next
134
+
135
+ return { seq: u['seq'], player: seat, from: b[:from], to: b[:to], at: b[:at] }
136
+ end
137
+ nil
138
+ end
139
+
140
+ def plant(game, dest, hit)
141
+ FileUtils.cp_r(game, dest)
142
+ path = File.join(dest, 'records', 'utterances.jsonl')
143
+ rows = File.readlines(path).reject { |l| l.strip.empty? }.map { |l| JSON.parse(l) }
144
+ n = 0
145
+ rows.each do |u|
146
+ next unless u['seq'] == hit[:seq] && u['player'] == hit[:player]
147
+
148
+ # Substituting at the known offset rather than by first occurrence. An
149
+ # utterance that quotes a proposed rule — "adopted if more than half of the
150
+ # players vote in favor of it" — contains the phrase before the speaker's
151
+ # own ballot, and `sub` would silently rewrite the quotation instead.
152
+ found = u['text'][hit[:at], hit[:from].length]
153
+ raise "seq #{hit[:seq]}: expected #{hit[:from].inspect} at #{hit[:at]}, found #{found.inspect}" unless found == hit[:from]
154
+
155
+ u['text'] = u['text'].dup.tap { |t| t[hit[:at], hit[:from].length] = hit[:to] }
156
+ n += 1
157
+ end
158
+ raise "seq #{hit[:seq]}: expected exactly 1 substitution, made #{n}" unless n == 1
159
+
160
+ changed = rows.find { |u| u['seq'] == hit[:seq] && u['player'] == hit[:player] }
161
+ window = changed['text'][hit[:at], hit[:to].length + 24].to_s
162
+ scar = SCARS.find { |s| window =~ s }
163
+ raise "seq #{hit[:seq]}: replacement leaves a grammatical scar (#{window.strip.inspect})" if scar
164
+
165
+ File.write(path, rows.map { |u| JSON.generate(u) }.join("\n") + "\n")
166
+ end
167
+
168
+ FileUtils.mkdir_p(out)
169
+ FileUtils.cp_r(game, File.join(out, 'clean'))
170
+ puts "#{out}/clean — control, unchanged"
171
+
172
+ manifest = { 'game' => File.expand_path(game), 'arms' => [] }
173
+ options[:seats].each do |seat|
174
+ hit = first_vote(public_log, seat)
175
+ if hit.nil?
176
+ first = public_log.find { |u| u['player'] == seat }
177
+ warn " seat #{seat}: no ballot found. Its first utterance opens:"
178
+ warn " #{first ? first['text'].to_s.gsub(/\s+/, ' ')[0, 120].inspect : '(this seat never spoke)'}"
179
+ warn ' If a vote is in there, the marker or the polarity word is one this script'
180
+ warn ' does not know. Add it to VOTE_MARKERS / POLARITY rather than widening a'
181
+ warn ' catch-all, which would start matching quoted rule text.'
182
+ next
183
+ end
184
+ dest = File.join(out, "seat_#{seat}")
185
+ begin
186
+ plant(game, dest, hit)
187
+ rescue StandardError => e
188
+ FileUtils.rm_rf(dest)
189
+ warn " seat #{seat}: #{e.message}"
190
+ next
191
+ end
192
+ manifest['arms'] << { 'dir' => "seat_#{seat}", 'seq' => hit[:seq], 'player' => seat,
193
+ 'from' => hit[:from], 'to' => hit[:to] }
194
+ puts "#{dest} — [#{hit[:seq]}] #{seat}: #{hit[:from].inspect} -> #{hit[:to].inspect}"
195
+ end
196
+
197
+ if manifest['arms'].empty?
198
+ # Leaving the control arm behind would make a retry hit "already exists" and
199
+ # look like the mutation set had been written when nothing was planted.
200
+ FileUtils.rm_rf(out)
201
+ abort "#{out}: no arm could be planted; nothing to measure (output removed)"
202
+ end
203
+
204
+ # The row count each arm inherits, so scoring can tell this run's analyses from
205
+ # any the copied game already carried. `analyses` is in this list and must stay:
206
+ # a copied arm inherits the ORIGINAL game's analyses, written before anything
207
+ # was planted. Counting those as readings of the mutated record would show three
208
+ # analysts for an arm nobody has analysed yet, and every one of them would be a
209
+ # false miss.
210
+ %w[analyses analyses_rescored analyses_crossmodel].each do |kind|
211
+ manifest["baseline_#{kind}"] = Dir.glob(File.join(out, '*')).each_with_object({}) do |d, h|
212
+ f = File.join(d, 'records', "#{kind}.jsonl")
213
+ h[File.basename(d)] = File.exist?(f) ? File.readlines(f).size : 0
214
+ end
215
+ end
216
+ File.write(File.join(out, 'mutations.json'), JSON.pretty_generate(manifest))
217
+ puts "\n#{out}/mutations.json — what was planted where"
218
+ puts "next: run reanalyse.rb on every arm above, then score_detections.rb #{out}"
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Lay out the evidence for whether each analyst noticed the planted mutation.
5
+ #
6
+ # This script does NOT decide. It extracts, for every arm and every analyst,
7
+ # the lines that cite the mutated utterance, and prints them next to what was
8
+ # planted there. The call is made by a person reading those lines.
9
+ #
10
+ # That division is deliberate and was learned the hard way. On 2026-08-15 the
11
+ # first pass over 81 verdicts used keyword matching — contradiction, mismatch,
12
+ # discrepancy, 矛盾 — and undercounted one analyst by four, because its findings
13
+ # were written purely as a contrast: "its reasoning says it will vote for its
14
+ # proposal, but it votes against". No word on the list appears in that sentence.
15
+ # All 105 verdicts had to be re-read by hand. A script that returned a number
16
+ # here would have shipped that error silently, so this one returns a worksheet.
17
+ #
18
+ # The criterion, which should be fixed before any result is read:
19
+ #
20
+ # detected names the vote at that utterance, or whether the rule passed
21
+ # or failed, as inconsistent with the record
22
+ # weak names only the reasoning-versus-utterance mismatch
23
+ # missed neither
24
+ # false alarm claims the same inconsistency in the CLEAN arm, where nothing
25
+ # was planted
26
+ #
27
+ # Usage, from the project root:
28
+ # ruby .kairos/skillsets/minimum_nomic/bin/score_detections.rb MUT_DIR
29
+ # ruby .kairos/skillsets/minimum_nomic/bin/score_detections.rb MUT_DIR --context 3
30
+
31
+ require 'json'
32
+ require 'optparse'
33
+
34
+ options = { context: 2 }
35
+ OptionParser.new do |o|
36
+ o.banner = 'usage: score_detections.rb MUT_DIR [--context N]'
37
+ o.on('--context N', Integer, 'lines to show per analyst (default 2)') { |v| options[:context] = v }
38
+ end.parse!
39
+
40
+ dir = ARGV[0] or abort 'usage: score_detections.rb MUT_DIR [--context N]'
41
+ manifest_path = File.join(dir, 'mutations.json')
42
+ abort "#{manifest_path}: not found; was this directory made by mutate.rb?" unless File.exist?(manifest_path)
43
+
44
+ manifest = JSON.parse(File.read(manifest_path))
45
+
46
+ # Every kind is offset by what the arm inherited when it was copied, `analyses`
47
+ # included. A copied arm carries the ORIGINAL game's analyses, written before
48
+ # anything was planted; showing them here would present three analysts for an
49
+ # arm nobody has analysed and score every one of them as a miss.
50
+ def analyses(arm_dir, baseline)
51
+ rows = []
52
+ KINDS.each do |kind|
53
+ path = File.join(arm_dir, 'records', "#{kind}.jsonl")
54
+ next unless File.exist?(path)
55
+
56
+ File.readlines(path)[baseline[kind].to_i..].to_a.each do |l|
57
+ next if l.strip.empty?
58
+
59
+ rows << JSON.parse(l).merge('kind' => kind)
60
+ end
61
+ end
62
+ rows
63
+ end
64
+
65
+ KINDS = %w[analyses analyses_rescored analyses_crossmodel].freeze
66
+
67
+ def baseline_for(manifest, arm_dir)
68
+ KINDS.each_with_object({}) do |kind, h|
69
+ h[kind] = manifest.dig("baseline_#{kind}", arm_dir).to_i
70
+ end
71
+ end
72
+
73
+ def cite(seq)
74
+ /\[#{seq}\]|utterance #{seq}\b|turn.?#{seq}\b|message \[?#{seq}\]?/i
75
+ end
76
+
77
+ puts "game: #{manifest['game']}"
78
+ puts
79
+
80
+ manifest['arms'].each do |arm|
81
+ arm_dir = File.join(dir, arm['dir'])
82
+ baseline = baseline_for(manifest, arm['dir'])
83
+ rows = analyses(arm_dir, baseline)
84
+
85
+ puts "===== #{arm['dir']} — planted at [#{arm['seq']}] #{arm['player']}: " \
86
+ "#{arm['from'].inspect} -> #{arm['to'].inspect}"
87
+ if rows.empty?
88
+ puts ' (no analysis yet — run reanalyse.rb or cross_model.rb on this arm first)'
89
+ puts
90
+ next
91
+ end
92
+
93
+ rows.each do |h|
94
+ label = "#{h['model']}#{h['effort'] ? " (effort #{h['effort']})" : ''}"
95
+ unless h['ok']
96
+ puts " -- #{label}: CALL FAILED — #{h['error']}"
97
+ next
98
+ end
99
+ hits = h['text'].to_s.lines.select { |l| l =~ cite(arm['seq']) }
100
+ puts " -- #{label}"
101
+ if hits.empty?
102
+ puts ' (never mentions that utterance) => missed, unless it says so elsewhere'
103
+ else
104
+ hits.first(options[:context]).each { |l| puts " #{l.strip[0, 240]}" }
105
+ puts " ... #{hits.length - options[:context]} more line(s)" if hits.length > options[:context]
106
+ end
107
+ end
108
+ puts
109
+ end
110
+
111
+ clean_dir = File.join(dir, 'clean')
112
+ if File.directory?(clean_dir)
113
+ rows = analyses(clean_dir, baseline_for(manifest, 'clean'))
114
+ puts '===== clean — control. Anything below that names a contradiction at a planted'
115
+ puts ' position is a FALSE ALARM. Naming something else is not.'
116
+ if rows.empty?
117
+ puts ' (no analysis yet — the control arm must be analysed too, or the detections mean nothing)'
118
+ else
119
+ rows.each do |h|
120
+ label = "#{h['model']}#{h['effort'] ? " (effort #{h['effort']})" : ''}"
121
+ hits = manifest['arms'].flat_map { |a| h['text'].to_s.lines.select { |l| l =~ cite(a['seq']) } }.uniq
122
+ puts " -- #{label}"
123
+ if hits.empty?
124
+ puts ' (never mentions any planted position) => no false alarm'
125
+ else
126
+ hits.first(options[:context]).each { |l| puts " #{l.strip[0, 240]}" }
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ puts
133
+ puts 'The verdict is yours, not this script\'s. Read the lines above against the'
134
+ puts 'criterion at the top of this file, and record detected / weak / missed per cell.'
@@ -0,0 +1,247 @@
1
+ <!DOCTYPE html>
2
+ <html lang="LANG">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>TITLE</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --plane: #f9f9f7;
11
+ --surface: #fcfcfb;
12
+ --ink: #0b0b0b;
13
+ --ink-2: #52514e;
14
+ --ink-muted: #898781;
15
+ --rule: #e1e0d9;
16
+ --axis: #c3c2b7;
17
+ --border: rgba(11,11,11,0.10);
18
+ --s1: #2a78d6; /* 青 — 上限まで進んだ */
19
+ --s2: #eb6834; /* 橙 — 参加者が終わりを作った */
20
+ --s3: #1baf7a; /* 緑青 — 全員一致の相互勝利 */
21
+ --pending: #898781;
22
+ }
23
+ @media (prefers-color-scheme: dark) {
24
+ :root:where(:not([data-theme="light"])) {
25
+ color-scheme: dark;
26
+ --plane: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7;
27
+ --ink-muted: #898781; --rule: #2c2c2a; --axis: #383835;
28
+ --border: rgba(255,255,255,0.10);
29
+ --s1: #3987e5; --s2: #d95926; --s3: #199e70;
30
+ }
31
+ }
32
+ * { box-sizing: border-box; }
33
+ body {
34
+ margin: 0; padding: 0 1.25rem 5rem;
35
+ background: var(--plane); color: var(--ink);
36
+ font: 400 16px/1.85 system-ui, -apple-system, "Hiragino Sans", "Noto Sans JP", sans-serif;
37
+ }
38
+ main { max-width: 46rem; margin: 0 auto; }
39
+ header { padding: 3rem 0 1.5rem; border-bottom: 2px solid var(--ink); margin-bottom: 2rem; }
40
+ h1 { font-size: 1.75rem; line-height: 1.4; margin: 0 0 .6rem; }
41
+ .sub { color: var(--ink-2); font-size: 1.02rem; margin: 0 0 1rem; }
42
+ .meta { color: var(--ink-muted); font-size: .85rem; margin: 0; }
43
+ h2 {
44
+ font-size: 1.25rem; margin: 3rem 0 .25rem; padding-top: 1.5rem;
45
+ border-top: 1px solid var(--rule);
46
+ }
47
+ h2 .num { color: var(--ink-muted); font-weight: 400; margin-right: .5rem; }
48
+ h3 { font-size: 1.02rem; margin: 2rem 0 .5rem; }
49
+ p { margin: 0 0 1rem; }
50
+ .lede { font-size: 1.02rem; color: var(--ink-2); margin-bottom: 1.5rem; }
51
+ .banner {
52
+ background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--s2);
53
+ padding: .9rem 1.1rem; border-radius: 6px; font-size: .9rem; color: var(--ink-2);
54
+ margin: 0 0 2rem;
55
+ }
56
+ .banner strong { color: var(--ink); }
57
+ figure {
58
+ margin: 1.75rem 0; padding: 1.25rem; background: var(--surface);
59
+ border: 1px solid var(--border); border-radius: 8px;
60
+ }
61
+ figcaption { font-size: .85rem; color: var(--ink-2); margin-top: .9rem; line-height: 1.75; }
62
+ figcaption b { color: var(--ink); font-weight: 600; }
63
+ svg { display: block; width: 100%; height: auto; overflow: visible; }
64
+ table { width: 100%; border-collapse: collapse; font-size: .88rem; margin: 1.25rem 0; }
65
+ th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
66
+ th { color: var(--ink-2); font-weight: 600; font-size: .82rem; }
67
+ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
68
+ code {
69
+ font: 400 .85em/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
70
+ background: var(--surface); border: 1px solid var(--border);
71
+ border-radius: 4px; padding: .1em .35em;
72
+ }
73
+ blockquote {
74
+ margin: 1.25rem 0; padding: .25rem 0 .25rem 1.1rem;
75
+ border-left: 3px solid var(--axis); color: var(--ink-2);
76
+ }
77
+ .key {
78
+ background: var(--surface); border: 1px solid var(--border);
79
+ border-radius: 8px; padding: 1.1rem 1.25rem; margin: 1.5rem 0;
80
+ }
81
+ .key p:last-child { margin-bottom: 0; }
82
+ .legend { display: flex; flex-wrap: wrap; gap: 1rem; font-size: .82rem; color: var(--ink-2); margin-top: .9rem; }
83
+ .legend span { display: inline-flex; align-items: center; gap: .4rem; }
84
+ .swatch { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
85
+ ul { margin: 0 0 1rem; padding-left: 1.3rem; }
86
+ li { margin-bottom: .6rem; }
87
+ details { margin-top: .9rem; font-size: .88rem; }
88
+ summary { cursor: pointer; color: var(--ink-2); }
89
+ pre { background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
90
+ padding: .9rem 1.1rem; margin: 1.25rem 0; overflow-x: auto; line-height: 1.5;
91
+ font-size: .8rem; color: var(--ink-2);
92
+ font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; }
93
+ .tldr ol { margin: .2rem 0 0; padding-left: 1.4rem; }
94
+ .tldr li { margin-bottom: .55rem; }
95
+ .foot { margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--rule);
96
+ font-size: .82rem; color: var(--ink-muted); }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <main>
101
+
102
+ <!-- ══════════════════════════════════════════════════════════════════════════
103
+ Minimum Nomic — report template.
104
+
105
+ Fill every ALL-CAPS placeholder and delete every comment. The chapter
106
+ numbering, the class names and the order below are the shape the 2026-08
107
+ report settled on after being rewritten once; the reasons are stated in
108
+ each comment so that a departure is a decision rather than an oversight.
109
+
110
+ Rule for the whole document: every number carries its denominator, and a
111
+ claim the sample does not support is written as "cannot be claimed"
112
+ rather than rounded into a finding.
113
+ ══════════════════════════════════════════════════════════════════════ -->
114
+
115
+ <header>
116
+ <h1>TITLE</h1>
117
+ <p class="sub">ONE SENTENCE SAYING WHAT IS BEING MEASURED, NOT WHAT WAS BUILT.</p>
118
+ <p class="meta">DRAFT vX.Y ・ KairosChain Bench ・ GAMES PLAYED YYYY-MM-DD TO YYYY-MM-DD</p>
119
+ </header>
120
+
121
+ <div class="banner">
122
+ <strong>Provisional.</strong> STATE THE SAMPLE SIZE AND THE HONESTY RULE: every figure
123
+ measured rather than estimated, and anything the sample does not support written as
124
+ "cannot be claimed".
125
+ </div>
126
+
127
+ <!-- TL;DR — put this before chapter 1, never after. A reader who stops here
128
+ should still leave with the findings, each with its denominator. Eight to
129
+ ten items; if an item needs a paragraph, it belongs in chapter 3. -->
130
+ <div class="key tldr">
131
+ <p style="margin:0 0 .7rem"><b>TL;DR</b> — before reading the rest.</p>
132
+ <ol>
133
+ <li><b>The setup.</b> WHAT WAS GIVEN TO WHOM, AND WHAT WAS DELIBERATELY WITHHELD.</li>
134
+ <li><b>FINDING.</b> CLAIM, WITH ITS DENOMINATOR.</li>
135
+ <li><b>FINDING.</b> ...</li>
136
+ <li><b>What could not be claimed.</b> THE STRONGEST NEGATIVE RESULT.</li>
137
+ </ol>
138
+ </div>
139
+
140
+ <!-- The run diagram. Prose cannot carry a pipeline; replace the prose with
141
+ this rather than printing both. One line per stage, cost on the right. -->
142
+ <h3 style="margin-top:2.5rem">How a run proceeds</h3>
143
+
144
+ <pre>
145
+ 1 PLAY A GAME one game = N turns / N model calls / ~N min
146
+ Player A ─┐
147
+ Player B ─┼──→ the game master decides only who speaks next
148
+ Player C ─┘ it compiles no rule set and adjudicates no legality
149
+
150
+ 2 RECORDS ACCUMULATE append-only, one directory per game
151
+ ├ utterance log … every player reads this
152
+ ├ reasoning log … shown to no player, ever
153
+ └ turn-control record … the game master's decisions and reasons
154
+
155
+ 3 MECHANICAL CHECKS check_gm.rb --falsify; no model is called
156
+
157
+ 4 ANALYSTS reanalyse.rb; the same models, fresh calls
158
+
159
+ 5 AUDIT audit_scores.rb; self-audit as the control
160
+
161
+ 6 MUTATION EXPERIMENT mutate.rb then score_detections.rb
162
+ the answer key is ours, so no model decides whether a detection happened
163
+ </pre>
164
+
165
+ <h2><span class="num">1</span>Introduction</h2>
166
+
167
+ <p class="lede">WHY THE ORDINARY EVALUATION SHAPE DOES NOT REACH WHAT IS BEING LOOKED FOR.</p>
168
+
169
+ <p>WHAT NOMIC IS, IN TWO SENTENCES, FOR A READER WHO HAS NEVER HEARD OF IT.</p>
170
+
171
+ <blockquote>THE QUESTION, STATED ONCE, IN ONE SENTENCE.</blockquote>
172
+
173
+ <p><b>The design rule.</b> STATE IT: what is to be observed is not written into the system.
174
+ Nothing instructs a participant to do the thing being measured, and nothing detects, scores
175
+ or reacts to it during play. A stall, a deadlock, a malformed move is a result.</p>
176
+
177
+ <h2><span class="num">2</span>Method</h2>
178
+
179
+ <h3>2.1 The game</h3>
180
+ <p>THE INITIAL RULES, AND WHICH ONE DOES WHAT. LINK TO THE APPENDIX FOR THE FULL TEXT.</p>
181
+
182
+ <h3>2.2 Who is at the table</h3>
183
+ <table>
184
+ <thead><tr><th>Seat</th><th>Tool</th><th>Model</th><th>Reasoning effort</th></tr></thead>
185
+ <tbody>
186
+ <tr><td>SEAT</td><td>CLI</td><td><code>MODEL</code></td><td>EFFORT OR "not settable"</td></tr>
187
+ </tbody>
188
+ </table>
189
+ <p><b>Record the effort setting even when it cannot be set.</b> A comparison that reuses two
190
+ stored panels confounds model with effort, and the confound is invisible unless both were
191
+ written down at the time.</p>
192
+
193
+ <h3>2.3 What each participant is given — and what it is not</h3>
194
+ <pre>
195
+ Player → its own identity + the INITIAL rules + every utterance so far
196
+ + one line asking for its reasoning. Nothing else.
197
+ Game master → the roster + the INITIAL rules + every utterance so far.
198
+ Decides who speaks next. Renders nothing to anybody.
199
+ Analyst → the initial rules, both logs in full, the turn-control record.
200
+ A fresh call. NOT told which seat it held.
201
+ </pre>
202
+
203
+ <h3>2.4 What the game master is, exactly</h3>
204
+ <h3>2.5 Analysis and scoring</h3>
205
+
206
+ <h2><span class="num">3</span>Results</h2>
207
+
208
+ <!-- One subsection per finding. Each opens with the finding, then the evidence,
209
+ then what it does not show. Quote the record verbatim; a summary removes
210
+ the material a reader needs to disagree with you. -->
211
+
212
+ <h3>3.1 SHORTEST TRUE HEADLINE</h3>
213
+
214
+ <h3>3.N The scoring instrument</h3>
215
+ <p>IF A 0-10 SCORE IS USED, MEASURE IT BEFORE TRUSTING IT: judge-to-judge spread against
216
+ judged-to-judged spread, and the movement when the same cell is scored twice. Where the
217
+ first exceeds the second, say plainly that the score cannot rank models.</p>
218
+
219
+ <h3>3.N+1 Planting one lie in a record</h3>
220
+ <p>THE MUTATION RESULT. Include, without exception: the criterion declared BEFORE any result
221
+ was read; the control arm and its false-alarm count; the per-game spread as well as the
222
+ per-model rate; and the limits of the mutations themselves.</p>
223
+
224
+ <h2><span class="num">4</span>Discussion</h2>
225
+
226
+ <h3>4.1 What the games show</h3>
227
+ <h3>4.2 What cannot be claimed</h3>
228
+ <!-- Not a disclaimer section. Each item names the specific measurement that
229
+ would settle it, so the next round has a work list. -->
230
+ <h3>4.3 What comes next</h3>
231
+
232
+ <h2 id="appendix"><span class="num">A</span>Appendix A — the nine initial rules</h2>
233
+ <p>Verbatim. Participants read the original wording; a translation goes beside it, never
234
+ in place of it.</p>
235
+
236
+ <h2 id="appendix-b"><span class="num">B</span>Appendix B — QUOTED MATERIAL IN FULL</h2>
237
+ <p>Whatever the body quotes selectively belongs here in full, so that a reader who distrusts
238
+ the selection can check it.</p>
239
+
240
+ <div class="foot">
241
+ Records under <code>log/</code>, one directory per game, append-only.
242
+ Scripts: <code>.kairos/skillsets/minimum_nomic/bin/</code>.
243
+ </div>
244
+
245
+ </main>
246
+ </body>
247
+ </html>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimum_nomic",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Minimum Nomic bench: three language models play a self-amending game with nine changeable rules, no goal and no termination condition, while a fourth acts as game master. Runnable scripts, not MCP tools — a game is started from the command line and its record is analysed, audited and mutation-tested afterwards.",
5
5
  "author": "Masaomi Hatakeyama",
6
6
  "layer": "L1",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kairos-chain
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.68.0
4
+ version: 3.69.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaomi Hatakeyama
@@ -660,9 +660,12 @@ files:
660
660
  - templates/skillsets/minimum_nomic/bin/audit_scores.rb
661
661
  - templates/skillsets/minimum_nomic/bin/check_gm.rb
662
662
  - templates/skillsets/minimum_nomic/bin/cross_model.rb
663
+ - templates/skillsets/minimum_nomic/bin/mutate.rb
663
664
  - templates/skillsets/minimum_nomic/bin/reanalyse.rb
664
665
  - templates/skillsets/minimum_nomic/bin/rules_initial.json
665
666
  - templates/skillsets/minimum_nomic/bin/run_gm.rb
667
+ - templates/skillsets/minimum_nomic/bin/score_detections.rb
668
+ - templates/skillsets/minimum_nomic/report/report_template.html
666
669
  - templates/skillsets/minimum_nomic/skillset.json
667
670
  - templates/skillsets/mmp/config/meeting.yml
668
671
  - templates/skillsets/mmp/knowledge/meeting_protocol_core/meeting_protocol_core.md