kairos-chain 3.30.0 → 3.32.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: 3ec94fadb095a49a89c0235010c80e37913aaf6781aa16d22a2e5855b1eb154d
4
- data.tar.gz: 38c3b631326b87f07285f9a5f4ecdd64fc22d086b6b08de76d5b2aaf2d7d7040
3
+ metadata.gz: c01be861d1c6a82ee12606f25c5526268351cd4353aa21cf00a0dcb28fc5ed30
4
+ data.tar.gz: 4f2cfd5edc74107aa944ba16a953d30935dca34141806bf3b3b71efe9ed84b51
5
5
  SHA512:
6
- metadata.gz: 7d3d7e9d5f52b58109a795ce4f67243551ad353d18d273d4062034e09bcafe8201559ceabcef16c5ad0d0fc0bca4b8b8629c27cb49e224091ae05ffd5aa364ac
7
- data.tar.gz: a20e8e7a2cb18d8cbcbd76f39c7fa69a9bbd55d4447c16fa289ca8227fd2537325693748e711e46e57693c6572b41f15096dc358368f89911d95eaea3c714ca8
6
+ metadata.gz: a88518aa910493860251c66f0ecdc60ce2d39131626bf2b1e571a3d274560c5e2a876c3ccae6a165f73c1c706ca94022ee9f28369e88fea76e9010ad8f234e7d
7
+ data.tar.gz: 8fe7372a93fdbebe24b9bb6a57e9d2d464966840cfef2c35674319230a6ff05d66f27e6934484bf78b1c5a3fad55de7088639038126e95d1cece92ad1695b5ed
data/CHANGELOG.md CHANGED
@@ -4,6 +4,73 @@ 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.32.0] - 2026-07-03
8
+
9
+ ### Added — multi-host SkillSet projection (Codex + OpenCode)
10
+
11
+ `PluginProjector` gains a `HostProfile` abstraction so projection targets Codex
12
+ CLI and OpenCode in addition to Claude Code. Claude behavior is unchanged
13
+ (byte-compatible; `projection_manifest.json` keeps its legacy name).
14
+
15
+ - Instruction mode: `AGENTS.md` with the mode body inlined for codex/opencode
16
+ (they do not resolve Claude's `@`-import); `CLAUDE.md` `@`-import for claude.
17
+ - Codex: `.codex/skills`, `.codex/agents`, `.codex/hooks.json` (merge-preserving
18
+ user hooks via `_projected_by` tags; re-projection hook commands carry
19
+ `--host codex`). Codex reads `<repo>/.codex/hooks.json` natively — no TOML.
20
+ - OpenCode: reuses `.claude/skills/` (Claude co-use assumption), converts agents
21
+ to `.opencode/agent/` frontmatter; hooks skipped (OpenCode hooks are JS plugins).
22
+ - CLI `kairos-chain mode` and `kairos-plugin-project` gain
23
+ `--host claude|codex|opencode`; MCP tool `plugin_project` gains a `host` param.
24
+ Per-host manifest slots so hosts coexist.
25
+ - Hardened via multi-LLM implementation review (codex/claude/cursor, all
26
+ APPROVE-WITH-CHANGES, 0 P0): hooks merge-preserve, agent-conversion Hash guard
27
+ and list-form `disallowedTools`, separator-boundary path containment, live-file
28
+ region status, malformed `.codex/hooks.json` normalization. 111 projector tests.
29
+
30
+ ### Added — codex mcp-server reviewer backend (opt-in)
31
+
32
+ `CodexMcpAdapter` drives Codex via `codex mcp-server` (stdio JSON-RPC, one-shot,
33
+ read-only sandbox) as an alternative to the `codex exec` CLI adapter. A codex
34
+ reviewer opts in with `backend: mcp` (default stays CLI, backward compatible);
35
+ the backend switch is wired through `llm_call`, `call_router`, and the
36
+ multi_llm_review dispatcher. A/B against the CLI path showed parity (same
37
+ verdict, comparable latency).
38
+
39
+ ## [3.31.0] - 2026-06-11
40
+
41
+ ### Changed — multi_llm_review roster: Fable 5 + Opus 4.6/4.8 (6 reviewers)
42
+
43
+ Default reviewer roster updated for the Fable 5 / Opus 4.8 model generation:
44
+
45
+ - Orchestrator/team slot: `claude-fable-5` (`claude_team_fable5`) replaces
46
+ Opus 4.7. Opus 4.8 added as a second subprocess CLI reviewer
47
+ (`claude_cli_opus4.8`) alongside Opus 4.6, which is retained for its
48
+ documented complementary bias (ambiguity-preserving, self-reference-friendly).
49
+ 4.7 retired: its register is covered by 4.8 and Fable 5.
50
+ - Convergence rules: `4/6 APPROVE` full roster, `3/5 APPROVE` after
51
+ orchestrator exclusion ("exclude" strategy only — "subprocess" keeps the
52
+ full roster; "delegate" re-adds the slot at collect, so 4/6 governs there).
53
+ - `timeout_seconds` raised 300 → 600 (live 6-roster run measured 381s
54
+ wall-clock with `max_concurrent: 2`).
55
+ - Validated by a 2-round self-referential review of the workflow L1 with the
56
+ new roster itself (R1 REVISE → fixes → R2 with 4.6/4.8/codex-5.4 APPROVE,
57
+ including a code-grounded Cursor correction of the exclusion semantics).
58
+
59
+ ### Changed — `multi_llm_review_workflow` L1 v3.5
60
+
61
+ - All roster-dependent sections updated (pre-flight checklist, CLI tool
62
+ matrix, convergence rules, orchestrator self-identification, orchestration
63
+ template, LLM identifiers). Claude CLI 4.6/4.8 rows verified live 2026-06-10.
64
+ - Effort escalation paragraph scoped to coding/design sub-agents and the
65
+ revision phase (reviewers stay at default per the 2026-04-29 policy).
66
+
67
+ ### Fixed — `knowledge_update` size guidance
68
+
69
+ - Removed the "~2 KB MCP stdio limit" warning from the tool description and
70
+ nil-content diagnostic: two ~40 KB updates succeeded over stdio on
71
+ 2026-06-10/11. The old figure generalized a single unreproduced
72
+ nil-content incident; the nil-content detection itself is retained.
73
+
7
74
  ## [3.30.0] - 2026-06-08
8
75
 
9
76
  ### Added — `dream_digest`: derived narrative view over L2/L1 fragments (dream SkillSet v0.3.0)
data/bin/kairos-chain CHANGED
@@ -376,6 +376,11 @@ when 'mode'
376
376
  KAIROS_PROJECT_ROOT env, then cwd if it contains a
377
377
  recognizable project marker (CLAUDE.md, .git/,
378
378
  .claude/, or prior projection manifest).
379
+ --host HOST Target host: claude (default) | codex | opencode.
380
+ claude writes CLAUDE.md with an @-import pointer;
381
+ codex/opencode write AGENTS.md with the mode body
382
+ inlined (they do not resolve @-import). Each host
383
+ uses a separate manifest slot, so hosts can coexist.
379
384
 
380
385
  Notes:
381
386
  - data_dir and consumer project root are decoupled (design v0.2,
@@ -410,6 +415,19 @@ when 'mode'
410
415
  ARGV.delete_at(idx)
411
416
  end
412
417
 
418
+ # 2026-07-02: target host for projection (claude | codex | opencode). Default claude.
419
+ # codex/opencode write AGENTS.md with the mode body inlined (they do not resolve @-import).
420
+ mode_host = 'claude'
421
+ if (idx = ARGV.index('--host'))
422
+ mode_host = ARGV[idx + 1]
423
+ if mode_host.nil? || mode_host.start_with?('--')
424
+ warn "ERROR: --host requires a value (claude, codex, or opencode)."
425
+ exit 1
426
+ end
427
+ ARGV.delete_at(idx + 1)
428
+ ARGV.delete_at(idx)
429
+ end
430
+
413
431
  require 'kairos_mcp/skills_config'
414
432
  require 'kairos_mcp/plugin_projector'
415
433
 
@@ -426,11 +444,15 @@ when 'mode'
426
444
 
427
445
  projector_mode = KairosMcp.projection_mode
428
446
  begin
429
- projector = KairosMcp::PluginProjector.new(project_root, mode: projector_mode, data_dir: KairosMcp.data_dir)
447
+ projector = KairosMcp::PluginProjector.new(project_root, mode: projector_mode, data_dir: KairosMcp.data_dir, host: mode_host)
430
448
  rescue KairosMcp::PluginProjector::CoincidenceRefused => e
431
449
  warn "ERROR: #{e.message}"
432
450
  warn " Use --project-root <path> to designate a directory distinct from the data directory."
433
451
  exit 1
452
+ rescue ArgumentError => e
453
+ warn "ERROR: #{e.message}"
454
+ warn " Valid --host values: claude, codex, opencode."
455
+ exit 1
434
456
  end
435
457
 
436
458
  case mode_action
@@ -464,27 +486,31 @@ when 'mode'
464
486
  exit 1
465
487
  end
466
488
 
489
+ ctx_file = projector.host.context_file
467
490
  puts "Instruction mode projected:"
468
491
  puts " mode : #{instructions_mode}#{version ? " v#{version}" : ''}"
492
+ puts " host : #{projector.host.key} (delivery: #{projector.host.instruction_mode_delivery})"
469
493
  puts " body source : #{body_path}"
470
494
  puts " project root : #{project_root} (source: #{KairosMcp.consumer_project_root_source})"
471
495
  puts " data dir : #{KairosMcp.data_dir}"
472
496
  puts " artifact : #{result[:artifact_path]}"
473
497
  puts " size : #{result[:size_bytes]} bytes"
474
- puts " CLAUDE.md region : #{result[:region_written] ? 'updated' : 'not updated (host file outside project root or unsafe)'}"
498
+ puts " #{ctx_file} region : #{result[:region_written] ? 'updated' : 'not updated (host file outside project root or unsafe)'}"
475
499
  puts ""
476
- puts "Restart Claude Code to apply (CLAUDE.md @-imports resolve at session start)."
500
+ puts "Restart the host to apply (#{ctx_file} is read at session start)."
477
501
  exit 0
478
502
 
479
503
  when 'status'
480
504
  s = projector.instruction_mode_status
505
+ ctx_file = projector.host.context_file
481
506
  puts "Project root : #{project_root} (source: #{KairosMcp.consumer_project_root_source})"
482
507
  puts "Data dir : #{KairosMcp.data_dir}"
508
+ puts "Host : #{projector.host.key}"
483
509
  if s[:active]
484
510
  puts "Instruction mode projection: ACTIVE"
485
511
  puts " mode : #{s[:mode_name]}#{s[:mode_version] ? " v#{s[:mode_version]}" : ''}"
486
512
  puts " artifact : #{s[:artifact_path]} (#{s[:artifact_size]} bytes)"
487
- puts " region : #{s[:region_present] ? 'present in CLAUDE.md' : 'absent'}"
513
+ puts " region : #{s[:region_present] ? "present in #{ctx_file}" : "absent from #{ctx_file}"}"
488
514
  puts " projected : #{s[:projected_at]}"
489
515
  else
490
516
  puts "Instruction mode projection: not active for this project."
@@ -493,11 +519,12 @@ when 'mode'
493
519
 
494
520
  when 'remove'
495
521
  result = projector.remove_projected_instruction_mode!
496
- puts "Instruction mode projection removed:"
522
+ ctx_file = projector.host.context_file
523
+ puts "Instruction mode projection removed (host: #{projector.host.key}):"
497
524
  puts " artifact : #{result[:artifact_removed] ? 'deleted' : 'not present'}"
498
- puts " CLAUDE.md : #{result[:region_removed] ? 'region removed' : 'region not present'}"
525
+ puts " #{ctx_file} : #{result[:region_removed] ? 'region removed' : 'region not present'}"
499
526
  puts ""
500
- puts "Restart Claude Code to apply (CLAUDE.md @-imports resolve at session start)."
527
+ puts "Restart the host to apply (#{ctx_file} is read at session start)."
501
528
  exit 0
502
529
 
503
530
  else
@@ -17,7 +17,27 @@ require_relative '../lib/kairos_mcp/skillset_manager'
17
17
 
18
18
  project_root = KairosMcp.project_root
19
19
  mode = KairosMcp.projection_mode
20
- projector = KairosMcp::PluginProjector.new(project_root, mode: mode)
20
+
21
+ # 2026-07-02: target host for projection (claude | codex | opencode). Default claude.
22
+ # Used both for manual invocation and by projected re-projection hooks (--host <host>).
23
+ host = 'claude'
24
+ if (idx = ARGV.index('--host'))
25
+ host = ARGV[idx + 1]
26
+ if host.nil? || host.start_with?('--')
27
+ warn "ERROR: --host requires a value (claude, codex, or opencode)."
28
+ exit 1
29
+ end
30
+ ARGV.delete_at(idx + 1)
31
+ ARGV.delete_at(idx)
32
+ end
33
+
34
+ begin
35
+ projector = KairosMcp::PluginProjector.new(project_root, mode: mode, host: host)
36
+ rescue ArgumentError => e
37
+ warn "ERROR: #{e.message}"
38
+ warn " Valid --host values: claude, codex, opencode."
39
+ exit 1
40
+ end
21
41
 
22
42
  if ARGV.include?('--status')
23
43
  require 'json'
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+ require_relative '../kairos_chain/chain'
6
+
7
+ module KairosMcp
8
+ module DriftDetection
9
+ # CorrespondenceChecker — INV-A detection floor (Cycle 1, toward by-construction).
10
+ #
11
+ # Checks whether a live L0/L1 artifact still corresponds to its *current
12
+ # recorded provenance*: the content digest stored for that artifact at the
13
+ # head of the constitutive record (the hash chain). This is detection only —
14
+ # it surfaces divergence; it does not prevent edits or gate writes. Those are
15
+ # later cycles (single-source enforcement, record-as-gate).
16
+ #
17
+ # Provenance is rooted in the hash chain, not in the SQLite knowledge_meta
18
+ # cache: INV-A names the chain head as the non-editable anchor. The chain is
19
+ # therefore the single source consulted here; the meta table (when present)
20
+ # is a derived view and is intentionally not used for the comparison.
21
+ #
22
+ # The digest is computed over the *raw file content* (frontmatter included),
23
+ # matching exactly how it was recorded on create/update (a verbatim write,
24
+ # no normalization). Comparing the parsed/stripped body would never match.
25
+ class CorrespondenceChecker
26
+ # Result of a single correspondence check.
27
+ #
28
+ # status:
29
+ # :match live artifact corresponds to recorded provenance
30
+ # :mismatch live content diverged from recorded provenance (silent edit)
31
+ # :missing_record live artifact relied upon, but no recorded provenance exists
32
+ # :missing_artifact recorded/expected artifact is absent at the reliance point
33
+ # :error the check itself could not complete (not a correspondence claim)
34
+ Result = Struct.new(
35
+ :status, :name, :active_digest, :recorded_digest, :message,
36
+ keyword_init: true
37
+ ) do
38
+ def corresponds?
39
+ status == :match
40
+ end
41
+
42
+ # A surfaced non-correspondence per INV-A (divergence, not an internal error).
43
+ def divergence?
44
+ %i[mismatch missing_record missing_artifact].include?(status)
45
+ end
46
+ end
47
+
48
+ class << self
49
+ # Check an L1 knowledge artifact against its recorded provenance.
50
+ #
51
+ # @param name [String] knowledge id (knowledge_id on the chain record)
52
+ # @param md_file_path [String, nil] path to the live .md file relied upon
53
+ # @param storage_backend [Storage::Backend, nil] backend for chain access
54
+ # @return [Result]
55
+ def check_l1(name:, md_file_path:, storage_backend: nil)
56
+ unless md_file_path && File.file?(md_file_path)
57
+ # Relied upon but absent — a missing artifact is itself a
58
+ # non-correspondence under INV-A (the expected set is recorded).
59
+ return Result.new(
60
+ status: :missing_artifact, name: name,
61
+ active_digest: nil, recorded_digest: nil,
62
+ message: "L1 '#{name}': artifact missing at the point of reliance"
63
+ )
64
+ end
65
+
66
+ active = Digest::SHA256.hexdigest(File.read(md_file_path))
67
+ recorded = recorded_digest_for(name, storage_backend)
68
+
69
+ if recorded.nil?
70
+ return Result.new(
71
+ status: :missing_record, name: name,
72
+ active_digest: active, recorded_digest: nil,
73
+ message: "L1 '#{name}': live artifact has no recorded provenance on the chain"
74
+ )
75
+ end
76
+
77
+ if active == recorded
78
+ Result.new(
79
+ status: :match, name: name,
80
+ active_digest: active, recorded_digest: recorded, message: nil
81
+ )
82
+ else
83
+ Result.new(
84
+ status: :mismatch, name: name,
85
+ active_digest: active, recorded_digest: recorded,
86
+ message: "L1 '#{name}': live content diverged from recorded provenance " \
87
+ "(active #{short(active)} ≠ recorded #{short(recorded)})"
88
+ )
89
+ end
90
+ rescue StandardError => e
91
+ Result.new(
92
+ status: :error, name: name,
93
+ active_digest: nil, recorded_digest: nil,
94
+ message: "L1 '#{name}': correspondence check could not complete: #{e.message}"
95
+ )
96
+ end
97
+
98
+ private
99
+
100
+ # The current recorded content digest for a knowledge_id: the next_hash of
101
+ # the most recent knowledge_update record, scanning the chain from head
102
+ # backward. Returns nil when the most recent relevant record removed the
103
+ # 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)
106
+ chain.chain.reverse_each do |block|
107
+ Array(block.data).each do |entry|
108
+ record = parse_entry(entry)
109
+ next unless record.is_a?(Hash)
110
+ next unless record['type'] == 'knowledge_update'
111
+ next unless record['knowledge_id'] == name
112
+
113
+ # First match from the head is the current provenance (may be nil
114
+ # if the artifact was removed — caller treats nil as no record).
115
+ return record['next_hash']
116
+ end
117
+ end
118
+ nil
119
+ end
120
+
121
+ def parse_entry(entry)
122
+ return entry if entry.is_a?(Hash)
123
+ return JSON.parse(entry) if entry.is_a?(String)
124
+
125
+ nil
126
+ rescue JSON::ParserError
127
+ nil
128
+ end
129
+
130
+ def short(digest)
131
+ digest ? digest[0, 12] : '-'
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -23,6 +23,11 @@ module KairosMcp
23
23
  # - Blockchain: Uses the configured storage backend
24
24
  #
25
25
  class KnowledgeProvider
26
+ # Main knowledge directory (constitutively-recorded L1). Exposed so callers
27
+ # can distinguish main-dir knowledge from read-only external SkillSet
28
+ # knowledge, e.g. to scope INV-A correspondence checks to recorded artifacts.
29
+ attr_reader :knowledge_dir
30
+
26
31
  ARCHIVED_DIR = '.archived'
27
32
  ARCHIVE_META_FILE = '.archive_meta.yml'
28
33
  # Backup directories created by upgrade flow (`.bak.<timestamp>`).