kairos-chain 3.51.0 → 3.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fea78afc847b26db63d7355188444750fdb3cbbf84494cc489e63402bea3b630
4
- data.tar.gz: b638968a01320bf5196d7f2b383b5c6dfa118a9432dc6feadf4ef04cbd4ee6b8
3
+ metadata.gz: e6bc15d467e1fbef211be82d39ec89bfdc760d7bdd67286635967c95c2acd2ce
4
+ data.tar.gz: b761078e7adc2798988a2fb0154bbbf998b37eed14d9979e87e7656194f03b76
5
5
  SHA512:
6
- metadata.gz: 0be2e69efbd5eda0c8c839c627389def7dd68db5cda29a904072e762677238459ef6a2fad8373d612715f1eeeb928579a7a318bc97ffdd5fb6c38eb54991c522
7
- data.tar.gz: 74fd8b7ba83ef25552130dbd7b86047ce96fda674b1d151b247813597adef09c60ec192c524507ad054d3af3c63154b497de71d912891f84cad8637e79bd1bdf
6
+ metadata.gz: 604f315facca09bdb89e8abe403cf2873abe6a201cf9771181f04a813c323685da0749237e0f3f547379f9ce1dc7a8976b3e12f657c4718604ef694368f15f27
7
+ data.tar.gz: 67b55d37e2ba35259b1df7e42c96222cf637a9b295eac63b877b715f060310ff19841c2152de9fad1a92345a8f40738803b47a68b020e74ad9405c923a17de6b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,49 @@ 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.51.3] - 2026-07-23
8
+
9
+ ### Multi-LLM Review — knowledge template sync (v3.6.1)
10
+
11
+ Completes the 3.51.2 roster update: `templates/knowledge/multi_llm_review_workflow/`
12
+ is synced to v3.6.1 (gpt-5.6-sol roster). 3.51.2 updated the skillset config
13
+ template only, so `kairos-chain upgrade` reported a knowledge conflict against
14
+ instances already carrying the v3.6.1 L1 locally. No code change.
15
+
16
+ ## [3.51.2] - 2026-07-23
17
+
18
+ ### Multi-LLM Review — Codex roster update (gpt-5.6-sol)
19
+
20
+ Template config only, no code change. In
21
+ `templates/skillsets/multi_llm_review/config/multi_llm_review.yml`, the Codex
22
+ reviewer `gpt-5.4` is retired and replaced by `gpt-5.6-sol`
23
+ (`role_label: codex_gpt5.6-sol`). `gpt-5.5` stays as the calibrated anchor
24
+ for cross-generation comparison. 5.6-sol's reviewer bias profile is not yet
25
+ calibrated — record (a)/(b)/(c) breakdowns per round until a profile
26
+ accumulates. Roster size (6) and convergence rules are unchanged. The
27
+ matching L1 knowledge `multi_llm_review_workflow` was updated to v3.6.1
28
+ (instance-local, chain-recorded).
29
+
30
+ ## [3.51.1] - 2026-07-23
31
+
32
+ ### CLI — `skillset upgrade` transparency log
33
+
34
+ `kairos-chain skillset upgrade` (both check and `--apply`) now prints a
35
+ structured report mirroring `kairos-chain upgrade --apply`, so a no-op run is
36
+ visibly a completed check rather than a bare `All SkillSets are up to date.`
37
+ one-liner. Every CHECKED SkillSet (an installed one with a gem-template
38
+ counterpart) is listed with its status, followed by a summary:
39
+
40
+ - `[UPGRADE]` / `[UPGRADED]` — version/files changed (changed files listed on
41
+ check), and, on `--apply`, `vFROM -> vTO (N files)`.
42
+ - `[UNCHANGED]` — checked, no difference (the case that was previously invisible).
43
+ - `[NEW-CORE]` — a core SkillSet not yet installed, installed on `--apply`.
44
+ - `[OPTIONAL]` — a non-core new SkillSet, never auto-installed (explicit approval).
45
+ - `## Summary` — counts of upgraded/installed, unchanged, and optional.
46
+
47
+ Display-only: the upgrade decision (`upgrade_check` / `upgrade_apply`) is
48
+ unchanged; `core_only: true` behavior for the plain CLI form is preserved.
49
+
7
50
  ## [3.51.0] - 2026-07-23
8
51
 
9
52
  ### Agent SkillSet — interruption resilience A-1 + A-2
data/bin/kairos-chain CHANGED
@@ -141,18 +141,43 @@ when 'skillset'
141
141
  manual = upgrades.select { |u| u[:new_skillset] && !KairosMcp::SkillSetManager::CORE_SKILLSETS.include?(u[:name]) }
142
142
  applicable = upgrades - manual
143
143
 
144
- if upgrades.empty?
145
- puts "All SkillSets are up to date."
146
- elsif apply
144
+ # Diagnostic transparency: report the full set of CHECKED SkillSets — not
145
+ # only the ones that changed — so a no-op run is visibly a completed check
146
+ # rather than a bare one-liner, mirroring `kairos-chain upgrade --apply`.
147
+ # This is display-only; the upgrade decision itself (upgrade_check /
148
+ # upgrade_apply) is unchanged. "Checked" = an installed SkillSet that has a
149
+ # gem-template counterpart (exactly what upgrade_check compares).
150
+ templates_dir = File.join(KairosMcp.gem_root, 'templates', 'skillsets')
151
+ checkable = manager.all_skillsets
152
+ .select { |ss| File.directory?(File.join(templates_dir, ss.name)) }
153
+ .sort_by(&:name)
154
+ existing_upgrade_names = applicable.reject { |u| u[:new_skillset] }.map { |u| u[:name] }
155
+ new_core = applicable.select { |u| u[:new_skillset] }
156
+
157
+ if apply
147
158
  results = manager.upgrade_apply(core_only: true)
159
+ upgraded_names = results.map { |r| r[:name] }
160
+ puts "# Applying SkillSet Upgrade"
161
+ puts ""
162
+ puts "## SkillSets"
148
163
  results.each do |r|
149
- puts "Updated #{r[:name]}: v#{r[:from]} -> v#{r[:to]} (#{r[:files_updated]} files)"
164
+ puts " [UPGRADED] #{r[:name].ljust(22)} v#{r[:from]} -> v#{r[:to]} (#{r[:files_updated]} files)"
165
+ end
166
+ checkable.reject { |ss| upgraded_names.include?(ss.name) }.each do |ss|
167
+ puts " [UNCHANGED] #{ss.name.ljust(22)} v#{ss.version}"
150
168
  end
151
- puts "Done. #{results.size} SkillSet(s) upgraded."
169
+ manual.each do |u|
170
+ puts " [OPTIONAL] #{u[:name].ljust(22)} v#{u[:available_version]} (not installed)"
171
+ end
172
+ puts ""
173
+ puts "## Summary"
174
+ puts " Upgraded/installed: #{results.size}"
175
+ puts " Unchanged: #{checkable.reject { |ss| upgraded_names.include?(ss.name) }.size}"
176
+ puts " Optional (not installed): #{manual.size}"
152
177
 
153
178
  if manual.any?
154
179
  puts ""
155
- puts "Not installed (optional, requires explicit approval):"
180
+ puts "Optional SkillSets require explicit approval:"
156
181
  manual.each { |u| puts " kairos-chain skillset install #{u[:name]}" }
157
182
  end
158
183
 
@@ -174,24 +199,39 @@ when 'skillset'
174
199
  end
175
200
  end
176
201
  else
177
- if applicable.any?
178
- puts "SkillSet upgrades available:"
179
- puts ""
180
- applicable.each do |u|
202
+ upgrade_by_name = applicable.reject { |u| u[:new_skillset] }.each_with_object({}) { |u, h| h[u[:name]] = u }
203
+ puts "# SkillSet Upgrade Check"
204
+ puts ""
205
+ puts "## SkillSets"
206
+ checkable.each do |ss|
207
+ if (u = upgrade_by_name[ss.name])
181
208
  label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
182
- puts " #{u[:name]}: #{label}"
183
- u[:changed_files].each { |f| puts " Modified: #{f}" }
184
- puts ""
209
+ puts " [UPGRADE] #{ss.name.ljust(22)} #{label}"
210
+ u[:changed_files].each { |f| puts " #{f}" }
211
+ else
212
+ puts " [UNCHANGED] #{ss.name.ljust(22)} v#{ss.version}"
185
213
  end
186
- puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
187
214
  end
215
+ new_core.each do |u|
216
+ puts " [NEW-CORE] #{u[:name].ljust(22)} v#{u[:available_version]} (will install on --apply)"
217
+ end
218
+ manual.each do |u|
219
+ puts " [OPTIONAL] #{u[:name].ljust(22)} v#{u[:available_version]} (not installed)"
220
+ end
221
+ puts ""
222
+ puts "## Summary"
223
+ puts " Available upgrades: #{applicable.size}"
224
+ puts " Unchanged: #{checkable.size - existing_upgrade_names.size}"
225
+ puts " Optional (not installed): #{manual.size}"
188
226
 
227
+ if applicable.any?
228
+ puts ""
229
+ puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
230
+ end
189
231
  if manual.any?
190
- puts "" if applicable.any?
191
- puts "Optional SkillSets available (not auto-installed; require explicit approval):"
192
- manual.each { |u| puts " #{u[:name]} v#{u[:available_version]} — #{u[:description]}" }
193
232
  puts ""
194
- puts "Install with: kairos-chain skillset install <name>"
233
+ puts "Optional SkillSets (require explicit approval):"
234
+ manual.each { |u| puts " kairos-chain skillset install #{u[:name]} # #{u[:description]}" }
195
235
  end
196
236
  end
197
237
 
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.51.0"
2
+ VERSION = "3.52.1"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: multi_llm_review_workflow
3
3
  description: "Multi-LLM review methodology and execution — workflow pattern, CLI tooling, consensus analysis, Persona Assembly. Applicable to design, implementation, documentation, or any artifact."
4
- version: "3.6"
4
+ version: "3.6.1"
5
5
  tags:
6
6
  - workflow
7
7
  - review
@@ -345,7 +345,7 @@ starting** and verify each against `config/multi_llm_review.yml`:
345
345
  - [ ] Your model (orchestrator): ___
346
346
  - [ ] Agent Team Personas model: = orchestrator model (NOT a different model)
347
347
  - [ ] Subprocess CLI models: Opus 4.6 AND Opus 4.8 (both, not either/or)
348
- - [ ] Codex models: gpt-5.5 (default) AND gpt-5.4 (both, not either/or)
348
+ - [ ] Codex models: gpt-5.6-sol (default) AND gpt-5.5 (both, not either/or)
349
349
  - [ ] Cursor model: default (composer-2.5, no --model flag)
350
350
  - [ ] Total reviewer count: 6 (or 5 after orchestrator exclusion from subprocess)
351
351
  - [ ] Convergence rule: 4/6 APPROVE (full) or 3/5 APPROVE (after exclusion)
@@ -356,7 +356,7 @@ starting** and verify each against `config/multi_llm_review.yml`:
356
356
  | Mistake | Correct behavior | Why it happens |
357
357
  |---------|-----------------|----------------|
358
358
  | Exclude orchestrator model from Agent Team Personas | Agent Team uses orchestrator model — they provide persona diversity, not epistemic diversity | LLM misreads "do not assign yourself as a reviewer" as applying to Agent Team; it applies only to subprocess CLI |
359
- | Run only Codex GPT-5.4, skip 5.5 | Run both — they catch different things (5.5 found §5 schema contradiction in Phase 2 Case A that no other reviewer caught) | Cost-saving heuristic; roster has both for a reason |
359
+ | Run only Codex GPT-5.6-sol, skip 5.5 | Run both — cross-generation entries catch different things (5.5 found §5 schema contradiction in Phase 2 Case A that no other reviewer caught) | Cost-saving heuristic; roster has both for a reason |
360
360
  | Use a smaller/cheaper model as Agent Team substitute | Use the orchestrator's own model with different personas | Confusing "model diversity" with "persona diversity" — Agent Team is the latter |
361
361
  | Run 3 reviewers instead of 6 (or 5 after exclusion) | Use the full roster from config | Ad-hoc "3 is enough" reasoning; config specifies 6 for empirical reasons |
362
362
 
@@ -580,7 +580,7 @@ which claude 2>/dev/null && echo "claude: available" || echo "claude: NOT FOUND"
580
580
 
581
581
  | Tool | Command | Prompt Input | Output Collection | Model |
582
582
  |------|---------|-------------|-------------------|-------|
583
- | **Codex** | `codex exec -m <model>` | stdin pipe: `cat prompt.md \| codex exec -` | `-o /path/output.md` | GPT-5.5 + GPT-5.4 (both roster entries, `-m` per entry) |
583
+ | **Codex** | `codex exec -m <model>` | stdin pipe: `cat prompt.md \| codex exec -` | `-o /path/output.md` | GPT-5.6-sol + GPT-5.5 (both roster entries, `-m` per entry) |
584
584
  | **Cursor Agent** | `agent -p` | File reference (stdin NOT supported) | stdout redirect: `> output.md` | Composer-2.5 (default) |
585
585
  | **Claude Code** | Agent tool (internal) | Direct prompt string | Write to workspace file | Fable 5 (session) |
586
586
  | **Claude CLI (4.6)** | `claude -p --model claude-opus-4-6 --bare` | stdin pipe: `cat prompt.md \| claude -p --model claude-opus-4-6 --bare` | stdout redirect: `> output.md` | Opus 4.6 |
@@ -597,7 +597,7 @@ Based on cross-evaluation experiment (7 models × 4 tasks + Nomic, 518 CLI calls
597
597
  | **Reviewer: Claude CLI** | Opus 4.6 / Opus 4.8 | (default; config `effort: medium`) | Evaluator quality is effort-independent (low≈high: 8.35 vs 8.16) — per 2026-04-29 policy reviewers stay at default |
598
598
  | **Coding sub-agent** | Opus 4.7 | `--effort medium` | Cost-effective default; use `high` for complex tasks |
599
599
  | **Design sub-agent** | Opus 4.7 | `--effort medium` | Cost-effective default; use `high` for complex tasks |
600
- | **Codex** | GPT-5.5 (default) | (no flag) | Fixed effort |
600
+ | **Codex** | GPT-5.6-sol / GPT-5.5 | (no flag) | Fixed effort |
601
601
  | **Cursor Agent** | Composer-2.5 | (no flag) | Fixed effort |
602
602
 
603
603
  Note (2026-06-10): the effort experiment data is from the Opus 4.6/4.7
@@ -898,11 +898,11 @@ Step 1: Generate review prompt
898
898
  Step 2: Detect environment and models
899
899
  - Run: which codex && which agent && which claude
900
900
  - Detect default models
901
- - Report: "Auto mode: Codex (gpt-5.5, gpt-5.4), Agent (composer-2.5), Claude Team (claude-fable-5), Claude CLI (opus-4.6, opus-4.8)"
901
+ - Report: "Auto mode: Codex (gpt-5.6-sol, gpt-5.5), Agent (composer-2.5), Claude Team (claude-fable-5), Claude CLI (opus-4.6, opus-4.8)"
902
902
 
903
903
  Step 3: Execute N reviews in parallel (default 6 reviewers)
904
904
  - Bash(background): cat prompt.md | codex exec -m gpt-5.5 -C workspace -o log/review_codex_gpt5.5.md -
905
- - Bash(background): cat prompt.md | codex exec -m gpt-5.4 -C workspace -o log/review_codex_gpt5.4.md -
905
+ - Bash(background): cat prompt.md | codex exec -m gpt-5.6-sol -C workspace -o log/review_codex_gpt5.6-sol.md -
906
906
  - Bash(background): agent -p --trust "Read prompt and review..." > log/review_cursor.md
907
907
  - Agent(background): Claude Team (orchestrator model, Fable 5) → write to log/review_claude_team_fable5.md
908
908
  - Bash(background): cat prompt.md | claude -p --model claude-opus-4-6 --bare > log/review_claude_opus4.6.md 2>log/review_claude_opus4.6.stderr.log
@@ -944,10 +944,10 @@ log/{artifact}_review{N}_consensus_{date}.md # Consensus analysis
944
944
  ```
945
945
 
946
946
  LLM identifiers: `claude_team_fable5`, `claude_cli_opus4.6`, `claude_cli_opus4.8`,
947
- `codex_gpt5.5`, `codex_gpt5.4`, `cursor_composer2.5`, `cursor_gpt5.4`,
947
+ `codex_gpt5.6-sol`, `codex_gpt5.5`, `cursor_composer2.5`, `cursor_gpt5.4`,
948
948
  `cursor_premium`
949
949
  (legacy, pre-2026-06-10: `claude_opus4.6`, `claude_team_opus4.6`, `claude_team_opus4.7`,
950
- `claude_cli_opus4.7`, `cursor_composer2`)
950
+ `claude_cli_opus4.7`, `cursor_composer2`; retired 2026-07-23: `codex_gpt5.4`)
951
951
 
952
952
  ## Internal Agent Team Review
953
953
 
@@ -997,6 +997,13 @@ Compression ratio: parallel agent raw → Assembly ≈ 2:1
997
997
  epistemic diversity — the personas that authored R1 findings all approved
998
998
  their own fixes in R2, while subprocess reviewers caught the seams.
999
999
  Design record: `docs/drafts/multi_llm_review_unknowns_pass_v0.3.1_FROZEN.md`
1000
+ - Roster update (v3.6.1, 2026-07-23): Codex gpt-5.6-sol replaces gpt-5.4
1001
+ (retired — its "everyday coding" register is covered by the frontier
1002
+ entries). gpt-5.5 retained as the calibrated anchor for cross-generation
1003
+ comparison. 5.6-sol's reviewer bias profile is uncalibrated — record
1004
+ (a)/(b)/(c) breakdowns per round until a profile accumulates in
1005
+ `multi_llm_reviewer_evaluation`; watch for agentic-coding drift into
1006
+ implementation detail during design-phase reviews ((c) inflation)
1000
1007
 
1001
1008
  **Key insight**: Design reviews and implementation reviews find
1002
- **categorically different bugs**. Both phases are necessary.
1009
+ **categorically different bugs**. Both phases are necessary.
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'distiller'
5
+
6
+ module KairosMcp
7
+ module SkillSets
8
+ module ChainDistillation
9
+ # Slice-2 carrier wiring: the slice-1 carrier seam becomes live
10
+ # here — design v0.4 §1 names the attestation carrier as the
11
+ # presence-visibility channel this slice adds under CD-8.
12
+ #
13
+ # The carrier is the shipped synoptis attestation structure consumed
14
+ # as-is: a ProofEnvelope with the INJECTED, content-independent
15
+ # proof_id = certificate_identity (CD-6), stored through the shipped
16
+ # registry. No behavioral change to the carrier is authored here —
17
+ # the parent design's depends_on re-open condition is not triggered.
18
+ # The envelope is written at issuance and is source-local; outward
19
+ # reachability is a separate property that begins at deposit
20
+ # approval (CD-8, exposure marker in the Depositor; the remote query
21
+ # surface itself is BL-S2-7).
22
+ module CarrierWiring
23
+ module_function
24
+
25
+ # Injectable registry seam (design-constraint tests). Production
26
+ # resolves the shipped synoptis file registry under the data dir.
27
+ @registry = nil
28
+
29
+ class << self
30
+ attr_writer :registry
31
+ end
32
+
33
+ # Explicitly setting the seam to false simulates "no carrier
34
+ # reachable" in tests; nil falls through to the shipped default.
35
+ def registry
36
+ return nil if @registry == false
37
+ @registry || default_registry
38
+ end
39
+
40
+ # Wire Distiller.@carrier to the synoptis registry. Idempotent;
41
+ # returns false (leaving the seam untouched) when synoptis is not
42
+ # installed — DISTILLATION then proceeds carrier-less exactly as
43
+ # in slice 1 (in-instance certification is not degraded), but
44
+ # DEPOSIT under slice 2 requires the carrier and declines without
45
+ # it (CD-8: neither form is optional — enforced at the deposit
46
+ # admission, see Depositor).
47
+ def wire!(registry: nil)
48
+ # Availability is checked WITHOUT construction; the registry
49
+ # itself (whose construction may create its data directory) is
50
+ # resolved lazily INSIDE the lambda — i.e. at issuance step 8,
51
+ # after every verdict — so wiring at tool load causes no
52
+ # persistent effect before judgment (impl review R6 (b)).
53
+ return false unless registry || carrier_available?
54
+ injected = registry
55
+ Distiller.carrier = lambda do |proof_id:, attester_id:, subject_ref:, claim:|
56
+ reg = injected || self.registry
57
+ unless defined?(::Synoptis::ProofEnvelope)
58
+ require_relative '../../../synoptis/lib/synoptis/proof_envelope'
59
+ end
60
+ envelope = ::Synoptis::ProofEnvelope.new(
61
+ proof_id: proof_id,
62
+ attester_id: attester_id.to_s,
63
+ subject_ref: subject_ref.to_s,
64
+ claim: JSON.generate(claim),
65
+ ttl: nil,
66
+ timestamp: Time.now.utc.iso8601
67
+ )
68
+ reg.store_proof(envelope)
69
+ end
70
+ true
71
+ end
72
+
73
+ # CD-8 at the deposit boundary, ADMISSION half: the mirror form
74
+ # must be reachable for a certificate to distribute. STRICTLY
75
+ # read-only — admission writes nothing and creates nothing
76
+ # (verdict precedes every effect, CD-9; impl review R4: even the
77
+ # default registry's directory creation must not happen before
78
+ # the verdict, so availability is checked WITHOUT instantiation).
79
+ def require_carrier!
80
+ return true if carrier_available?
81
+ raise Distiller::Declined, JSON.generate(
82
+ distiller: 'chain_distillation', verdict: 'decline',
83
+ rule: 'cd-8/carrier-unavailable',
84
+ remedy: 'the attestation carrier is required for distribution; install/enable synoptis before depositing'
85
+ )
86
+ end
87
+
88
+ # Availability without side effects: an injected seam answers
89
+ # directly; the default answers iff the synoptis code loads —
90
+ # no registry object (and no data directory) is created here.
91
+ def carrier_available?
92
+ return false if @registry == false
93
+ return true if @registry
94
+ require_relative '../../../synoptis/lib/synoptis/proof_envelope'
95
+ require_relative '../../../synoptis/lib/synoptis/registry/file_registry'
96
+ true
97
+ rescue LoadError
98
+ false
99
+ end
100
+
101
+ # CD-8 at the deposit boundary, POST-VERDICT half: runs only after
102
+ # the certificate verified and the crossing was approved. Backfills
103
+ # the envelope for certificates issued before the wiring was live;
104
+ # an existing envelope whose carried claim is not THIS certificate
105
+ # declines loudly rather than silently shadowing it (a planted
106
+ # pre-seeded proof must not become the exposed carrier answer).
107
+ def ensure_envelope!(certificate, identity, attester_id: 'chain_distillation')
108
+ require_carrier!
109
+ # Post-verdict: instantiating the default registry (which may
110
+ # create its data directory) is an approved effect here.
111
+ reg = registry
112
+ claim_json = JSON.generate(certificate)
113
+ existing = reg.find_proof(identity)
114
+ if existing
115
+ # Semantic comparison in canonical form: byte order of JSON
116
+ # keys must not fabricate a mismatch (impl review R3 — a
117
+ # foreign serializer reordering keys would otherwise decline
118
+ # a legitimate certificate forever). Canonical form is
119
+ # injective on content, so a false MATCH stays impossible.
120
+ same = begin
121
+ Canon.canonical(JSON.parse(existing.claim)) == Canon.canonical(certificate)
122
+ rescue StandardError
123
+ false
124
+ end
125
+ return true if same
126
+ raise Distiller::Declined, JSON.generate(
127
+ distiller: 'chain_distillation', verdict: 'decline',
128
+ rule: 'cd-8/carrier-envelope-mismatch',
129
+ remedy: 'a carrier envelope exists for this identity but does not carry this certificate; inspect the carrier before depositing'
130
+ )
131
+ end
132
+ unless defined?(::Synoptis::ProofEnvelope)
133
+ require_relative '../../../synoptis/lib/synoptis/proof_envelope'
134
+ end
135
+ digest = certificate.dig('claim_core', 'derivation', 'distillate_commitment')
136
+ envelope = ::Synoptis::ProofEnvelope.new(
137
+ proof_id: identity,
138
+ attester_id: attester_id,
139
+ subject_ref: "distillate:#{digest}",
140
+ claim: claim_json,
141
+ ttl: nil,
142
+ timestamp: Time.now.utc.iso8601
143
+ )
144
+ reg.store_proof(envelope)
145
+ true
146
+ end
147
+
148
+ # CD-6/CD-11: the chain is the authoritative revocation channel;
149
+ # the carrier MIRRORS it, never the reverse. Mirroring is
150
+ # fail-visible best-effort — a failed mirror is disclosed lag
151
+ # (BL-S2-2/3), never a rollback of the chain record.
152
+ def mirror_revocation(identity, reason)
153
+ reg = registry
154
+ return { 'status' => 'unavailable', 'note' => 'carrier registry not reachable; mirror pending' } unless reg
155
+ begin
156
+ require_relative '../../../synoptis/lib/synoptis/revocation_manager'
157
+ envelope = reg.find_proof(identity)
158
+ return { 'status' => 'no_envelope', 'note' => 'no carrier envelope for this identity' } unless envelope
159
+ manager = ::Synoptis::RevocationManager.new(registry: reg)
160
+ result = manager.revoke(proof_id: identity, reason: reason,
161
+ revoker_id: envelope.attester_id)
162
+ JSON.parse(JSON.generate(result))
163
+ rescue StandardError => e
164
+ { 'status' => 'mirror_error', 'error' => e.class.name, 'message' => e.message }
165
+ end
166
+ end
167
+
168
+ def default_registry
169
+ require_relative '../../../synoptis/lib/synoptis/proof_envelope'
170
+ require_relative '../../../synoptis/lib/synoptis/registry/file_registry'
171
+ dir = File.join(data_root, 'synoptis_data')
172
+ ::Synoptis::Registry::FileRegistry.new(data_dir: dir)
173
+ rescue LoadError
174
+ nil
175
+ end
176
+
177
+ def data_root
178
+ if defined?(KairosMcp) && KairosMcp.respond_to?(:data_dir)
179
+ KairosMcp.data_dir
180
+ else
181
+ ENV['KAIROS_DATA_DIR'] || File.join(Dir.pwd, '.kairos')
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end