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 +4 -4
- data/CHANGELOG.md +77 -0
- data/bin/kairos-chain +34 -7
- data/bin/kairos-plugin-project +21 -1
- data/lib/kairos_mcp/plugin_projector.rb +235 -24
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/knowledge/loop_engineering_patterns/loop_engineering_patterns.md +49 -0
- data/templates/knowledge/loop_engineering_patterns/references/claude_code_loops_2026.md +23 -0
- data/templates/knowledge/skill_authoring_patterns/skill_authoring_patterns.md +1 -1
- data/templates/skillsets/llm_client/lib/llm_client/call_router.rb +8 -2
- data/templates/skillsets/llm_client/lib/llm_client/codex_mcp_adapter.rb +160 -0
- data/templates/skillsets/llm_client/tools/llm_call.rb +13 -2
- data/templates/skillsets/multi_llm_review/config/multi_llm_review.yml +5 -0
- data/templates/skillsets/multi_llm_review/lib/multi_llm_review/dispatcher.rb +4 -0
- data/templates/skillsets/plugin_projector/tools/plugin_project.rb +13 -3
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a393cfbac08011bdd6f7b8cd3129177f535c7507868215d6b85ef2af732e8521
|
|
4
|
+
data.tar.gz: 202af122d66197111412f5cdfc48287872713781602fe7f5f428ec40f3e111a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 "
|
|
498
|
+
puts " #{ctx_file} region : #{result[:region_written] ? 'updated' : 'not updated (host file outside project root or unsafe)'}"
|
|
475
499
|
puts ""
|
|
476
|
-
puts "Restart
|
|
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] ?
|
|
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
|
-
|
|
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 "
|
|
525
|
+
puts " #{ctx_file} : #{result[:region_removed] ? 'region removed' : 'region not present'}"
|
|
499
526
|
puts ""
|
|
500
|
-
puts "Restart
|
|
527
|
+
puts "Restart the host to apply (#{ctx_file} is read at session start)."
|
|
501
528
|
exit 0
|
|
502
529
|
|
|
503
530
|
else
|
data/bin/kairos-plugin-project
CHANGED
|
@@ -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
|
-
|
|
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
|
-
@
|
|
48
|
-
@
|
|
49
|
-
@
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
832
|
+
warn "[PluginProjector] WARNING: refusing to mutate non-project #{@host.context_file} at '#{path}'"
|
|
622
833
|
false
|
|
623
834
|
end
|
|
624
835
|
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|