kairos-chain 3.62.0 → 3.64.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -0
  3. data/lib/kairos_mcp/admin/router.rb +20 -8
  4. data/lib/kairos_mcp/admin/views/chain.erb +7 -2
  5. data/lib/kairos_mcp/admin/views/dashboard.erb +7 -2
  6. data/lib/kairos_mcp/admin/views/partials/_chain_blocks.erb +9 -1
  7. data/lib/kairos_mcp/drift_detection/correspondence_checker.rb +18 -3
  8. data/lib/kairos_mcp/kairos_chain/chain.rb +327 -59
  9. data/lib/kairos_mcp/storage/backend.rb +31 -2
  10. data/lib/kairos_mcp/storage/file_backend.rb +43 -7
  11. data/lib/kairos_mcp/tools/chain_history.rb +13 -4
  12. data/lib/kairos_mcp/tools/chain_status.rb +5 -1
  13. data/lib/kairos_mcp/tools/chain_verify.rb +8 -4
  14. data/lib/kairos_mcp/tools/formalization_history.rb +14 -0
  15. data/lib/kairos_mcp/version.rb +1 -1
  16. data/templates/knowledge/multi_llm_review_workflow/multi_llm_review_workflow.md +144 -2
  17. data/templates/skillsets/introspection/lib/introspection/safety_inspector.rb +5 -1
  18. data/templates/skillsets/introspection/tools/introspection_check.rb +28 -7
  19. data/templates/skillsets/multi_llm_review/bin/dispatch_worker.rb +27 -1
  20. data/templates/skillsets/multi_llm_review/lib/multi_llm_review/consensus.rb +92 -18
  21. data/templates/skillsets/multi_llm_review/lib/multi_llm_review/dispatcher.rb +26 -2
  22. data/templates/skillsets/multi_llm_review/lib/multi_llm_review/pending_state.rb +8 -0
  23. data/templates/skillsets/multi_llm_review/lib/multi_llm_review/prompt_builder.rb +19 -5
  24. data/templates/skillsets/multi_llm_review/lib/multi_llm_review/sanitizer.rb +11 -1
  25. data/templates/skillsets/multi_llm_review/skillset.json +2 -2
  26. data/templates/skillsets/multi_llm_review/test/test_dispatcher_usage.rb +66 -0
  27. data/templates/skillsets/multi_llm_review/test/test_evidence_fidelity.rb +14 -7
  28. data/templates/skillsets/multi_llm_review/test/test_multi_llm_review.rb +338 -3
  29. data/templates/skillsets/multi_llm_review/test/test_mutation_survivors.rb +5 -2
  30. data/templates/skillsets/multi_llm_review/tools/multi_llm_review_collect.rb +141 -13
  31. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d07ef9eddf8433aed348e6ff25d910547da041e89abdf94dc6e5008f2148f417
4
- data.tar.gz: bddb2825ffb56e57681be7a1c368e683103133dad90888dff052a8fc5c430ae8
3
+ metadata.gz: 4ba66e678675e9b4e7776676832765d509f0ac7bb49feed159ee1513ba4b7318
4
+ data.tar.gz: 28af1484af3a0c98529603bfa30fd9ff188e5b18895c06c11a313bcf3a95d8b2
5
5
  SHA512:
6
- metadata.gz: f432b43e89098c0d794b8e96d8241009802a6ca41e71804ab77f0ad73abaac07aa0b413889f5d84f0425e22bfddcc6f9fa9a3cbc968196bd6ddbda843e89d1f1
7
- data.tar.gz: ff82bd049b30fd06ba14d529ff3910ca9d540c071f441519c23f0189619b90a1b1bfa072449713ba8b53bd575cd5dde763eed6022d141465849f8b6769e5e862
6
+ metadata.gz: 67e98820708350d674970e3db49f3583efc01491ba8d58f5ecb702b988f64a5ad1efeb083637f45e77897c44dbb30a84569400431e8f669020fb13a2dae7b4aa
7
+ data.tar.gz: 27fef53ab90cdb4a7745955b49d41e3834d8313f0a0417605a96e21e84b0ab44fcb5c0e3ad27d9ff2cf698bda2a75c0dc7712869ab5e686daeb8cc468da77cdf
data/CHANGELOG.md CHANGED
@@ -4,6 +4,84 @@ 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.64.0] - 2026-08-06
8
+
9
+ ### Fixed
10
+
11
+ - **An append can no longer erase the chain history on disk.** Two paths
12
+ erased history before this release, both measured, neither raising an
13
+ exception: two holders of the chain at the same length both saved and the
14
+ second save replaced the disk sequence with its own; and a ledger that
15
+ existed but could not be read collapsed to nil, read as a fresh install,
16
+ and the next save rebuilt from genesis over it — 687 blocks became 2. An
17
+ append now takes a key file beside the ledger, re-reads and classifies the
18
+ disk under that key (absent / unreadable / empty / corrupt / readable),
19
+ refuses every state but `:readable` and `:absent`, and always builds on
20
+ the tail that is on disk at that moment. Failures on the read and write
21
+ side raise `Storage::Error` instead of flattening into nil or false, and
22
+ every reader consults `Chain#load_state` before trusting the sequence.
23
+ Includes the 3.62.1 locale fix, and one more of the same family: while
24
+ the ledger does not yet exist, a symlinked name resolves to its target
25
+ before the lock key is derived, so a fresh install reached through two
26
+ names is written under one key (measured pre-fix: 51 blocks shrank to
27
+ 35–49 under concurrent two-name appends). Verified by 149 assertions and
28
+ a 34-row falsification harness in which each protective mechanism is
29
+ removed in isolation and its named check confirmed to go red.
30
+
31
+ ## [3.62.1] - 2026-08-06
32
+
33
+ ### Fixed
34
+
35
+ - **A non-UTF-8 locale no longer erases the ledger on the next append.**
36
+ Emergency hotfix cut from the shipped 3.62.0 (branch
37
+ `hotfix/utf8-ledger-read`): `FileBackend#load_blocks` read
38
+ `blockchain.json` with the locale-derived default encoding, so a
39
+ LANG-unset start (launchd, cron, plain containers) made a non-ASCII
40
+ ledger unparseable, the failure was flattened into "no ledger", and one
41
+ ordinary append overwrote 705 blocks with 2 (measured on a copy of a
42
+ production ledger). The ledger is now read as UTF-8 explicitly. This is
43
+ the only change over 3.62.0; 3.64.0 contains the full erasure fix.
44
+
45
+ ## [3.63.0] - 2026-08-06
46
+
47
+ ### Changed
48
+
49
+ - **`multi_llm_review` findings gain a weight axis, and a worker death no
50
+ longer discards completed seats** (SkillSet 0.9.1 → 0.10.0, frozen after one
51
+ review round; every deployment-grounded finding fixed in-round).
52
+
53
+ The severity of a finding said what kind of defect it was, never what it
54
+ costs: measured on a three-persona panel, 3 of one round's 7 P0s were
55
+ factually correct findings that cost nobody anything, landing at the same
56
+ severity as a defect that silently corrupts published output. The reviewer
57
+ prompt contract now requires a `[consequence: who is harmed, and how]`
58
+ clause on every P0, and aggregation records an unclaused P0 at P2 with the
59
+ stated severity and the demotion reason kept beside it. Only presence is
60
+ checked, mechanically; whether a stated consequence is real or trivial
61
+ stays the orchestrator's call. The clause is read before the byte bound
62
+ cuts the tail, the first non-empty clause counts, deduplication compares
63
+ issues without their clauses, and the demotion mark survives merging in
64
+ any member order. Reviewer prompts also stop naming the round number, and
65
+ the L1 workflow knowledge (v3.10.0) adds the matching orchestrator-side
66
+ rule: never tell a reviewer its finding count, the round number, or prior
67
+ verdicts — counts a reviewer performs select for finding-production over
68
+ finding-weight.
69
+
70
+ Separately, one stuck seat used to cost a whole round: a stale heartbeat
71
+ killed the detached worker and every completed seat's reply died with it,
72
+ because nothing left the worker's memory until all seats were done. The
73
+ worker now persists each seat's outcome the moment it is decided
74
+ (`partial_results.json`, atomic, single writer — including the
75
+ dispatcher's own deadline skips, so recovery cannot relabel a reached
76
+ seat as lost). The collect tool's crash and timeout branches recover the
77
+ completed seats, but only after the reaper CONFIRMS the worker's death: a
78
+ stale heartbeat is not a death certificate, and a record sealed over a
79
+ live worker would be permanently false once idempotent replay pins it.
80
+ Unconfirmed death falls back to the previous retryable total-loss report.
81
+ Seats the worker never reached enter the denominator as skip rows named
82
+ `worker_crashed_seat_lost`, and the payload carries `worker_failure`
83
+ naming the death and the recovered and lost seat labels.
84
+
7
85
  ## [3.62.0] - 2026-08-06
8
86
 
9
87
  ### Changed
@@ -346,6 +346,7 @@ module KairosMcp
346
346
 
347
347
  html_response(200, render_partial('_chain_blocks',
348
348
  blocks: paged_blocks,
349
+ load_state: chain.load_state,
349
350
  total: total,
350
351
  limit: limit,
351
352
  offset: offset))
@@ -353,6 +354,10 @@ module KairosMcp
353
354
 
354
355
  def handle_chain_block_detail_partial(index)
355
356
  chain = KairosChain::Chain.new
357
+ unless chain.valid?
358
+ return html_response(200, "<p>Block ##{index} unavailable: ledger is #{chain.load_state}.</p>")
359
+ end
360
+
356
361
  block = chain.chain.find { |b| b.respond_to?(:index) ? b.index == index : b['index'] == index }
357
362
 
358
363
  if block
@@ -364,13 +369,17 @@ module KairosMcp
364
369
 
365
370
  def handle_chain_verify_partial
366
371
  chain = KairosChain::Chain.new
367
- valid = chain.valid?
368
- length = chain.chain.length
369
372
 
370
- result = if valid
371
- "<div class='flash flash-success'>Chain is valid. #{length} blocks verified.</div>"
373
+ # Three outcomes: a ledger that does not exist yet is not a failure.
374
+ result = case chain.load_state
375
+ when :readable
376
+ "<div class='flash flash-success'>Chain is valid. " \
377
+ "#{chain.chain.length} blocks verified.</div>"
378
+ when :absent
379
+ "<div class='flash'>Chain not created yet. Nothing to verify.</div>"
372
380
  else
373
- "<div class='flash flash-error'>Chain integrity check FAILED!</div>"
381
+ "<div class='flash flash-error'>Chain integrity check FAILED! " \
382
+ "(state: #{chain.load_state})</div>"
374
383
  end
375
384
  html_response(200, result)
376
385
  end
@@ -475,17 +484,20 @@ module KairosMcp
475
484
  backend = SkillsConfig.storage_backend
476
485
 
477
486
  latest = chain.latest_block
478
- latest_h = latest.respond_to?(:to_h) ? latest.to_h : latest
487
+ latest_h = latest.respond_to?(:to_h) ? latest&.to_h : latest
479
488
 
480
489
  {
481
490
  valid: chain.valid?,
491
+ state: chain.load_state,
482
492
  length: chain.chain.length,
483
493
  storage: { backend: backend },
484
494
  latest_block: latest_h
485
495
  }
486
496
  rescue StandardError => e
487
- { valid: false, length: 0, storage: { backend: 'unknown' },
488
- latest_block: {}, error: e.message }
497
+ # Chain.new does not raise; this branch is reachable only for failures
498
+ # outside the ledger (config, rendering). It must not fabricate a state.
499
+ { valid: false, state: :error, length: 0, storage: { backend: 'unknown' },
500
+ latest_block: nil, error: e.message }
489
501
  end
490
502
 
491
503
  def fetch_knowledge_list(search: nil)
@@ -11,13 +11,18 @@
11
11
  <div>
12
12
  <p>
13
13
  <strong>Status:</strong>
14
+ <%# Three displays, not two: an absent ledger is a fresh install. %>
14
15
  <% if chain[:valid] %>
15
16
  <span class="badge badge-success">Valid</span>
17
+ <% elsif chain[:state] == :absent %>
18
+ <span class="badge">Not created yet</span>
16
19
  <% else %>
17
- <span class="badge badge-error">Invalid</span>
20
+ <span class="badge badge-error">Invalid (<%= h(chain[:state].to_s) %>)</span>
18
21
  <% end %>
19
22
  </p>
20
- <p><strong>Length:</strong> <%= chain[:length] %> blocks</p>
23
+ <% if chain[:valid] %>
24
+ <p><strong>Length:</strong> <%= chain[:length] %> blocks</p>
25
+ <% end %>
21
26
  </div>
22
27
  <div>
23
28
  <p><strong>Backend:</strong> <%= h(chain.dig(:storage, :backend) || 'unknown') %></p>
@@ -9,13 +9,18 @@
9
9
  <article>
10
10
  <header>Blockchain</header>
11
11
  <p>
12
+ <%# Three displays, not two: an absent ledger is a fresh install. %>
12
13
  <% if chain[:valid] %>
13
14
  <span class="badge badge-success">Valid</span>
15
+ <% elsif chain[:state] == :absent %>
16
+ <span class="badge">Not created yet</span>
14
17
  <% else %>
15
- <span class="badge badge-error">Invalid</span>
18
+ <span class="badge badge-error">Invalid (<%= h(chain[:state].to_s) %>)</span>
16
19
  <% end %>
17
20
  </p>
18
- <p><strong><%= chain[:length] %></strong> blocks</p>
21
+ <% if chain[:valid] %>
22
+ <p><strong><%= chain[:length] %></strong> blocks</p>
23
+ <% end %>
19
24
  <p><small>Backend: <%= h(chain.dig(:storage, :backend) || 'unknown') %></small></p>
20
25
  <footer><a href="/admin/chain">View Chain &rarr;</a></footer>
21
26
  </article>
@@ -1,5 +1,13 @@
1
1
  <% if blocks.empty? %>
2
- <p>No blocks found.</p>
2
+ <%# Three displays, not two: an absent ledger is a fresh install, and an
3
+ unreadable one must not be reported as an empty history. %>
4
+ <% if load_state == :readable %>
5
+ <p>No blocks found.</p>
6
+ <% elsif load_state == :absent %>
7
+ <p>Chain not created yet.</p>
8
+ <% else %>
9
+ <p>History unavailable: ledger is <%= h(load_state.to_s) %>.</p>
10
+ <% end %>
3
11
  <% else %>
4
12
  <div class="overflow-auto">
5
13
  <table>
@@ -64,7 +64,23 @@ module KairosMcp
64
64
  end
65
65
 
66
66
  active = Digest::SHA256.hexdigest(File.read(md_file_path))
67
- recorded = recorded_digest_for(name, storage_backend)
67
+
68
+ chain = KairosChain::Chain.new(storage_backend: storage_backend)
69
+ # :absent completes normally: a fresh install has no ledger, the scan
70
+ # over zero blocks finds nothing, and :missing_record is the honest
71
+ # verdict. The other failed states mean provenance is UNAVAILABLE,
72
+ # which is not the same claim as "never recorded" — reporting
73
+ # :missing_record there would accuse every artifact whenever the
74
+ # ledger cannot be read.
75
+ unless chain.valid? || chain.load_state == :absent
76
+ return Result.new(
77
+ status: :error, name: name,
78
+ active_digest: active, recorded_digest: nil,
79
+ message: "L1 '#{name}': provenance unavailable — ledger state is #{chain.load_state}"
80
+ )
81
+ end
82
+
83
+ recorded = recorded_digest_for(name, chain)
68
84
 
69
85
  if recorded.nil?
70
86
  return Result.new(
@@ -101,8 +117,7 @@ module KairosMcp
101
117
  # the most recent knowledge_update record, scanning the chain from head
102
118
  # backward. Returns nil when the most recent relevant record removed the
103
119
  # artifact (next_hash nil — delete/archive) or when none exists.
104
- def recorded_digest_for(name, storage_backend)
105
- chain = KairosChain::Chain.new(storage_backend: storage_backend)
120
+ def recorded_digest_for(name, chain)
106
121
  chain.chain.reverse_each do |block|
107
122
  Array(block.data).each do |entry|
108
123
  record = parse_entry(entry)