kairos-chain 3.31.0 → 3.34.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: 227d024f36839c595295ed0e3b4415c6764750597283b59a21ea1f5e16112210
4
- data.tar.gz: 8fd8767580dbe3db617cbaf4ee0dcfafdb38f5823315f0321bae0c4893b2f45d
3
+ metadata.gz: a393cfbac08011bdd6f7b8cd3129177f535c7507868215d6b85ef2af732e8521
4
+ data.tar.gz: 202af122d66197111412f5cdfc48287872713781602fe7f5f428ec40f3e111a9
5
5
  SHA512:
6
- metadata.gz: 7486fef959f54577c0a13a301112f746f8c440100f11f5d424f27771759ad3024d6d30988b7a9e7c58a4544db1ffe6c2705b3271226d9445f1537dc591f96ef7
7
- data.tar.gz: 8d670bb2a2f5d8d848072101527fcb5b3153c23ea4c5ae1359c1d46c30187011911b32512086e1e1734e72ba5d0d3ae20913499d5d1d8c4b4d1d359b79963281
6
+ metadata.gz: 880c1f5206e0e84ab3b448abd25e5ec3a2fd012840a0f0f2b1d67bd8883fc54da27a945e451de3c2dfc96c36628658db625190b8a8ad8dd8796b1c5b7cbf2eb1
7
+ data.tar.gz: 4a2187976ddddc4e4a27ef51075f38ddeff13539f60c8928150bce1d3d97d607b6bb09aa327179569209ddcf440cae575b53b1460e5f9f350553d2760a6e4a7a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,83 @@ 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.34.0] - 2026-07-03
8
+
9
+ ### Added — L1 knowledge (dev repo): loop_validation
10
+
11
+ New L1 knowledge entry `loop_validation` (v0.4), the mechanical loop-verdict
12
+ methodology distilled from the Autonomous Growth Loop governance design
13
+ (FROZEN v0.3.1, 3-round multi-LLM review) and its slice-1 implementation.
14
+ Body-agnostic reference engine (`scripts/kairos_verdict.py`, fail-closed, no
15
+ LLM), example spec, and an instance-local hermes body adapter appendix.
16
+ Realizes candidate A of the loop-engineering keystone analysis. Frozen at
17
+ v0.4 after 3 multi-LLM review rounds (6/6 unanimous R3); R1 caught a real
18
+ engine crash on malformed specs (execution-verified), R2 caught a wrapper
19
+ constant-key breach and a reason-taxonomy desync.
20
+
21
+ Scoped to `knowledge/` (dev repo) only this release — NOT promoted to
22
+ `templates/` (user distribution) because the hermes adapter appendix carries
23
+ instance-local absolute paths. Promotion to `templates/` awaits proven use
24
+ and path placeholder-izing.
25
+
26
+ ### Design (docs, not gem): Autonomous Growth Loop governance v0.3.1 FROZEN
27
+
28
+ 10 governance invariants for an unattended growth loop (evidence-grounded
29
+ fail-closed verdict, dual stop floor, norm-change asymmetry, human-authored
30
+ run mandate grounded at the human per Prop 9, execution-events-as-attestation
31
+ vs Meta-Ledger-for-capability-changes). Design artifact under `docs/drafts/`.
32
+
33
+ ## [3.33.0] - 2026-07-03
34
+
35
+ ### Added — L1 knowledge: loop_engineering_patterns
36
+
37
+ New gem-bundled L1 knowledge entry `loop_engineering_patterns` (v0.3), sibling
38
+ of `skill_authoring_patterns` (same distill-an-Anthropic-post-through-layers
39
+ method). Distills Claude Code's "Getting Started with Loops" into: §A four loop
40
+ types (two axes: trigger × human presence), §B universal loop-design craft,
41
+ §C KairosChain layer placement (harness vs gem/SkillSet) plus the cross-cutting
42
+ Prop 10 procedural-floor reading that the source omits. Source text is not
43
+ redistributed; a facts-only provenance pointer lives in `references/`.
44
+
45
+ - Shipped to both `knowledge/` and `templates/knowledge/` (mirrors sibling).
46
+ - `skill_authoring_patterns` Related gains a reciprocal sibling link.
47
+ - Frozen after 2 multi-LLM review rounds (6-reviewer roster, delegate strategy).
48
+ R1 caught one genuine (a) factual error (`confidence_exit_threshold` wrongly
49
+ described as unwired — it is wired at agent_step.rb Gate 7) and one overstated
50
+ mechanism status; R2 narrowed to wording-precision findings, resolved in v0.3.
51
+
52
+ ## [3.32.0] - 2026-07-03
53
+
54
+ ### Added — multi-host SkillSet projection (Codex + OpenCode)
55
+
56
+ `PluginProjector` gains a `HostProfile` abstraction so projection targets Codex
57
+ CLI and OpenCode in addition to Claude Code. Claude behavior is unchanged
58
+ (byte-compatible; `projection_manifest.json` keeps its legacy name).
59
+
60
+ - Instruction mode: `AGENTS.md` with the mode body inlined for codex/opencode
61
+ (they do not resolve Claude's `@`-import); `CLAUDE.md` `@`-import for claude.
62
+ - Codex: `.codex/skills`, `.codex/agents`, `.codex/hooks.json` (merge-preserving
63
+ user hooks via `_projected_by` tags; re-projection hook commands carry
64
+ `--host codex`). Codex reads `<repo>/.codex/hooks.json` natively — no TOML.
65
+ - OpenCode: reuses `.claude/skills/` (Claude co-use assumption), converts agents
66
+ to `.opencode/agent/` frontmatter; hooks skipped (OpenCode hooks are JS plugins).
67
+ - CLI `kairos-chain mode` and `kairos-plugin-project` gain
68
+ `--host claude|codex|opencode`; MCP tool `plugin_project` gains a `host` param.
69
+ Per-host manifest slots so hosts coexist.
70
+ - Hardened via multi-LLM implementation review (codex/claude/cursor, all
71
+ APPROVE-WITH-CHANGES, 0 P0): hooks merge-preserve, agent-conversion Hash guard
72
+ and list-form `disallowedTools`, separator-boundary path containment, live-file
73
+ region status, malformed `.codex/hooks.json` normalization. 111 projector tests.
74
+
75
+ ### Added — codex mcp-server reviewer backend (opt-in)
76
+
77
+ `CodexMcpAdapter` drives Codex via `codex mcp-server` (stdio JSON-RPC, one-shot,
78
+ read-only sandbox) as an alternative to the `codex exec` CLI adapter. A codex
79
+ reviewer opts in with `backend: mcp` (default stays CLI, backward compatible);
80
+ the backend switch is wired through `llm_call`, `call_router`, and the
81
+ multi_llm_review dispatcher. A/B against the CLI path showed parity (same
82
+ verdict, comparable latency).
83
+
7
84
  ## [3.31.0] - 2026-06-11
8
85
 
9
86
  ### Changed — multi_llm_review roster: Fable 5 + Opus 4.6/4.8 (6 reviewers)
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'
@@ -26,8 +26,79 @@ module KairosMcp
26
26
  INSTRUCTION_MODE_REL_PATH = 'kairos/instruction_mode.md'
27
27
  INSTRUCTION_MODE_SIZE_WARN = 150 * 1024
28
28
  INSTRUCTION_MODE_SIZE_REFUSE = 256 * 1024
29
+ # Inline delivery (AGENTS.md) can be truncated by a host's context-file byte cap
30
+ # (e.g. Codex project-doc limit), so warn earlier than the artifact thresholds above.
31
+ INSTRUCTION_MODE_INLINE_WARN = 32 * 1024
32
+
33
+ # Host-specific projection profile (2026-07-02, Codex support).
34
+ # Encapsulates per-host layout / context-file / instruction-mode delivery
35
+ # differences so the projection engine stays host-agnostic. See
36
+ # log/20260702_codex_projection_and_mcp_reviewer_implementation_plan.md
37
+ class HostProfile
38
+ attr_reader :key, :output_subdir, :context_file, :instruction_mode_delivery, :manifest_suffix,
39
+ :hooks_strategy, :skill_projection, :agents_subdir, :agents_format
40
+
41
+ # @param skill_projection [Symbol] :own (project skills into this host) or
42
+ # :reuse_claude (host reads .claude/skills/ directly — skip skill projection).
43
+ # @param agents_subdir [String] subdir under output_root for agents ('agents' or 'agent').
44
+ # @param agents_format [Symbol] :claude (verbatim) or :opencode (frontmatter converted).
45
+ def initialize(key:, output_subdir:, context_file:, instruction_mode_delivery:, manifest_suffix:,
46
+ hooks_strategy:, skill_projection:, agents_subdir:, agents_format:)
47
+ @key = key
48
+ @output_subdir = output_subdir
49
+ @context_file = context_file
50
+ @instruction_mode_delivery = instruction_mode_delivery
51
+ @manifest_suffix = manifest_suffix
52
+ @hooks_strategy = hooks_strategy
53
+ @skill_projection = skill_projection
54
+ @agents_subdir = agents_subdir
55
+ @agents_format = agents_format
56
+ end
57
+
58
+ # Legacy manifest names carry no suffix so existing .kairos/ manifests keep working.
59
+ def manifest_filename(base)
60
+ @manifest_suffix ? "#{base}.#{@manifest_suffix}.json" : "#{base}.json"
61
+ end
62
+
63
+ # Claude Code: .claude/ layout, CLAUDE.md with @-import pointer, hooks in settings.json.
64
+ def self.claude_code
65
+ new(key: 'claude', output_subdir: '.claude', context_file: 'CLAUDE.md',
66
+ instruction_mode_delivery: :import, manifest_suffix: nil,
67
+ hooks_strategy: :claude_settings,
68
+ skill_projection: :own, agents_subdir: 'agents', agents_format: :claude)
69
+ end
70
+
71
+ # Codex CLI: .codex/ layout, AGENTS.md with inlined body (Codex does not resolve @-import).
72
+ # Hooks go to <repo>/.codex/hooks.json — same event structure as Claude, read natively by Codex.
73
+ def self.codex
74
+ new(key: 'codex', output_subdir: '.codex', context_file: 'AGENTS.md',
75
+ instruction_mode_delivery: :inline, manifest_suffix: 'codex',
76
+ hooks_strategy: :codex_hooks_json,
77
+ skill_projection: :own, agents_subdir: 'agents', agents_format: :claude)
78
+ end
79
+
80
+ # OpenCode: reads .claude/skills/ natively, so skills are NOT re-projected (Claude co-use
81
+ # assumption). AGENTS.md carries the inlined mode body (shared with Codex). Agents convert
82
+ # to .opencode/agent/ with OpenCode frontmatter. Hooks are JS/TS plugins → skipped.
83
+ def self.opencode
84
+ new(key: 'opencode', output_subdir: '.opencode', context_file: 'AGENTS.md',
85
+ instruction_mode_delivery: :inline, manifest_suffix: 'opencode',
86
+ hooks_strategy: :opencode_plugin,
87
+ skill_projection: :reuse_claude, agents_subdir: 'agent', agents_format: :opencode)
88
+ end
89
+
90
+ def self.for(host)
91
+ return host if host.is_a?(HostProfile)
92
+ case host.to_s
93
+ when 'claude', 'claude_code', 'claude-code' then claude_code
94
+ when 'codex' then codex
95
+ when 'opencode', 'open-code' then opencode
96
+ else raise ArgumentError, "unknown projection host: #{host.inspect}"
97
+ end
98
+ end
99
+ end
29
100
 
30
- attr_reader :mode, :project_root, :output_root, :data_dir
101
+ attr_reader :mode, :project_root, :output_root, :data_dir, :host
31
102
 
32
103
  # Construct a PluginProjector.
33
104
  #
@@ -39,14 +110,15 @@ module KairosMcp
39
110
  # data_dir = project_root/.kairos is used for manifest location (backward-compat).
40
111
  #
41
112
  # @raise [CoincidenceRefused] when project_root and data_dir resolve to the same real path
42
- def initialize(project_root, mode: :auto, data_dir: nil)
113
+ def initialize(project_root, mode: :auto, data_dir: nil, host: :claude)
43
114
  @project_root = project_root
44
115
  @data_dir = data_dir || File.join(project_root, '.kairos')
45
116
  enforce_no_coincidence!
46
117
  @mode = resolve_mode(mode)
47
- @output_root = @mode == :plugin ? project_root : File.join(project_root, '.claude')
48
- @manifest_path = File.join(@data_dir, 'projection_manifest.json')
49
- @instruction_mode_manifest_path = File.join(@data_dir, 'instruction_mode_manifest.json')
118
+ @host = HostProfile.for(host)
119
+ @output_root = @mode == :plugin ? project_root : File.join(project_root, @host.output_subdir)
120
+ @manifest_path = File.join(@data_dir, @host.manifest_filename('projection_manifest'))
121
+ @instruction_mode_manifest_path = File.join(@data_dir, @host.manifest_filename('instruction_mode_manifest'))
50
122
  end
51
123
 
52
124
  # Raised when project_root and data_dir resolve to the same real path (design Inv 3).
@@ -62,16 +134,20 @@ module KairosMcp
62
134
  current_outputs = {}
63
135
  merged_hooks = @mode == :plugin ? load_seed_hooks : { 'hooks' => {} }
64
136
 
137
+ # OpenCode reads .claude/skills/ directly (Claude co-use assumption), so it reuses the
138
+ # Claude skill projection instead of duplicating skills into .opencode/skills/.
139
+ reuse_skills = @host.skill_projection == :reuse_claude
140
+
65
141
  enabled_skillsets.each do |ss|
66
142
  next unless ss.has_plugin?
67
143
 
68
144
  plugin_dir = File.join(ss.path, 'plugin')
69
- project_skill!(ss, plugin_dir, current_outputs)
145
+ project_skill!(ss, plugin_dir, current_outputs) unless reuse_skills
70
146
  project_agents!(ss, plugin_dir, current_outputs)
71
147
  collect_hooks!(ss, plugin_dir, merged_hooks)
72
148
  end
73
149
 
74
- project_knowledge_meta_skill!(knowledge_entries, current_outputs)
150
+ project_knowledge_meta_skill!(knowledge_entries, current_outputs) unless reuse_skills
75
151
  write_merged_hooks!(merged_hooks, current_outputs)
76
152
  cleanup_stale!(previous_manifest, current_outputs)
77
153
  save_manifest(current_outputs, enabled_skillsets, knowledge_entries)
@@ -130,6 +206,11 @@ module KairosMcp
130
206
  size = body.bytesize
131
207
  raise InstructionModeTooLarge.new(size, INSTRUCTION_MODE_SIZE_REFUSE) if size > INSTRUCTION_MODE_SIZE_REFUSE
132
208
  warn "[PluginProjector] WARNING: instruction mode body is #{size} bytes (warn threshold #{INSTRUCTION_MODE_SIZE_WARN})" if size > INSTRUCTION_MODE_SIZE_WARN
209
+ if @host.instruction_mode_delivery == :inline && size > INSTRUCTION_MODE_INLINE_WARN
210
+ warn "[PluginProjector] WARNING: inlining #{size} bytes into #{@host.context_file}; " \
211
+ "host '#{@host.key}' may cap the context-file read (e.g. Codex project-doc byte limit). " \
212
+ "Raise the host's context-file byte cap if the projected mode body appears truncated."
213
+ end
133
214
 
134
215
  artifact_path = File.join(@output_root, INSTRUCTION_MODE_REL_PATH)
135
216
  raise "instruction mode artifact path outside output_root: #{artifact_path}" unless safe_path?(artifact_path)
@@ -137,7 +218,7 @@ module KairosMcp
137
218
  FileUtils.mkdir_p(File.dirname(artifact_path))
138
219
  atomic_write(artifact_path, body)
139
220
 
140
- region_written = merge_instruction_mode_region!(mode_name, mode_version, artifact_path)
221
+ region_written = merge_instruction_mode_region!(mode_name, mode_version, artifact_path, body)
141
222
 
142
223
  save_instruction_mode_manifest(
143
224
  'mode_name' => mode_name,
@@ -184,11 +265,20 @@ module KairosMcp
184
265
  mode_version: manifest['mode_version'],
185
266
  artifact_path: manifest['artifact_path'],
186
267
  artifact_size: manifest['artifact_size'],
187
- region_present: manifest['region_present'],
268
+ # Verify against the actual context file, not just the manifest: another host
269
+ # sharing AGENTS.md may have stripped the region since this host projected.
270
+ region_present: context_region_present?,
188
271
  projected_at: manifest['projected_at']
189
272
  }
190
273
  end
191
274
 
275
+ # True if the managed marker region currently exists in this host's context file.
276
+ def context_region_present?
277
+ path = claudemd_path
278
+ return false unless File.exist?(path)
279
+ File.read(path).include?(INSTRUCTION_MODE_MARKER_BEGIN)
280
+ end
281
+
192
282
  # Raised when a mode body exceeds the hard refusal threshold.
193
283
  class InstructionModeTooLarge < StandardError
194
284
  def initialize(size, limit)
@@ -283,14 +373,49 @@ module KairosMcp
283
373
  return unless Dir.exist?(agents_src)
284
374
  return unless safe_name?(ss.name)
285
375
  Dir.glob(File.join(agents_src, '*.md')).each do |f|
286
- target = File.join(@output_root, 'agents', "#{ss.name}-#{File.basename(f)}")
376
+ target = File.join(@output_root, @host.agents_subdir, "#{ss.name}-#{File.basename(f)}")
287
377
  next unless safe_path?(target)
288
378
  FileUtils.mkdir_p(File.dirname(target))
289
- atomic_write(target, File.read(f))
379
+ content = File.read(f)
380
+ content = convert_agent_to_opencode(content) if @host.agents_format == :opencode
381
+ atomic_write(target, content)
290
382
  outputs[target] = { 'source' => f, 'type' => 'agent', 'skillset' => ss.name }
291
383
  end
292
384
  end
293
385
 
386
+ # Convert a Claude Code agent (.md + YAML frontmatter) to OpenCode agent frontmatter.
387
+ # - drop `name` (OpenCode derives the agent name from the filename)
388
+ # - drop `model` (OpenCode subagents inherit the caller's model — free-LLM friendly)
389
+ # - `disallowedTools: A, B` -> `tools: { a: false, b: false }`
390
+ # - add `mode: subagent`
391
+ # Body (system prompt) is preserved verbatim. Returns input unchanged if no frontmatter.
392
+ def convert_agent_to_opencode(content)
393
+ m = content.match(/\A---\s*\n(.*?)\n---\s*\n?(.*)\z/m)
394
+ return content unless m
395
+
396
+ require 'yaml'
397
+ src = begin
398
+ YAML.safe_load(m[1])
399
+ rescue StandardError
400
+ return content
401
+ end
402
+ return content unless src.is_a?(Hash)
403
+ body = m[2]
404
+
405
+ out = {}
406
+ out['description'] = src['description'] if src['description']
407
+ out['mode'] = 'subagent'
408
+ if src['disallowedTools']
409
+ # Accept both comma-string ("Write, Edit") and YAML list ([Write, Edit]) forms.
410
+ disabled = Array(src['disallowedTools']).flat_map { |t| t.to_s.split(',') }
411
+ .map { |t| t.strip.downcase }.reject(&:empty?)
412
+ out['tools'] = disabled.each_with_object({}) { |t, h| h[t] = false } unless disabled.empty?
413
+ end
414
+
415
+ front = YAML.dump(out).sub(/\A---\n/, '').rstrip
416
+ "---\n#{front}\n---\n\n#{body.lstrip}"
417
+ end
418
+
294
419
  # =========================================================================
295
420
  # L1 Knowledge meta skill
296
421
  # =========================================================================
@@ -374,10 +499,79 @@ module KairosMcp
374
499
  if @mode == :plugin
375
500
  write_hooks_file!(merged_hooks, outputs)
376
501
  else
377
- write_hooks_to_settings!(merged_hooks, outputs)
502
+ case @host.hooks_strategy
503
+ when :codex_hooks_json then write_hooks_to_codex_json!(merged_hooks, outputs)
504
+ when :opencode_plugin then skip_hooks_for_plugin_host!(merged_hooks)
505
+ else write_hooks_to_settings!(merged_hooks, outputs)
506
+ end
378
507
  end
379
508
  end
380
509
 
510
+ # Codex reads <repo>/.codex/hooks.json (same event structure as Claude hooks).
511
+ # KairosChain owns this file (overwrite), mirroring plugin-mode hooks/hooks.json.
512
+ # kairos-plugin-project commands are rewritten to re-project this same host.
513
+ def write_hooks_to_codex_json!(merged_hooks, outputs)
514
+ hooks_file = File.join(@output_root, 'hooks.json')
515
+ existing = load_settings(hooks_file) # {} when absent, nil on parse error
516
+ return if existing.nil?
517
+
518
+ # A hand-authored file may hold a malformed 'hooks' (non-Hash, or non-Array
519
+ # event values). Normalize before merging so projection never crashes; a
520
+ # malformed 'hooks' carries no preservable user hooks anyway.
521
+ existing['hooks'] = {} unless existing['hooks'].is_a?(Hash)
522
+ existing['hooks'].transform_values! { |v| v.is_a?(Array) ? v : [] }
523
+
524
+ # Strip previously projected entries, preserving user-authored (untagged) hooks —
525
+ # mirrors the Claude settings.json path so re-projection never destroys user hooks.
526
+ existing['hooks'].each_value do |handlers|
527
+ handlers.reject! { |h| h.is_a?(Hash) && h['_projected_by'] == PROJECTED_BY }
528
+ end
529
+ existing['hooks'].delete_if { |_, v| v.empty? }
530
+
531
+ projected = rewrite_hook_commands_for_host(merged_hooks)
532
+ unless projected['hooks'].empty?
533
+ projected['hooks'].each do |event, handlers|
534
+ existing['hooks'][event] ||= []
535
+ existing['hooks'][event].concat(handlers.map { |h| h.merge('_projected_by' => PROJECTED_BY) })
536
+ end
537
+ end
538
+
539
+ existing.delete('hooks') if existing['hooks'].empty?
540
+ # Only delete the file if nothing (projected or user) remains — never clobber user content.
541
+ if existing.empty?
542
+ FileUtils.rm_f(hooks_file)
543
+ return
544
+ end
545
+ FileUtils.mkdir_p(File.dirname(hooks_file))
546
+ atomic_write(hooks_file, JSON.pretty_generate(existing))
547
+ outputs[hooks_file] = { 'type' => 'hooks_codex_json' }
548
+ end
549
+
550
+ # OpenCode hooks are JS/TS plugins, not a declarative file — cannot be projected here.
551
+ def skip_hooks_for_plugin_host!(merged_hooks)
552
+ return if merged_hooks['hooks'].empty?
553
+ warn "[PluginProjector] WARNING: host '#{@host.key}' uses plugin-based hooks (JS/TS); " \
554
+ "skipping projection of #{merged_hooks['hooks'].size} hook event(s). Author an OpenCode plugin instead."
555
+ end
556
+
557
+ # Ensure projected re-projection hooks target THIS host, not the default (claude).
558
+ # Deep-copies so the shared merged_hooks (used by other hosts) is not mutated.
559
+ def rewrite_hook_commands_for_host(merged_hooks)
560
+ copy = JSON.parse(JSON.generate(merged_hooks))
561
+ copy.fetch('hooks', {}).each_value do |handlers|
562
+ next unless handlers.is_a?(Array)
563
+ handlers.each do |h|
564
+ Array(h['hooks']).each do |inner|
565
+ cmd = inner['command']
566
+ next unless cmd.is_a?(String) && cmd.include?('kairos-plugin-project') && !cmd.include?('--host')
567
+ # Insert right after the binary token so compound commands (a && b) stay correct.
568
+ inner['command'] = cmd.sub('kairos-plugin-project', "kairos-plugin-project --host #{@host.key}")
569
+ end
570
+ end
571
+ end
572
+ copy
573
+ end
574
+
381
575
  # Plugin mode: write hooks/hooks.json
382
576
  def write_hooks_file!(merged_hooks, outputs)
383
577
  hooks_dir = File.join(@output_root, 'hooks')
@@ -457,12 +651,12 @@ module KairosMcp
457
651
  current_files = current_outputs.keys
458
652
  stale = previous_files - current_files
459
653
  stale.each do |f|
460
- # Path safety: only delete files under output_root
461
- canonical = File.expand_path(f)
462
- unless canonical.start_with?(File.expand_path(@output_root))
654
+ # Path safety: only delete files under output_root (separator-boundary check)
655
+ unless within_output_root?(f)
463
656
  warn "[PluginProjector] WARNING: skipping stale cleanup of '#{f}' (outside output_root)"
464
657
  next
465
658
  end
659
+ canonical = File.expand_path(f)
466
660
  FileUtils.rm_f(canonical)
467
661
  dir = File.dirname(canonical)
468
662
  FileUtils.rmdir(dir) if Dir.exist?(dir) && Dir.empty?(dir)
@@ -507,7 +701,7 @@ module KairosMcp
507
701
  def find_orphaned_files(manifest_outputs)
508
702
  projected_dirs = [
509
703
  File.join(@output_root, 'skills'),
510
- File.join(@output_root, 'agents')
704
+ File.join(@output_root, @host.agents_subdir)
511
705
  ]
512
706
  actual_files = projected_dirs.flat_map do |dir|
513
707
  next [] unless Dir.exist?(dir)
@@ -532,10 +726,17 @@ module KairosMcp
532
726
  true
533
727
  end
534
728
 
729
+ # True if path is exactly output_root or a descendant. Uses a separator boundary
730
+ # so a sibling like '<root>/.codex_backup' does not match the '<root>/.codex' prefix.
731
+ def within_output_root?(path)
732
+ root = File.expand_path(@output_root)
733
+ canonical = File.expand_path(path)
734
+ canonical == root || canonical.start_with?(root + File::SEPARATOR)
735
+ end
736
+
535
737
  # Validate target path is under output_root
536
738
  def safe_path?(target)
537
- canonical = File.expand_path(target)
538
- unless canonical.start_with?(File.expand_path(@output_root))
739
+ unless within_output_root?(target)
539
740
  warn "[PluginProjector] WARNING: path '#{target}' is outside output_root, skipping"
540
741
  return false
541
742
  end
@@ -570,16 +771,26 @@ module KairosMcp
570
771
 
571
772
  # Merge or insert the managed marker region in project-root CLAUDE.md.
572
773
  # Returns true if the region is now present, false otherwise.
573
- def merge_instruction_mode_region!(mode_name, mode_version, artifact_path)
774
+ def merge_instruction_mode_region!(mode_name, mode_version, artifact_path, body)
574
775
  claudemd = claudemd_path
575
776
  return false unless safe_claudemd_path?(claudemd)
576
777
 
577
- import_path = relative_import_path(artifact_path)
578
778
  header = "<!-- Active mode: #{mode_name}#{mode_version ? " v#{mode_version}" : ''} | source: .kairos/skills/#{mode_name}.md -->"
779
+ # Claude Code resolves @-import at session start (pointer). Codex does not,
780
+ # so its body is inlined directly into AGENTS.md.
781
+ payload = case @host.instruction_mode_delivery
782
+ when :inline then body
783
+ else "@#{relative_import_path(artifact_path)}"
784
+ end
785
+ if @host.instruction_mode_delivery == :inline &&
786
+ (body.include?(INSTRUCTION_MODE_MARKER_BEGIN) || body.include?(INSTRUCTION_MODE_MARKER_END))
787
+ warn "[PluginProjector] WARNING: inlined mode body contains an instruction-mode marker; " \
788
+ "re-projection region detection may be unreliable for #{@host.context_file}."
789
+ end
579
790
  region = [
580
791
  INSTRUCTION_MODE_MARKER_BEGIN,
581
792
  header,
582
- "@#{import_path}",
793
+ payload,
583
794
  INSTRUCTION_MODE_MARKER_END
584
795
  ].join("\n")
585
796
 
@@ -607,9 +818,9 @@ module KairosMcp
607
818
  true
608
819
  end
609
820
 
610
- # Project-root CLAUDE.md absolute path.
821
+ # Project-root context file absolute path (CLAUDE.md for Claude, AGENTS.md for Codex).
611
822
  def claudemd_path
612
- File.join(@project_root, 'CLAUDE.md')
823
+ File.join(@project_root, @host.context_file)
613
824
  end
614
825
 
615
826
  # Path safety for the host file: must be exactly <project_root>/CLAUDE.md.
@@ -618,7 +829,7 @@ module KairosMcp
618
829
  canonical = File.expand_path(path)
619
830
  expected = File.expand_path(claudemd_path)
620
831
  return true if canonical == expected
621
- warn "[PluginProjector] WARNING: refusing to mutate non-project CLAUDE.md at '#{path}'"
832
+ warn "[PluginProjector] WARNING: refusing to mutate non-project #{@host.context_file} at '#{path}'"
622
833
  false
623
834
  end
624
835
 
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.31.0"
2
+ VERSION = "3.34.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: loop_engineering_patterns
3
+ description: Use before designing or classifying an agent loop in KairosChain — to pick a loop type, apply loop-design craft, and place each loop at the right layer under the Prop 10 floor. Claude Code's four loop types + design principles, extended with the governance constraint Anthropic omits.
4
+ version: "0.3"
5
+ tags: [loops, agent, autonomy, taxonomy, layer-placement, provenance]
6
+ ---
7
+
8
+ # Loop Engineering Patterns
9
+
10
+ Distilled from Claude Code's post [Getting Started with Loops](https://claude.com/blog/getting-started-with-loops) (Anthropic, 2026). Local provenance / facts index: `references/claude_code_loops_2026.md` (pointer; canonical text is the URL, not redistributed here). Sibling method: [[skill_authoring_patterns]] — same distill-an-Anthropic-post-through-KairosChain-layers move.
11
+
12
+ ## When to reference this entry
13
+ Open *before* designing or classifying an agent loop — not a runtime tool:
14
+ - New agent loop / autonomous cycle → pick a §A type, apply §B craft, then place it via §C.
15
+ - "Which layer owns this loop?" (harness vs gem/SkillSet, plus the cross-cutting L0 governance floor) → §C table + the L0 note below it.
16
+ - Strengthening the `agent` OODA loop (self-verification, semantic stop) → §B + §C evaluator/verifier notes.
17
+ - Explaining why KairosChain's autonomous mode keeps human checkpoints → §C Prop 10 floor.
18
+ - Not for: skill lifecycle/maturation → [[agent_skill_evolution_guide]]; general layer rules → [[layer_placement_guide]].
19
+
20
+ ## A. Four loop types (base-level classification)
21
+ Two axes generate the four types: **trigger** (user prompt / goal spec / schedule-or-event) and **human presence** (present / absent). The autonomous type is the no-human variant of the schedule/event trigger, not a fourth independent trigger — kept as a peer type here to mirror the source taxonomy.
22
+ 1. **Turn-driven** — trigger: user prompt; stop: task done or needs context; for short exploratory work.
23
+ 2. **Goal-driven** — trigger: goal spec (`/goal`); stop: goal met OR max turns; a separate evaluator checking exit criteria raises reliability; for tasks with verifiable exit.
24
+ 3. **Time-driven** — trigger: schedule/interval (`/loop`, `/schedule`); stop: user cancels or work done; for recurring work / external monitoring.
25
+ 4. **Autonomous** — trigger: events/schedule, no human present; stop: each task exits on goal, routine runs until disabled; for well-defined recurring streams (triage, migrations).
26
+
27
+ Micro (small) and macro (broad) loops compose; match loop complexity to problem abstraction.
28
+
29
+ ## B. Design craft (universal — adopt as-is)
30
+ Output quality tracks the surrounding harness, not the model alone. Keep the codebase clean so the loop follows existing patterns. **Encode verification as a skill with quantitative checks** so the loop measures its own work. Keep docs accessible/current. **Use a second, independent agent for review** (fresh context, separate session/model). Set explicit success/stop criteria and max-turn caps to bound cost; script deterministic steps instead of re-reasoning them; monitor token spend.
31
+
32
+ ## C. KairosChain reading (layer placement + the floor Anthropic omits)
33
+ The four types are harness-framed (`/goal`, `/loop`, `/schedule`, `/usage`, auto mode are Claude Code mechanisms). KairosChain already implements most of the substance across layers — so the work is mostly *placement*, not adoption; one item is still design-stage (flagged in the table):
34
+
35
+ | Type | KairosChain home | Existing mechanism (status) |
36
+ |---|---|---|
37
+ | Turn-driven | harness (Claude Code) | interactive session (shipped) |
38
+ | Goal-driven | gem / SkillSet | `agent` OODA loop with explicit goal + max-step cap; goal-satisfaction judged from reflect-phase confidence/remaining, human-mediated stop (shipped). The *wired* auto-exit on confidence is the Autonomous row's Gate 7, not this path. |
39
+ | Time-driven | harness / gem | harness: `/loop`, `/schedule`, ScheduleWakeup/Cron (shipped, harness-layer); gem: routines × autonomous-growth (design, not yet shipped) |
40
+ | Autonomous | gem | `autonomos_cycle`/`loop`, `autoexec`, and the `agent` autonomous path — whose Gate 7 confidence early-exit (`confidence_exit_threshold`, wired in `agent_step.rb`) terminates on goal satisfaction (shipped) |
41
+
42
+ **L0 (cross-cutting, not a per-type home):** the Prop 10 procedural floor binds every row — it is a governance constraint over all four types, not a home for any one. See the paragraph below.
43
+
44
+ The article's craft maps onto existing mechanisms (illustratively, not as strict 1:1 equivalences): its "separate evaluator" for goal-driven ≈ the agent's reflect-phase confidence evaluation (Observe/Reflect), not `multi_llm_review`; "verification as skill" ≈ `introspection_check` / `definition_verify`; "second agent for review" ≈ `multi_llm_review` (independent session). These pre-exist; they are not new imports.
45
+
46
+ **The governance floor Anthropic omits.** Every article loop optimizes for completion/throughput; none carries a consent/audit constraint. KairosChain adds a cross-cutting axis: **every loop, whatever its type, is bound by the Prop 10 procedural floor** — no loop may bypass consent, harm protection, or audit, and any autonomous norm must stay recordable and contestable. (Prop 10 is currently provisional/inline in masa mode, not yet promoted to L0; the floor it names is nonetheless the intended binding.) Concretely, the autonomous type's "run until disabled, no human present" cannot be adopted verbatim: KairosChain's autonomous loop carries human checkpoints and safety gates (the `agent` skill), and a stop is not a bare exit but a constitutively recorded Kairos moment (Prop 5). This floor is the KairosChain-specific delta — the article has no place for it because a weight-hosted assistant has no revisable governance surface. Consistent with (not proof of) masa mode's harness-as-cultivation-surface bet in [[skill_authoring_patterns]] §C.
47
+
48
+ ## Related
49
+ [[skill_authoring_patterns]] · [[layer_placement_guide]] · [[agent_skill_evolution_guide]] (instance-local, not gem-bundled) · [[kairoschain_self_development]] · kairoschain_meta_philosophy (Prop 5 constitutive recording). Prop 10 procedural floor: current authoritative home is masa mode § Proposition 10 (inline, provisional); migration into L1/L0 is pending.
@@ -0,0 +1,23 @@
1
+ # [Source pointer] Getting Started with Loops
2
+
3
+ > **Provenance / 来歴**
4
+ > - **Source (official):** https://claude.com/blog/getting-started-with-loops
5
+ > - **Publisher:** Anthropic / Claude Code
6
+ > - **Retrieved:** 2026-07-03
7
+ > - **Companion:** JP video walkthrough ("【神記事】Claude Code 公式が遂に『ループエンジニアリング』を解説") — user-supplied summary; canonical URL not recorded.
8
+ >
9
+ > **This is a pointer, not a copy.** The article's text is Anthropic's
10
+ > copyrighted content and is **not redistributed in this gem**. The canonical
11
+ > source is the URL above.
12
+
13
+ ## What the source covers (facts, for indexing)
14
+
15
+ **Four loop types:** turn-driven (trigger: user prompt; stop: task done or needs context) · goal-driven (`/goal`; stop: goal met OR max turns; a separate evaluator model checks exit criteria) · time-driven (`/loop`, `/schedule`; stop: user cancels or work done) · autonomous/proactive (events/schedule, no human present; each task exits on goal, routine runs until disabled).
16
+
17
+ **Design principles:** maintain clean codebases; encode verification as skills with quantitative checks; keep documentation accessible/current; deploy a second independent agent for review. Micro/macro loops compose — match loop complexity to problem abstraction.
18
+
19
+ **Operational notes:** bound cost with explicit success criteria + max-turn caps; script deterministic work rather than re-reasoning it; monitor spend (`/usage`, `/goal` metrics, `/workflows`); match scheduling interval to actual change frequency; auto mode removes manual permission for routine execution.
20
+
21
+ **Slash examples:** `/goal get the homepage Lighthouse score to 90 or above, stop after 5 tries` · `/loop 5m check my PR, address review comments, and fix failing CI` · `/schedule every hour: check #project-feedback for bug reports`.
22
+
23
+ For the KairosChain reading through layers, see `loop_engineering_patterns.md` §A/§B/§C. For full prose, open the URL above.
@@ -44,4 +44,4 @@ The real difference is *where philosophy lives*: closed in the core (Anthropic)
44
44
  **Why this bet (operational vs decorative).** Harness-hosted norms are readable/recorded/revisable where weight-baked norms are opaque (Prop 10, provisional in masa.md) — arguably the right home even post-fine-tuning. Hiring a model ≠ cultivating one; the harness is the weight-less user's only cultivation surface. Norms must stay operational (hooks, introspection_check), not decorative (philosophy theater). Full dialogue: `references/why_harness_norms_bet.md`.
45
45
 
46
46
  ## Related
47
- [[agent_skill_evolution_guide]] · skillset_implementation_quality_guide · kairoschain_meta_philosophy (Prop 1; Prop 10 provisional in masa.md).
47
+ [[agent_skill_evolution_guide]] · [[loop_engineering_patterns]] (sibling: same distill-an-Anthropic-post-through-layers method) · skillset_implementation_quality_guide · kairoschain_meta_philosophy (Prop 1; Prop 10 provisional in masa.md).
@@ -135,6 +135,7 @@ module KairosMcp
135
135
  c['dispatch_id'] = args['dispatch_id'] if args['dispatch_id']
136
136
  c['sandbox_mode'] = true if args['sandbox_mode']
137
137
  c['effort'] = args['effort'] if args['effort']
138
+ c['backend'] = args['backend'] if args['backend']
138
139
  c
139
140
  end
140
141
 
@@ -147,8 +148,13 @@ module KairosMcp
147
148
  require_relative 'claude_code_adapter'
148
149
  ClaudeCodeAdapter.new(config)
149
150
  when 'codex'
150
- require_relative 'codex_adapter'
151
- CodexAdapter.new(config)
151
+ if config['backend'].to_s == 'mcp'
152
+ require_relative 'codex_mcp_adapter'
153
+ CodexMcpAdapter.new(config)
154
+ else
155
+ require_relative 'codex_adapter'
156
+ CodexAdapter.new(config)
157
+ end
152
158
  when 'cursor'
153
159
  require_relative 'cursor_adapter'
154
160
  CursorAdapter.new(config)
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require_relative 'adapter'
6
+
7
+ module KairosMcp
8
+ module SkillSets
9
+ module LlmClient
10
+ # Adapter that drives OpenAI Codex through its MCP server (`codex mcp-server`)
11
+ # rather than `codex exec`. Spawns the server over stdio, performs the MCP
12
+ # handshake, invokes the `codex` tool once (sandbox read-only by default so a
13
+ # reviewer/eval call cannot mutate the workspace), and returns the normalized
14
+ # response. One-shot: the server is started and torn down per call.
15
+ #
16
+ # Companion to CodexAdapter (CLI). Selectable as the codex backend in
17
+ # multi_llm_review / cross-eval to A/B the MCP path against `codex exec`.
18
+ class CodexMcpAdapter < Adapter
19
+ DEFAULT_TIMEOUT = 300
20
+ PROTOCOL_VERSION = '2025-06-18'
21
+
22
+ def call(messages:, system: nil, tools: nil, model: nil,
23
+ max_tokens: nil, temperature: nil, output_schema: nil)
24
+ prompt = build_prompt(messages, system, output_schema)
25
+ sandbox = @config&.dig('sandbox') || 'read-only' # reviewers/evals must not mutate
26
+ timeout = (@config&.dig('timeout_seconds') || DEFAULT_TIMEOUT).to_i
27
+
28
+ result = run_codex_tool(prompt: prompt, model: model, sandbox: sandbox, timeout: timeout)
29
+
30
+ {
31
+ 'content' => result[:text],
32
+ 'tool_use' => nil,
33
+ 'stop_reason' => 'end_turn',
34
+ 'model' => model || 'codex-mcp-default',
35
+ 'thread_id' => result[:thread_id],
36
+ 'input_tokens' => nil,
37
+ 'output_tokens' => nil
38
+ }
39
+ rescue ApiError
40
+ raise
41
+ rescue Errno::ENOENT
42
+ raise ApiError.new('codex CLI not found. Install: https://github.com/openai/codex',
43
+ provider: 'codex', retryable: false)
44
+ rescue StandardError => e
45
+ raise ApiError.new("codex mcp error: #{e.message}", provider: 'codex')
46
+ end
47
+
48
+ private
49
+
50
+ def run_codex_tool(prompt:, model:, sandbox:, timeout:)
51
+ args = { 'prompt' => prompt, 'sandbox' => sandbox,
52
+ 'approval-policy' => (@config&.dig('approval_policy') || 'never') }
53
+ args['model'] = model.to_s if model && !model.to_s.empty?
54
+
55
+ stdin, stdout, wait = Open3.popen2('codex', 'mcp-server')
56
+ deadline = now + timeout
57
+ begin
58
+ send_msg(stdin, id: 1, method: 'initialize', params: {
59
+ 'protocolVersion' => PROTOCOL_VERSION, 'capabilities' => {},
60
+ 'clientInfo' => { 'name' => 'kairos-chain', 'version' => '0.1' }
61
+ })
62
+ read_result(stdout, 1, deadline)
63
+ send_msg(stdin, method: 'notifications/initialized')
64
+
65
+ send_msg(stdin, id: 2, method: 'tools/call',
66
+ params: { 'name' => 'codex', 'arguments' => args })
67
+ resp = read_result(stdout, 2, deadline)
68
+ extract(resp)
69
+ ensure
70
+ stdin.close unless stdin.closed?
71
+ unless wait.join(3)
72
+ begin
73
+ Process.kill('TERM', wait.pid)
74
+ rescue StandardError
75
+ nil
76
+ end
77
+ end
78
+ stdout.close unless stdout.closed?
79
+ end
80
+ end
81
+
82
+ def send_msg(io, method:, id: nil, params: nil)
83
+ msg = { 'jsonrpc' => '2.0', 'method' => method }
84
+ msg['id'] = id if id
85
+ msg['params'] = params if params
86
+ io.puts(JSON.generate(msg))
87
+ io.flush
88
+ end
89
+
90
+ # Read newline-delimited JSON-RPC until a response with the given id arrives,
91
+ # skipping notifications/progress and unrelated lines. Raises on timeout/error.
92
+ def read_result(io, id, deadline)
93
+ loop do
94
+ remaining = deadline - now
95
+ timed_out!(id) if remaining <= 0
96
+ timed_out!(id) unless IO.select([io], nil, nil, remaining)
97
+ line = io.gets
98
+ raise ApiError.new('codex mcp server closed the connection unexpectedly',
99
+ provider: 'codex', retryable: true) if line.nil?
100
+ line = line.strip
101
+ next if line.empty?
102
+ begin
103
+ msg = JSON.parse(line)
104
+ rescue JSON::ParserError
105
+ next
106
+ end
107
+ next unless msg['id'] == id
108
+ if msg['error']
109
+ raise ApiError.new("codex mcp error: #{msg['error']['message'] || msg['error']}",
110
+ provider: 'codex')
111
+ end
112
+ return msg['result']
113
+ end
114
+ end
115
+
116
+ def timed_out!(id)
117
+ raise ApiError.new("codex mcp timed out waiting for id=#{id}",
118
+ provider: 'codex', retryable: true)
119
+ end
120
+
121
+ def extract(result)
122
+ text = +''
123
+ thread_id = nil
124
+ if result.is_a?(Hash)
125
+ Array(result['content']).each do |blk|
126
+ text << blk['text'].to_s if blk.is_a?(Hash) && blk['type'] == 'text'
127
+ end
128
+ thread_id = result.dig('structuredContent', 'threadId') ||
129
+ result.dig('_meta', 'threadId') || result['threadId']
130
+ end
131
+ { text: text, thread_id: thread_id }
132
+ end
133
+
134
+ def build_prompt(messages, system, output_schema)
135
+ parts = []
136
+ parts << "[System]: #{system}\n" if system
137
+ if output_schema
138
+ parts << '[Output Format]: Respond with ONLY valid JSON (no markdown fences) matching this schema:'
139
+ parts << JSON.generate(output_schema)
140
+ parts << ''
141
+ end
142
+ messages.each do |m|
143
+ role = m['role'] || m[:role]
144
+ content = m['content'] || m[:content]
145
+ case role
146
+ when 'user' then parts << content.to_s
147
+ when 'assistant' then parts << "[Previous assistant response]: #{content}"
148
+ when 'tool' then parts << "[Tool result]: #{content}"
149
+ end
150
+ end
151
+ parts.join("\n")
152
+ end
153
+
154
+ def now
155
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -99,6 +99,11 @@ module KairosMcp
99
99
  'claude_code: low/medium/high/xhigh/max. ' \
100
100
  'codex: minimal/low/medium/high. ' \
101
101
  'Ignored by providers that do not support it.'
102
+ },
103
+ backend: {
104
+ type: 'string',
105
+ description: 'Provider transport backend. codex: "cli" (default, `codex exec`) ' \
106
+ 'or "mcp" (`codex mcp-server`, read-only sandbox). Ignored by other providers.'
102
107
  }
103
108
  },
104
109
  required: ['messages']
@@ -134,6 +139,7 @@ module KairosMcp
134
139
  config['dispatch_id'] = arguments['dispatch_id'] if arguments['dispatch_id']
135
140
  config['sandbox_mode'] = true if arguments['sandbox_mode']
136
141
  config['effort'] = arguments['effort'] if arguments['effort']
142
+ config['backend'] = arguments['backend'] if arguments['backend']
137
143
 
138
144
  actual_provider = config['provider']
139
145
  adapter = build_adapter(config)
@@ -252,8 +258,13 @@ module KairosMcp
252
258
  when 'claude_code'
253
259
  ClaudeCodeAdapter.new(config)
254
260
  when 'codex'
255
- require_relative '../lib/llm_client/codex_adapter'
256
- CodexAdapter.new(config)
261
+ if config['backend'].to_s == 'mcp'
262
+ require_relative '../lib/llm_client/codex_mcp_adapter'
263
+ CodexMcpAdapter.new(config)
264
+ else
265
+ require_relative '../lib/llm_client/codex_adapter'
266
+ CodexAdapter.new(config)
267
+ end
257
268
  when 'cursor'
258
269
  require_relative '../lib/llm_client/cursor_adapter'
259
270
  CursorAdapter.new(config)
@@ -128,6 +128,11 @@ reviewers:
128
128
  # GPT-5.5 added 2026-04-24 for benchmarking against gpt-5.4. Both codex
129
129
  # entries dispatch via the same `codex exec` CLI; the adapter passes
130
130
  # `-m <model>` so the response can be attributed per-model.
131
+ #
132
+ # Transport backend (2026-07-03): codex entries default to the `codex exec` CLI.
133
+ # Add `backend: mcp` to route a codex reviewer through `codex mcp-server` instead
134
+ # (CodexMcpAdapter, read-only sandbox). Opt-in for A/B against the CLI path;
135
+ # keep the default CLI until MCP parity is confirmed (see log 20260702 Appendix F).
131
136
  - provider: codex
132
137
  model: gpt-5.5
133
138
  effort: medium
@@ -161,6 +161,10 @@ module KairosMcp
161
161
  effort = reviewer[:effort] || reviewer['effort']
162
162
  args['effort'] = effort if effort
163
163
 
164
+ # Per-reviewer transport backend (codex: "cli" default | "mcp"). Opt-in.
165
+ backend = reviewer[:backend] || reviewer['backend']
166
+ args['backend'] = backend if backend
167
+
164
168
  # Pass dispatch_id and sandbox_mode to llm_call for adapter config.
165
169
  args['dispatch_id'] = dispatch_id
166
170
  args['sandbox_mode'] = true if review_context == 'independent'
@@ -39,6 +39,12 @@ module KairosMcp
39
39
  force: {
40
40
  type: 'boolean',
41
41
  description: 'Force re-projection ignoring digest check (project command only)'
42
+ },
43
+ host: {
44
+ type: 'string',
45
+ enum: %w[claude codex opencode],
46
+ description: 'Target host (default claude). claude=.claude/ + settings.json; ' \
47
+ 'codex=.codex/ + .codex/hooks.json; opencode=.opencode/ (hooks skipped).'
42
48
  }
43
49
  },
44
50
  required: %w[command]
@@ -51,7 +57,9 @@ module KairosMcp
51
57
 
52
58
  project_root = ::KairosMcp.project_root
53
59
  mode = ::KairosMcp.projection_mode
54
- projector = ::KairosMcp::PluginProjector.new(project_root, mode: mode)
60
+ host = arguments['host'] || 'claude'
61
+ projector = ::KairosMcp::PluginProjector.new(project_root, mode: mode, host: host)
62
+ reload_hint = host == 'claude' ? ' Run /reload-plugins to activate.' : ''
55
63
 
56
64
  case arguments['command']
57
65
  when 'project'
@@ -64,15 +72,17 @@ module KairosMcp
64
72
  text_content(JSON.pretty_generate({
65
73
  status: 'projected',
66
74
  mode: mode,
75
+ host: host,
67
76
  force: true,
68
- message: 'Run /reload-plugins to activate new skills.'
77
+ message: "Projected to #{host}.#{reload_hint}"
69
78
  }))
70
79
  else
71
80
  changed = projector.project_if_changed!(enabled, knowledge_entries: knowledge_entries)
72
81
  text_content(JSON.pretty_generate({
73
82
  status: changed ? 'projected' : 'unchanged',
74
83
  mode: mode,
75
- message: changed ? 'Run /reload-plugins to activate new skills.' : 'No changes detected.'
84
+ host: host,
85
+ message: changed ? "Projected to #{host}.#{reload_hint}" : 'No changes detected.'
76
86
  }))
77
87
  end
78
88
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kairos-chain
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.31.0
4
+ version: 3.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaomi Hatakeyama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-10 00:00:00.000000000 Z
11
+ date: 2026-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -275,6 +275,8 @@ files:
275
275
  - templates/knowledge/llm_cross_evaluation/scripts/test_calibration_v23.rb
276
276
  - templates/knowledge/llm_cross_evaluation/scripts/test_intra_family_v23.rb
277
277
  - templates/knowledge/llm_cross_evaluation/scripts/test_layer_d_v23.rb
278
+ - templates/knowledge/loop_engineering_patterns/loop_engineering_patterns.md
279
+ - templates/knowledge/loop_engineering_patterns/references/claude_code_loops_2026.md
278
280
  - templates/knowledge/mcp_to_saas_development_workflow/mcp_to_saas_development_workflow.md
279
281
  - templates/knowledge/multi_agent_design_workflow/multi_agent_design_workflow.md
280
282
  - templates/knowledge/multi_agent_design_workflow_jp/multi_agent_design_workflow_jp.md
@@ -459,6 +461,7 @@ files:
459
461
  - templates/skillsets/llm_client/lib/llm_client/call_router.rb
460
462
  - templates/skillsets/llm_client/lib/llm_client/claude_code_adapter.rb
461
463
  - templates/skillsets/llm_client/lib/llm_client/codex_adapter.rb
464
+ - templates/skillsets/llm_client/lib/llm_client/codex_mcp_adapter.rb
462
465
  - templates/skillsets/llm_client/lib/llm_client/cursor_adapter.rb
463
466
  - templates/skillsets/llm_client/lib/llm_client/error_taxonomy.rb
464
467
  - templates/skillsets/llm_client/lib/llm_client/headless.rb