kairos-chain 3.91.0 → 3.92.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: 8edb08c26be7f068db03e377e1f9d48ba67135b233dfa16f0fab9e7126364bca
4
- data.tar.gz: 81557a33e8214065bf3e692948b58e3655e0758f5b95e13ae19c9bcc67c4cf17
3
+ metadata.gz: 3cea3a83409113bb9513f6da35f8e202ed84762f726997b55d4c0807e3764565
4
+ data.tar.gz: e88354debd6836834ffcf28b905f361b4e9d0d154d22d68f6112a20076fc45c1
5
5
  SHA512:
6
- metadata.gz: a6fbd7b1e9a1a347f941d292c6b0b55c8801b8ca609503281721d175a7567f27e2b20791ded6b2404eefbc3c6d7d8e84b16f3136fa0faffdad39d3bed574b044
7
- data.tar.gz: fcff23454d07a3720ddb982a421137b292e9844228276e445181062ec64ac06025bf4cc4d0afe6c5be4202d889f1b8ddacf10eaa3859cc87ee2d998e627789d4
6
+ metadata.gz: 811debe565ba6b2815b24860edc7d43a1cec1f3c0da9a8b4d4aed0c52d88052ddbd8f53b544575b253860da2ad669d197f9c5a105ffdc08a8d8e9fb002aade2b
7
+ data.tar.gz: 9682dc5d58a0264960323374db889ad4074e0c063685a0062866e811ec04eb1c91a94fef720a4260b58f69d26f12f9640087213c713dc8c9c1438fb0f836089f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,59 @@ 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.92.0] - 2026-10-13
8
+
9
+ ### Added — agent: judgment precedents the shadow judge reads, distilled from your answers
10
+
11
+ The shadow judge is a fresh model at every point, so it does not improve with
12
+ use. It can now also read an instance L1 entry, `agent_judgment_precedents`,
13
+ that generalizes your terminal answers and their reasons. The entry is drafted
14
+ by a separate model and reaches the judge only after you adopt it at the
15
+ terminal. **The judge's answer still changes nothing**; every row stays
16
+ `shadow`.
17
+
18
+ - **What the driver records**: when an answer at either point commits, the
19
+ plans it covered are filed in the session (`point_facts.jsonl`), fixed at
20
+ the moment you answered (an approve at an autonomous checkpoint never
21
+ covers the cycles it starts): each step's tool name and effect where the
22
+ act-route table classified it, counts elsewhere. A step the table does not classify is never named. The ruling
23
+ carries the digest of the filed line.
24
+ - **Drafting**: after ten new answers you typed at the terminal with a reason,
25
+ a separate process (`bin/agent_distill.rb`, `claude-opus-5-5` at `xhigh`
26
+ through `llm_call`, sandboxed) drafts the entry again from all your attested
27
+ answers, their terminal reasons, the filed facts and your edits to earlier
28
+ drafts. It never reads the agent's text, the judge's verdicts or an earlier
29
+ entry. Every statement names the answers it rests on, all at one point,
30
+ and its count and that point's totals are computed by code. It runs only while the delegation table is in force, one
31
+ at a time, off the answer path; a failure waits for ten more answers. The
32
+ draft is recorded on the chain (`agent_distill_draft`).
33
+ - **Adoption**: edit the draft's `.edit.md` copy under
34
+ `.kairos/storage/agent_distill/drafts/`, then
35
+ `ruby .kairos/skillsets/agent/bin/agent_rule.rb adopt`. It checks the draft
36
+ against its chain record, refuses while any session is mid-run (checked
37
+ again after you confirm), shows your changes and the difference from the
38
+ entry in force, asks for the nonce, and records one delegation ruling that
39
+ pins exactly the bytes adopted; on any failure or interrupt the previous
40
+ entry is put back. When the entry has left its pin (an interrupted
41
+ adoption, a hand edit), adoption is also the way back. `agent_rule.rb
42
+ distill` drafts now, whatever the count; `agent_rule.rb status` shows the
43
+ count, the last run and waiting drafts.
44
+ - **Only adoption pins new precedents**: any other ruling of the delegation
45
+ table refuses while the entry differs from its last pin.
46
+ - **Counting per version**: `agent_rule.rb shadow` reports agreement apart for
47
+ each combination of the two tables, what the judge read and the judge's own
48
+ prompt (now carried on every seal as `judge_prompt_sha256`). A new adoption
49
+ or an instruction-mode edit starts a new count.
50
+ - The terminal's reason prompt now says that reasons may be distilled into
51
+ this entry, which the agent can read.
52
+
53
+ **After upgrading, rule the delegation table again** where it was in force:
54
+ the shipped table now names `judgment_precedents`, so the judge stops (hash
55
+ mismatch) until you run
56
+ `ruby .kairos/skillsets/agent/bin/agent_rule.rb activate --table delegation`,
57
+ which pins the entry as absent. An instance with no delegation ruling is
58
+ unaffected and drafts nothing.
59
+
7
60
  ## [3.91.0] - 2026-10-07
8
61
 
9
62
  ### Added — agent: a shadow judge beside the operator (approval delegation, phase 1)
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.91.0"
2
+ VERSION = "3.92.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # The distiller's process (distillation design v0.2, INV-K1 / INV-K4).
5
+ # Started detached by the driver after an attested, reasoned answer at a
6
+ # delegable point commits, or in the foreground by `agent_rule.rb distill`.
7
+ # Counting, the one-draft lock, the table's status and the inputs are all
8
+ # decided here, off the response path; DistillRun does the run.
9
+ #
10
+ # It loads no agent tool and never opens a session's advance lock. What it
11
+ # reads is what the operator authored and what the driver observed: the
12
+ # attested answers on the chain, the terminal-sourced reasons they name, the
13
+ # point facts they name, and the operator's changes recorded at adoption.
14
+ #
15
+ # The model is reached through llm_call as the shadow judge reaches it:
16
+ # provider claude_code with sandbox_mode, no fallback.
17
+ #
18
+ # argv: [--manual] run whatever the count (the operator asked for it)
19
+ # env: KAIROS_SERVER_LIB lib dir to load kairos_mcp from
20
+ # KAIROS_DATA_DIR the server's data dir (the chain read and written)
21
+ # KAIROS_PROJECT_ROOT the server's working directory (its .kairos/storage)
22
+
23
+ require 'json'
24
+
25
+ manual = ARGV.include?('--manual')
26
+
27
+ begin
28
+ Process.setsid unless manual
29
+ rescue StandardError
30
+ # Already a group leader (spawned with pgroup): acceptable.
31
+ end
32
+
33
+ begin
34
+ Dir.chdir(ENV['KAIROS_PROJECT_ROOT']) if ENV['KAIROS_PROJECT_ROOT'] && Dir.exist?(ENV['KAIROS_PROJECT_ROOT'])
35
+ $LOAD_PATH.unshift(ENV['KAIROS_SERVER_LIB']) if ENV['KAIROS_SERVER_LIB'] && !ENV['KAIROS_SERVER_LIB'].empty?
36
+ require 'kairos_mcp'
37
+ KairosMcp.data_dir = ENV['KAIROS_DATA_DIR'] if ENV['KAIROS_DATA_DIR'] && !ENV['KAIROS_DATA_DIR'].empty?
38
+ require 'kairos_mcp/kairos_chain/chain'
39
+
40
+ lib = File.expand_path('../lib', __dir__)
41
+ %w[session admission mandate_adapter act_classification answer_ruling delegation point_facts distiller
42
+ distill_run distiller_prompt].each { |f| require File.join(lib, 'agent', f) }
43
+ require 'kairos_mcp/tools/base_tool'
44
+ # agent depends on llm_client; both are installed side by side.
45
+ require File.expand_path('../../llm_client/tools/llm_call', __dir__)
46
+ rescue ScriptError, StandardError => e
47
+ warn JSON.generate({ 'status' => 'failed', 'reason' => "bootstrap: #{e.class}: #{e.message[0, 200]}" })
48
+ exit 1
49
+ end
50
+
51
+ agent = KairosMcp::SkillSets::Agent
52
+ ar = agent::AnswerRuling
53
+ pf = agent::PointFacts
54
+ sessions = agent::Session.storage_path('agent_sessions')
55
+ dir = agent::Session.storage_path('agent_distill')
56
+
57
+ session_dir = lambda do |sid|
58
+ next nil unless sid.to_s.match?(/\A[\w-]+\z/)
59
+
60
+ d = File.join(sessions, sid.to_s)
61
+ File.directory?(d) ? d : nil
62
+ end
63
+
64
+ # The reason the operator typed at the terminal, matched by the digest the
65
+ # ruling names. A caller's text is never the operator's reason.
66
+ reason = lambda do |sid, anchor, sha|
67
+ d = session_dir.call(sid)
68
+ next nil unless d
69
+
70
+ ar.texts_at(d, anchor, source: 'terminal').map { |t| t['rationale'] }.find { |r| r && ar.sha256(r) == sha }
71
+ end
72
+
73
+ # The plans the answer covered, as the driver filed them when the answer
74
+ # committed; nil unless the filed line is the one the ruling names. An answer
75
+ # at a point always covers a plan, so an empty window is facts not recorded.
76
+ # Only what the driver observed passes: the cycle, and per step the tool and
77
+ # effect the table gave it, the unclassified count and the step count. The
78
+ # plan's own marks for a person are the agent's declaration and stay behind.
79
+ facts = lambda do |sid, anchor, sha|
80
+ d = session_dir.call(sid)
81
+ next nil unless d
82
+
83
+ entry, line_sha = pf.answer_facts(d, anchor)
84
+ plans = entry && line_sha == sha ? entry['plans'] : nil
85
+ next nil unless plans.is_a?(Array) && !plans.empty?
86
+ next nil unless plans.all? { |p| p.is_a?(Hash) && p['facts'].is_a?(Hash) }
87
+
88
+ plans.map do |p|
89
+ f = p['facts']
90
+ { 'cycle' => p['cycle'], 'facts' => f.slice('steps', 'unclassified', 'step_count') }
91
+ end
92
+ end
93
+
94
+ edits = Dir.glob(File.join(dir, 'adoptions', '*.json')).sort.filter_map do |f|
95
+ e = JSON.parse(File.read(f))
96
+ e.is_a?(Hash) ? e.slice('adopted_block', 'changes') : nil
97
+ rescue StandardError
98
+ nil
99
+ end.sort_by { |e| e['adopted_block'].to_i }
100
+
101
+ llm = lambda do |system:, user:, model:, effort:, provider:|
102
+ out = KairosMcp::SkillSets::LlmClient::Tools::LlmCall.new(nil).call(
103
+ 'messages' => [{ 'role' => 'user', 'content' => user }], 'system' => system, 'model' => model,
104
+ 'effort' => effort, 'sandbox_mode' => true, 'provider_override' => provider
105
+ )
106
+ payload = JSON.parse(Array(out).first[:text])
107
+ raise "llm_call #{payload.dig('error', 'type')}: #{payload.dig('error', 'message')}" unless payload['status'] == 'ok'
108
+
109
+ payload['response'].merge('provider' => payload['provider'])
110
+ end
111
+
112
+ records, records_error = ar.chain_records
113
+ if records_error
114
+ warn JSON.generate({ 'status' => 'failed', 'reason' => "chain unreadable: #{records_error}" })
115
+ exit 1
116
+ end
117
+
118
+ result = agent::DistillRun.run(
119
+ dir: dir, delegation_status: agent::Delegation.load_effective['status'], records: records,
120
+ facts: facts, reason: reason, edits: edits, llm: llm, chain: KairosMcp::KairosChain::Chain.new,
121
+ judge: agent::Delegation::JUDGE, prompts: agent::DistillerPrompt, manual: manual
122
+ )
123
+ (manual ? $stdout : $stderr).puts JSON.generate(result)
124
+ exit(%w[failed].include?(result['status']) ? 1 : 0)
@@ -11,7 +11,12 @@
11
11
  # ruby .kairos/skillsets/agent/bin/agent_rule.rb answer SESSION_ID
12
12
  # # answer a stopped session; the MCP answer there must then match
13
13
  # ruby .kairos/skillsets/agent/bin/agent_rule.rb shadow # count the shadow judge's agreement
14
- # # with your answers (records nothing)
14
+ # # with your answers, per version of what
15
+ # # shaped the judge (records nothing)
16
+ # ruby .kairos/skillsets/agent/bin/agent_rule.rb distill # draft judgment precedents now, whatever
17
+ # # the count (calls the distiller model)
18
+ # ruby .kairos/skillsets/agent/bin/agent_rule.rb adopt [STAMP]
19
+ # # adopt a draft (the newest by default) after editing its .edit.md copy
15
20
  #
16
21
  # Options: --table NAME activate / withdraw: act_classification (default)
17
22
  # or delegation
@@ -23,7 +28,7 @@
23
28
  # Needed when the server runs with --data-dir
24
29
  # pointing at another project's .kairos.
25
30
  #
26
- # activate, withdraw and answer read a nonce typed back at /dev/tty and refuse to run
31
+ # activate, withdraw, answer and adopt read a nonce typed back at /dev/tty and refuse to run
27
32
  # without a terminal. That keeps an MCP call from producing a ruling through
28
33
  # this path. It does not prove a person typed: any process that can start
29
34
  # programs can open a pseudo-terminal, read the nonce and type it back (shown
@@ -32,12 +37,15 @@
32
37
  # was at the keyboard (INV-D8 boundary; the constrained party is the agent).
33
38
 
34
39
  require 'json'
40
+ require 'rbconfig'
35
41
 
36
- action = ARGV.find { |a| %w[activate withdraw status answer shadow].include?(a) }
37
- abort 'usage: agent_rule.rb activate|withdraw [--table act_classification|delegation]|status|answer SESSION_ID|shadow ' \
38
- '[--data-dir DIR] [--project-dir DIR]' unless action
42
+ action = ARGV.find { |a| %w[activate withdraw status answer shadow distill adopt].include?(a) }
43
+ abort 'usage: agent_rule.rb activate|withdraw [--table act_classification|delegation]|status|answer SESSION_ID|shadow|' \
44
+ 'distill|adopt [STAMP] [--data-dir DIR] [--project-dir DIR]' unless action
39
45
  session_id = action == 'answer' ? ARGV[ARGV.index('answer') + 1].to_s : nil
40
46
  abort 'usage: agent_rule.rb answer SESSION_ID [--data-dir DIR]' if session_id&.then { |s| s.empty? || s.start_with?('-') }
47
+ stamp = action == 'adopt' ? ARGV[ARGV.index('adopt') + 1] : nil
48
+ stamp = nil if stamp.to_s.start_with?('-')
41
49
 
42
50
  tidx = ARGV.index('--table')
43
51
  table = tidx ? ARGV[tidx + 1].to_s : 'act_classification'
@@ -63,12 +71,16 @@ require File.join(lib, 'agent', 'act_classification')
63
71
  require File.join(lib, 'agent', 'answer_ruling')
64
72
  require File.join(lib, 'agent', 'delegation')
65
73
  require File.join(lib, 'agent', 'shadow_judge')
74
+ require File.join(lib, 'agent', 'point_facts')
75
+ require File.join(lib, 'agent', 'distiller')
76
+ require File.join(lib, 'agent', 'distill_run')
77
+ require File.join(lib, 'agent', 'adoption')
66
78
  ac = KairosMcp::SkillSets::Agent::ActClassification
67
79
  dl = KairosMcp::SkillSets::Agent::Delegation
68
80
  path = ac::BASE_PATH
69
81
 
70
82
  puts "Data dir: #{data_dir}"
71
- if %w[answer shadow].include?(action)
83
+ if %w[answer shadow status distill adopt].include?(action)
72
84
  require 'kairos_mcp/invocation_context'
73
85
  require File.join(lib, 'agent', 'session')
74
86
  require File.join(lib, 'agent', 'advance_gate')
@@ -77,7 +89,9 @@ if %w[answer shadow].include?(action)
77
89
  # data dir. The two can differ; the answer is read from the first and
78
90
  # recorded on the second, the chain the server reads.
79
91
  pidx = ARGV.index('--project-dir')
80
- abort 'usage: agent_rule.rb answer SESSION_ID --project-dir DIR' if pidx && ARGV[pidx + 1].to_s.then { |v| v.empty? || v.start_with?('-') }
92
+ if pidx && ARGV[pidx + 1].to_s.then { |v| v.empty? || v.start_with?('-') }
93
+ abort "usage: agent_rule.rb #{action == 'answer' ? 'answer SESSION_ID' : action} --project-dir DIR"
94
+ end
81
95
  project_dir = pidx ? File.expand_path(ARGV[pidx + 1].to_s) : File.dirname(data_dir)
82
96
  abort "no project dir at #{project_dir}" unless File.directory?(project_dir)
83
97
  Dir.chdir(project_dir)
@@ -102,9 +116,45 @@ if action == 'status'
102
116
  puts " file sha256: #{deff['sha256']}"
103
117
  puts " detail: #{deff['detail']}" if deff['detail']
104
118
  deff['rows'].each { |r| puts " row #{r['id']} at #{r['point']} (#{r['mode']})" }
105
- deff['reads'].each { |p| puts " judge reads #{p['name']}: #{p['path']} (sha256 #{p['sha256']})" }
119
+ deff['reads'].each do |p|
120
+ puts(p['absent'] ? " judge reads #{p['name']}: #{p['path']} (absent: nothing adopted yet)" :
121
+ " judge reads #{p['name']}: #{p['path']} (sha256 #{p['sha256']})")
122
+ end
123
+ # Drafting (distillation design v0.2 INV-K4): shown here, never in an advance.
124
+ distill_dir = KairosMcp::SkillSets::Agent::Session.storage_path('agent_distill')
125
+ drun = KairosMcp::SkillSets::Agent::DistillRun
126
+ st = drun.state(distill_dir)
127
+ records, rerr = KairosMcp::SkillSets::Agent::AnswerRuling.chain_records
128
+ counted = rerr ? "unknown (#{rerr})" :
129
+ KairosMcp::SkillSets::Agent::Distiller.count_new(records, high_water: st['high_water'].to_i)
130
+ puts 'Judgment precedents drafting:'
131
+ puts " reasoned answers since the last draft: #{counted} (a draft starts at #{KairosMcp::SkillSets::Agent::Distiller::THRESHOLD}; " \
132
+ "only while the delegation table is in force)"
133
+ last = st['last'] || {}
134
+ puts " last run: #{last.empty? ? '(none)' : last.map { |k, v| "#{k} #{v}" }.join(', ')}"
135
+ if last['status'] == 'running'
136
+ # A run holds the lock for its whole life; a free lock means it died.
137
+ # Read the state again while holding the lock: a run that finished in
138
+ # between released it, and is not dead.
139
+ died = File.open(File.join(distill_dir, drun::LOCK_FILE), 'a') do |f|
140
+ f.flock(File::LOCK_EX | File::LOCK_NB) && drun.state(distill_dir).dig('last', 'status') == 'running'
141
+ end
142
+ puts ' (no process holds the drafting lock: that run died before it finished)' if died
143
+ end
144
+ drafts = drun.drafts(distill_dir)
145
+ puts " drafts: #{drafts.empty? ? '(none)' : drafts.first(3).map { |d| d['stamp'] }.join(', ')}"
146
+ puts " to adopt the newest: edit #{drafts.first['edit_path']}, then run agent_rule.rb adopt" unless drafts.empty?
106
147
  exit 0
107
148
  end
149
+ if action == 'distill'
150
+ # The worker in the foreground, run by the operator: the same code path a
151
+ # count starts, without the count.
152
+ env = { 'KAIROS_PROJECT_ROOT' => Dir.pwd, 'KAIROS_DATA_DIR' => data_dir }
153
+ env['KAIROS_SERVER_LIB'] = ENV['KAIROS_SERVER_LIB'] if ENV['KAIROS_SERVER_LIB']
154
+ puts 'Drafting (this calls the distiller model and may take several minutes)...'
155
+ ok = system(env, RbConfig.ruby, File.expand_path('agent_distill.rb', __dir__), '--manual')
156
+ exit(ok ? 0 : 1)
157
+ end
108
158
  if action == 'shadow'
109
159
  ar = KairosMcp::SkillSets::Agent::AnswerRuling
110
160
  sj = KairosMcp::SkillSets::Agent::ShadowJudge
@@ -114,14 +164,25 @@ if action == 'shadow'
114
164
  verdict_dir = lambda do |sid, anchor|
115
165
  sid.to_s.match?(/\A[\w-]+\z/) ? sj.anchor_dir(File.join(sessions, sid.to_s), anchor) : File::NULL
116
166
  end
117
- sum = sj.agreement(records, verdict_dir: verdict_dir)
118
- yes = sum['operator_approved']
119
- no = sum['operator_did_not_approve']
120
- puts "Pairs that count as evidence (your terminal answer, sealed before it): #{sum['pairs']}"
121
- puts " you approved: #{yes['n']}; the judge also approved #{yes['judge_agreed']}"
122
- puts " you did not approve: #{no['n']}; the judge gave the same answer #{no['judge_agreed']}, approved #{no['judge_approved']}"
123
- puts " the judge approved #{sum['judge_approved']} of #{sum['pairs']}"
124
- puts "Not counted: #{sum['not_counted'].empty? ? '(none)' : sum['not_counted'].map { |k, v| "#{k} #{v}" }.join(', ')}"
167
+ # Counted apart for each version of what shaped the judge (distillation
168
+ # design v0.2 INV-K5): a new adoption, a mode edit or a new judge prompt
169
+ # starts a new count; earlier counts are shown, not merged.
170
+ by = sj.agreement_by_version(records, verdict_dir: verdict_dir)
171
+ total = by['versions'].sum { |v| v['summary']['pairs'] }
172
+ puts "Pairs that count as evidence (your terminal answer, sealed before it): #{total}, " \
173
+ "in #{by['versions'].size} version(s), newest first"
174
+ by['versions'].each_with_index do |v, i|
175
+ sum = v['summary']
176
+ key = v['version']
177
+ yes = sum['operator_approved']
178
+ no = sum['operator_did_not_approve']
179
+ puts "Version #{i + 1} (seals ##{v['first_seal']}..##{v['last_seal']}): judge prompt #{key['judge_prompt'].to_s[0, 12].then { |x| x.empty? ? '(before 3.92.0)' : x }}, " \
180
+ "reads #{key['reads'].map { |r| r.sub(/:(\h{12})\h+\z/, ':\1') }.join(' ')}"
181
+ puts " you approved: #{yes['n']}; the judge also approved #{yes['judge_agreed']}"
182
+ puts " you did not approve: #{no['n']}; the judge gave the same answer #{no['judge_agreed']}, approved #{no['judge_approved']}"
183
+ puts " the judge approved #{sum['judge_approved']} of #{sum['pairs']}"
184
+ end
185
+ puts "Not counted: #{by['not_counted'].empty? ? '(none)' : by['not_counted'].map { |k, v| "#{k} #{v}" }.join(', ')}"
125
186
  exit 0
126
187
  end
127
188
 
@@ -132,7 +193,13 @@ rescue StandardError => e
132
193
  end
133
194
 
134
195
  begin
135
- result = if action == 'answer'
196
+ result = if action == 'adopt'
197
+ KairosMcp::SkillSets::Agent::Adoption.interactive_adopt(
198
+ dir: KairosMcp::SkillSets::Agent::Session.storage_path('agent_distill'),
199
+ sessions_dir: KairosMcp::SkillSets::Agent::Session.storage_path('agent_sessions'),
200
+ stamp: stamp, tty_in: tty, tty_out: tty
201
+ )
202
+ elsif action == 'answer'
136
203
  KairosMcp::SkillSets::Agent::AnswerRuling.interactive_answer(
137
204
  session: session, gate: KairosMcp::SkillSets::Agent::AdvanceGate.new(session.guard_dir),
138
205
  tty_in: tty, tty_out: tty, reload: -> { KairosMcp::SkillSets::Agent::Session.load(session_id) }