kairos-chain 3.25.2 → 3.26.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: 2ff49b5dba49e9c78161990be5bbf9bc94252542aeafe636b0c6cd424952ccff
4
- data.tar.gz: 816f240e2cea9d045b4ac0c19c622792fe603dc49d2b454ec9409a8a6ab4f74a
3
+ metadata.gz: efb171bcb7680f795a7132e8d2d68ffb5ca4574d298ca1ea47ac7d44b42398e8
4
+ data.tar.gz: da787664c3683e38f960da502a395bdde91285550a9af8003b59c2cc5cd5cfef
5
5
  SHA512:
6
- metadata.gz: aa98790fe6f4b71d1995b6d6a6822692a246c5aff24e3d99c9087ed44e78c9dc92ee19497628335862b1b6c93d156da514a7059bb275116d6e2ee62474e091a2
7
- data.tar.gz: d50285be992138c811fb27bf9bf375648bbab801b71f37c9150d980996fc418da9e8bbd0bbaed6b506d19ecb187df932395f0a70f4501772b027c2bea8695581
6
+ metadata.gz: 1ed8251d8184beed84b8b3d424a32d5e81c667d30f304e90767aa7d80f279291ef2ce18ed129bd1ddc5c527d2ae03b153854c400f1d929f7acc17c50b43e930c
7
+ data.tar.gz: f386544999d14dff7298ecbfcb2c5939adc593767d2dc10843bc09e2e143bc003f2616dc8f01f463bf22f8b75b3571007c7a4e389477e4401dd486377cab0f18
data/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ 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.26.0] - 2026-05-12
8
+
9
+ ### Added — consumer_project_root separation (design v0.2)
10
+
11
+ Decouples the consumer project root (where `CLAUDE.md` and `.claude/` are written)
12
+ from the data directory (where `.kairos/` lives). Fixes silent projection failure
13
+ when `--data-dir` points outside the consumer workspace
14
+ (see `log/handoff_kairoschain_plugin_projection_bug.md`).
15
+
16
+ - `KairosMcp.consumer_project_root` accessor + `consumer_project_root_source`
17
+ provenance (`:explicit_cli`, `:explicit_env`, `:transport_default`, `:absent`).
18
+ - Resolution order: `--project-root` CLI flag → `KAIROS_PROJECT_ROOT` env →
19
+ per-transport default with plausibility check.
20
+ - Plausibility markers: `CLAUDE.md`, `.git/`, `.claude/`, or prior
21
+ `.kairos/projection_manifest.json`.
22
+ - `PluginProjector` constructor accepts `data_dir:` kwarg; refuses construction
23
+ (`CoincidenceRefused`) when project root and data dir resolve to the same
24
+ real path.
25
+ - `kairos-chain mode project|status` now prints resolved project root + source +
26
+ data dir.
27
+ - New `--project-root <path>` global flag and `mode` subcommand option.
28
+ - Manifest files (`projection_manifest.json`,
29
+ `instruction_mode_manifest.json`) now live under `data_dir/`, decoupled from
30
+ `project_root/.kairos/`.
31
+
32
+ ### Design references
33
+
34
+ - Design draft v0.2: `log/20260512_consumer_project_root_separation_design_v0.2.md`
35
+ - Multi-LLM review round 1 verdict: REVISE (1/5 APPROVE).
36
+ Reject log: `log/20260512_consumer_project_root_separation_design_v0.2_reject_log.md`.
37
+
38
+ ### Out of scope (deferred to future releases)
39
+
40
+ - Multi-consumer routing (Inv 9 invariant declared, implementation deferred —
41
+ single-consumer only in 3.26.0).
42
+ - Remote HTTP MCP projection delivery: HTTP MCP transport now loud-refuses
43
+ when no explicit `--project-root` is configured (no more silent failure),
44
+ but cross-machine artifact delivery is unimplemented. Local HTTP with
45
+ explicit configuration works.
46
+ - Explicit authorization UX (e.g. `kairos-chain mode authorize`): v0.2 treats
47
+ explicit designation as implicit authorization.
48
+
49
+ ### Tests
50
+
51
+ - New `test_consumer_project_root.rb` (18 runs, 43 assertions) covering:
52
+ default rule per transport, plausibility check, symlink real-path resolution,
53
+ `PluginProjector` coincidence refusal, and SUSHI-bug regression scenarios.
54
+ - Updated `test_plugin_projector_instruction_mode.rb` for new manifest location.
55
+ - All existing test suites pass (186 total across `test_plugin_projector.rb`,
56
+ `test_plugin_projector_instruction_mode.rb`, `test_capability.rb`,
57
+ `test_skillset_manager.rb`, `test_consumer_project_root.rb`).
58
+
7
59
  ## [3.25.2] - 2026-05-07
8
60
 
9
61
  ### Changed (L1 knowledge: reviewer evaluation feedback loop)
data/bin/kairos-chain CHANGED
@@ -370,9 +370,18 @@ when 'mode'
370
370
  region from CLAUDE.md. Manifest is cleared.
371
371
 
372
372
  Options:
373
- --data-dir DIR Override the .kairos/ data directory location.
373
+ --data-dir DIR Override the .kairos/ data directory location.
374
+ --project-root DIR Consumer project root (where CLAUDE.md and .claude/
375
+ are written). Resolution order: --project-root,
376
+ KAIROS_PROJECT_ROOT env, then cwd if it contains a
377
+ recognizable project marker (CLAUDE.md, .git/,
378
+ .claude/, or prior projection manifest).
374
379
 
375
380
  Notes:
381
+ - data_dir and consumer project root are decoupled (design v0.2,
382
+ log/20260512_consumer_project_root_separation_design_v0.2.md).
383
+ When --data-dir points outside the consumer workspace, you MUST
384
+ set --project-root (or KAIROS_PROJECT_ROOT) explicitly.
376
385
  - The active mode is read from `instructions_mode` in
377
386
  .kairos/skills/config.yml. Use `instructions_update` MCP tool
378
387
  to change it; then re-run `mode project`.
@@ -394,12 +403,35 @@ when 'mode'
394
403
  ARGV.delete_at(idx)
395
404
  end
396
405
 
406
+ # v0.2: Honor --project-root if present (consumer project root, distinct from data_dir)
407
+ if (idx = ARGV.index('--project-root'))
408
+ KairosMcp.consumer_project_root = File.expand_path(ARGV[idx + 1])
409
+ ARGV.delete_at(idx + 1)
410
+ ARGV.delete_at(idx)
411
+ end
412
+
397
413
  require 'kairos_mcp/skills_config'
398
414
  require 'kairos_mcp/plugin_projector'
399
415
 
400
- project_root = KairosMcp.project_root
416
+ # v0.2: resolve consumer_project_root via the documented order (CLI → env → cwd default).
417
+ # CLI-direct transport is implicit here (we are running from the user's shell).
418
+ project_root = KairosMcp.resolve_consumer_project_root(transport: :cli_direct)
419
+ if project_root.nil?
420
+ warn "ERROR: no plausible consumer project root resolved."
421
+ warn " Set explicitly with --project-root <path> or KAIROS_PROJECT_ROOT env var,"
422
+ warn " or run from a directory containing a recognizable project marker"
423
+ warn " (CLAUDE.md, .git/, .claude/, or a prior .kairos/projection_manifest.json)."
424
+ exit 1
425
+ end
426
+
401
427
  projector_mode = KairosMcp.projection_mode
402
- projector = KairosMcp::PluginProjector.new(project_root, mode: projector_mode)
428
+ begin
429
+ projector = KairosMcp::PluginProjector.new(project_root, mode: projector_mode, data_dir: KairosMcp.data_dir)
430
+ rescue KairosMcp::PluginProjector::CoincidenceRefused => e
431
+ warn "ERROR: #{e.message}"
432
+ warn " Use --project-root <path> to designate a directory distinct from the data directory."
433
+ exit 1
434
+ end
403
435
 
404
436
  case mode_action
405
437
  when 'project'
@@ -433,17 +465,21 @@ when 'mode'
433
465
  end
434
466
 
435
467
  puts "Instruction mode projected:"
436
- puts " mode : #{instructions_mode}#{version ? " v#{version}" : ''}"
437
- puts " source : #{body_path}"
438
- puts " artifact : #{result[:artifact_path]}"
439
- puts " size : #{result[:size_bytes]} bytes"
440
- puts " CLAUDE.md : region #{result[:region_written] ? 'updated' : 'not updated (host file outside project root or unsafe)'}"
468
+ puts " mode : #{instructions_mode}#{version ? " v#{version}" : ''}"
469
+ puts " body source : #{body_path}"
470
+ puts " project root : #{project_root} (source: #{KairosMcp.consumer_project_root_source})"
471
+ puts " data dir : #{KairosMcp.data_dir}"
472
+ puts " artifact : #{result[:artifact_path]}"
473
+ puts " size : #{result[:size_bytes]} bytes"
474
+ puts " CLAUDE.md region : #{result[:region_written] ? 'updated' : 'not updated (host file outside project root or unsafe)'}"
441
475
  puts ""
442
476
  puts "Restart Claude Code to apply (CLAUDE.md @-imports resolve at session start)."
443
477
  exit 0
444
478
 
445
479
  when 'status'
446
480
  s = projector.instruction_mode_status
481
+ puts "Project root : #{project_root} (source: #{KairosMcp.consumer_project_root_source})"
482
+ puts "Data dir : #{KairosMcp.data_dir}"
447
483
  if s[:active]
448
484
  puts "Instruction mode projection: ACTIVE"
449
485
  puts " mode : #{s[:mode_name]}#{s[:mode_version] ? " v#{s[:mode_version]}" : ''}"
@@ -481,6 +517,10 @@ OptionParser.new do |opts|
481
517
  options[:data_dir] = dir
482
518
  end
483
519
 
520
+ opts.on('--project-root DIR', 'Consumer project root for projection artifacts (default: KAIROS_PROJECT_ROOT env, then cwd with plausibility check)') do |dir|
521
+ options[:project_root] = dir
522
+ end
523
+
484
524
  opts.on('--http', 'Start in Streamable HTTP mode (default: stdio)') do
485
525
  options[:http] = true
486
526
  end
@@ -556,6 +596,11 @@ if options[:data_dir]
556
596
  KairosMcp.data_dir = File.expand_path(options[:data_dir])
557
597
  end
558
598
 
599
+ # v0.2: Set consumer_project_root if specified via CLI
600
+ if options[:project_root]
601
+ KairosMcp.consumer_project_root = File.expand_path(options[:project_root])
602
+ end
603
+
559
604
  # Handle --version
560
605
  if options[:version]
561
606
  puts "KairosChain MCP Server v#{KairosMcp::VERSION}"
@@ -27,14 +27,33 @@ module KairosMcp
27
27
  INSTRUCTION_MODE_SIZE_WARN = 150 * 1024
28
28
  INSTRUCTION_MODE_SIZE_REFUSE = 256 * 1024
29
29
 
30
- attr_reader :mode, :project_root, :output_root
30
+ attr_reader :mode, :project_root, :output_root, :data_dir
31
31
 
32
- def initialize(project_root, mode: :auto)
32
+ # Construct a PluginProjector.
33
+ #
34
+ # @param project_root [String] consumer project root (where .claude/ and CLAUDE.md live)
35
+ # @param mode [Symbol] :auto, :project, or :plugin
36
+ # @param data_dir [String, nil] KairosChain data directory. When provided, the
37
+ # projector enforces design v0.2 Inv 3: refuses construction when
38
+ # real_path(project_root) == real_path(data_dir). When nil, the legacy assumption
39
+ # data_dir = project_root/.kairos is used for manifest location (backward-compat).
40
+ #
41
+ # @raise [CoincidenceRefused] when project_root and data_dir resolve to the same real path
42
+ def initialize(project_root, mode: :auto, data_dir: nil)
33
43
  @project_root = project_root
44
+ @data_dir = data_dir || File.join(project_root, '.kairos')
45
+ enforce_no_coincidence!
34
46
  @mode = resolve_mode(mode)
35
47
  @output_root = @mode == :plugin ? project_root : File.join(project_root, '.claude')
36
- @manifest_path = File.join(project_root, '.kairos', 'projection_manifest.json')
37
- @instruction_mode_manifest_path = File.join(project_root, '.kairos', 'instruction_mode_manifest.json')
48
+ @manifest_path = File.join(@data_dir, 'projection_manifest.json')
49
+ @instruction_mode_manifest_path = File.join(@data_dir, 'instruction_mode_manifest.json')
50
+ end
51
+
52
+ # Raised when project_root and data_dir resolve to the same real path (design Inv 3).
53
+ class CoincidenceRefused < StandardError
54
+ def initialize(path)
55
+ super("consumer project root and data directory coincide at real path #{path.inspect} (design Inv 3): explicit configuration required")
56
+ end
38
57
  end
39
58
 
40
59
  # Main entry: project all SkillSet plugin artifacts + L1 knowledge meta skill
@@ -185,6 +204,23 @@ module KairosMcp
185
204
  :project
186
205
  end
187
206
 
207
+ # Inv 3 enforcement: refuse if project_root and data_dir resolve to the same
208
+ # real path. Comparison happens post-realpath (design Inv 8). Non-existent
209
+ # paths fall back to expand_path; coincidence at expand_path level still counts.
210
+ def enforce_no_coincidence!
211
+ pr_real = canonicalize(@project_root)
212
+ dd_real = canonicalize(@data_dir)
213
+ return unless pr_real && dd_real && pr_real == dd_real
214
+ raise CoincidenceRefused, pr_real
215
+ end
216
+
217
+ def canonicalize(path)
218
+ return nil if path.nil?
219
+ File.realpath(File.expand_path(path))
220
+ rescue Errno::ENOENT
221
+ File.expand_path(path)
222
+ end
223
+
188
224
  # =========================================================================
189
225
  # Skill projection
190
226
  # =========================================================================
@@ -137,12 +137,27 @@ module KairosMcp
137
137
 
138
138
  # Project plugin artifacts (only if .claude/ exists — avoids creating
139
139
  # Claude Code artifacts for non-Claude clients like Cursor or Codex)
140
- project_root = KairosMcp.project_root
140
+ #
141
+ # Design v0.2: resolve consumer_project_root explicitly. Skip projection if
142
+ # no plausible root is available (Inv 5: graceful skip). Refuse on coincidence
143
+ # with data_dir (Inv 3, raised by PluginProjector constructor).
144
+ project_root = KairosMcp.consumer_project_root
145
+ if project_root.nil?
146
+ warn "[PluginProjector] no plausible consumer project root resolved " \
147
+ "(source: #{KairosMcp.consumer_project_root_source}); projection skipped"
148
+ return
149
+ end
150
+
141
151
  mode = KairosMcp.projection_mode
142
152
  output_root = mode == :plugin ? project_root : File.join(project_root, '.claude')
143
153
  return unless File.directory?(output_root)
144
154
 
145
- projector = PluginProjector.new(project_root, mode: mode)
155
+ begin
156
+ projector = PluginProjector.new(project_root, mode: mode, data_dir: KairosMcp.data_dir)
157
+ rescue PluginProjector::CoincidenceRefused => e
158
+ warn "[PluginProjector] #{e.message}; projection skipped"
159
+ return
160
+ end
146
161
  enabled = manager.enabled_skillsets
147
162
  knowledge_entries = collect_knowledge_entries
148
163
 
@@ -229,8 +244,9 @@ module KairosMcp
229
244
  end
230
245
 
231
246
  # True if the active instruction mode has been projected for this project.
247
+ # v0.2: manifest now lives at data_dir level (decoupled from project_root).
232
248
  def instruction_mode_projected?(mode)
233
- manifest_path = File.join(KairosMcp.project_root, '.kairos', 'instruction_mode_manifest.json')
249
+ manifest_path = File.join(KairosMcp.data_dir, 'instruction_mode_manifest.json')
234
250
  return false unless File.exist?(manifest_path)
235
251
  data = JSON.parse(File.read(manifest_path))
236
252
  data['mode_name'] == mode && data['region_present']
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.25.2"
2
+ VERSION = "3.26.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
data/lib/kairos_mcp.rb CHANGED
@@ -276,12 +276,132 @@ module KairosMcp
276
276
  end
277
277
 
278
278
  # =========================================================================
279
- # Plugin Projection support
279
+ # Plugin Projection support — consumer project root
280
280
  # =========================================================================
281
+ #
282
+ # Design v0.2 (log/20260512_consumer_project_root_separation_design_v0.2.md):
283
+ # consumer_project_root is decoupled from data_dir. Resolution order:
284
+ # 1. Explicit setter (CLI flag --project-root)
285
+ # 2. Environment variable KAIROS_PROJECT_ROOT
286
+ # 3. Per-transport default with plausibility check:
287
+ # stdio_mcp / cli_direct: Dir.pwd if plausible
288
+ # http_mcp: no default (returns nil; caller must refuse projection)
289
+ #
290
+ # See design §2 (invariants) and §6 (failure taxonomy).
291
+
292
+ PLAUSIBILITY_MARKERS = ['CLAUDE.md', '.git', '.claude',
293
+ File.join('.kairos', 'projection_manifest.json')].freeze
294
+
295
+ # Consumer project root: where projection artifacts (CLAUDE.md, .claude/) are written.
296
+ # Distinct from data_dir (where KairosChain state lives).
297
+ #
298
+ # @return [String, nil] absolute real path, or nil if no plausible root is available
299
+ def consumer_project_root
300
+ resolve_consumer_project_root unless defined?(@consumer_project_root_resolved) && @consumer_project_root_resolved
301
+ @consumer_project_root
302
+ end
303
+
304
+ # Source of the currently resolved consumer_project_root.
305
+ # @return [Symbol] :explicit_cli, :explicit_env, :transport_default, :absent
306
+ def consumer_project_root_source
307
+ consumer_project_root # trigger resolution
308
+ @consumer_project_root_source || :absent
309
+ end
310
+
311
+ # Explicit setter (used by CLI --project-root flag).
312
+ # Setting to nil clears any cached resolution.
313
+ def consumer_project_root=(path)
314
+ if path.nil?
315
+ @consumer_project_root = nil
316
+ @consumer_project_root_source = :absent
317
+ else
318
+ @consumer_project_root = real_path(File.expand_path(path))
319
+ @consumer_project_root_source = :explicit_cli
320
+ end
321
+ @consumer_project_root_resolved = true
322
+ end
323
+
324
+ # Reset resolution cache (for testing or re-initialization).
325
+ def reset_consumer_project_root!
326
+ @consumer_project_root = nil
327
+ @consumer_project_root_source = nil
328
+ @consumer_project_root_resolved = false
329
+ end
330
+
331
+ # Resolve consumer_project_root following the documented order.
332
+ # @param transport [Symbol] :stdio_mcp, :http_mcp, :cli_direct (default: auto-detect)
333
+ # @return [String, nil] resolved absolute real path, or nil
334
+ def resolve_consumer_project_root(transport: nil)
335
+ # Skip env/default lookup if explicit setter was used
336
+ if @consumer_project_root_source == :explicit_cli && @consumer_project_root
337
+ @consumer_project_root_resolved = true
338
+ return @consumer_project_root
339
+ end
340
+
341
+ transport ||= detect_transport
342
+
343
+ # 1. Environment variable
344
+ if (env_val = ENV['KAIROS_PROJECT_ROOT']) && !env_val.empty?
345
+ @consumer_project_root = real_path(File.expand_path(env_val))
346
+ @consumer_project_root_source = :explicit_env
347
+ @consumer_project_root_resolved = true
348
+ return @consumer_project_root
349
+ end
350
+
351
+ # 2. Transport default
352
+ if transport == :http_mcp
353
+ # HTTP MCP: no default permitted (design §4)
354
+ @consumer_project_root = nil
355
+ @consumer_project_root_source = :absent
356
+ @consumer_project_root_resolved = true
357
+ return nil
358
+ end
359
+
360
+ # stdio_mcp / cli_direct: cwd default with plausibility
361
+ candidate = real_path(Dir.pwd)
362
+ if plausibility_check(candidate)
363
+ @consumer_project_root = candidate
364
+ @consumer_project_root_source = :transport_default
365
+ else
366
+ @consumer_project_root = nil
367
+ @consumer_project_root_source = :absent
368
+ end
369
+ @consumer_project_root_resolved = true
370
+ @consumer_project_root
371
+ end
372
+
373
+ # Plausibility predicate (design Inv 6 / §11).
374
+ # Candidate passes if any recognizable project marker exists at the path.
375
+ def plausibility_check(path)
376
+ return false if path.nil? || path.empty?
377
+ return false unless Dir.exist?(path)
378
+ PLAUSIBILITY_MARKERS.any? do |marker|
379
+ candidate = File.join(path, marker)
380
+ File.exist?(candidate) || Dir.exist?(candidate)
381
+ end
382
+ end
383
+
384
+ # Resolve a path to its real path (symlinks resolved). Falls back to expand_path
385
+ # for non-existent paths.
386
+ def real_path(path)
387
+ File.realpath(File.expand_path(path))
388
+ rescue Errno::ENOENT
389
+ File.expand_path(path)
390
+ end
391
+
392
+ # Detect current transport mode based on runtime state.
393
+ # @return [Symbol] :http_mcp or :stdio_mcp
394
+ def detect_transport
395
+ return :http_mcp if @http_server
396
+ :stdio_mcp
397
+ end
281
398
 
282
- # Project root: parent of .kairos/ data directory
399
+ # DEPRECATED in v0.2: parent-of-data-dir derivation.
400
+ # Returns consumer_project_root when available, falling back to File.dirname(data_dir)
401
+ # only for backward compatibility with internal callers that have not yet been
402
+ # migrated. New code should use consumer_project_root and handle nil explicitly.
283
403
  def project_root
284
- File.dirname(data_dir)
404
+ consumer_project_root || File.dirname(data_dir)
285
405
  end
286
406
 
287
407
  # Determine projection mode for PluginProjector
@@ -289,7 +409,7 @@ module KairosMcp
289
409
  # :plugin — writes to plugin root skills/, agents/, hooks/hooks.json
290
410
  def projection_mode
291
411
  return :plugin if ENV['KAIROS_PROJECTION_MODE'] == 'plugin'
292
- root = project_root
412
+ root = consumer_project_root || File.dirname(data_dir)
293
413
  plugin_json = File.join(root, '.claude-plugin', 'plugin.json')
294
414
  claude_dir = File.join(root, '.claude')
295
415
  return :plugin if File.exist?(plugin_json) && !File.exist?(claude_dir)
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: kairos_hook_projector
3
+ description: >
4
+ Compile mode_hooks definitions into the plugin_projector pipeline.
5
+ Use to inspect mode_hooks status (read-only in stage 0).
6
+ ---
7
+
8
+ # Kairos Hook Projector
9
+
10
+ Rides the existing `plugin_projector` pipeline as a compiler layer:
11
+ mode_hooks YAML → `plugin/hooks.json` → `.claude/settings.json` (via `plugin_projector`).
12
+
13
+ ## Stage
14
+
15
+ v0.1 stage 0: skeleton + schema + `hooks_status` (read-only). Zero side effect.
16
+ Later stages add compile / project / unproject / composition.
17
+
18
+ ## Design
19
+
20
+ See `docs/drafts/kairos_hook_projector_design_v0.2_draft.md` (frozen).
@@ -0,0 +1,3 @@
1
+ {
2
+ "hooks": {}
3
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "kairos_hook_projector",
3
+ "version": "0.1.0",
4
+ "description": "Compiles mode_hooks definitions into the plugin_projector hook pipeline. Enables deterministic invocation paths for KairosChain MCP tools via Claude Code hooks. v0.1 stage 0: skeleton + schema + read-only status tool, zero side effect on .claude/settings.json. See docs/drafts/kairos_hook_projector_design_v0.2_draft.md.",
5
+ "author": "Masaomi Hatakeyama",
6
+ "layer": "L1",
7
+ "depends_on": [
8
+ {
9
+ "name": "plugin_projector"
10
+ }
11
+ ],
12
+ "provides": [
13
+ "hook_compilation",
14
+ "mode_hooks_schema",
15
+ "hooks_status_readonly"
16
+ ],
17
+ "tool_classes": [],
18
+ "config_files": [],
19
+ "knowledge_dirs": [],
20
+ "min_core_version": "3.25.0"
21
+ }
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'json'
5
+
6
+ class TestKairosHookProjectorSkillsetJson < Minitest::Test
7
+ SKILLSET_ROOT = File.expand_path('..', __dir__)
8
+
9
+ def setup
10
+ @path = File.join(SKILLSET_ROOT, 'skillset.json')
11
+ @json = JSON.parse(File.read(@path))
12
+ end
13
+
14
+ def test_skillset_json_parses
15
+ assert_kind_of Hash, @json
16
+ end
17
+
18
+ def test_name_is_kairos_hook_projector
19
+ assert_equal 'kairos_hook_projector', @json['name']
20
+ end
21
+
22
+ def test_layer_is_l1
23
+ assert_equal 'L1', @json['layer']
24
+ end
25
+
26
+ def test_depends_on_plugin_projector
27
+ deps = @json['depends_on']
28
+ assert_kind_of Array, deps
29
+ names = deps.map { |d| d['name'] }
30
+ assert_includes names, 'plugin_projector'
31
+ end
32
+
33
+ def test_plugin_skill_md_exists
34
+ skill_md = File.join(SKILLSET_ROOT, 'plugin', 'SKILL.md')
35
+ assert File.exist?(skill_md), "plugin/SKILL.md must exist"
36
+ end
37
+
38
+ def test_plugin_hooks_json_is_empty_object
39
+ hooks_json = File.join(SKILLSET_ROOT, 'plugin', 'hooks.json')
40
+ assert File.exist?(hooks_json), "plugin/hooks.json must exist"
41
+ parsed = JSON.parse(File.read(hooks_json))
42
+ assert_equal({ 'hooks' => {} }, parsed,
43
+ 'stage 0: hooks.json must be {"hooks":{}} — no projections yet')
44
+ end
45
+ end
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.25.2
4
+ version: 3.26.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-05-07 00:00:00.000000000 Z
11
+ date: 2026-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -411,6 +411,10 @@ files:
411
411
  - templates/skillsets/introspection/tools/introspection_check.rb
412
412
  - templates/skillsets/introspection/tools/introspection_health.rb
413
413
  - templates/skillsets/introspection/tools/introspection_safety.rb
414
+ - templates/skillsets/kairos_hook_projector/plugin/SKILL.md
415
+ - templates/skillsets/kairos_hook_projector/plugin/hooks.json
416
+ - templates/skillsets/kairos_hook_projector/skillset.json
417
+ - templates/skillsets/kairos_hook_projector/test/test_skillset_json.rb
414
418
  - templates/skillsets/knowledge_creator/config/knowledge_creator.yml
415
419
  - templates/skillsets/knowledge_creator/knowledge/creation_guide/creation_guide.md
416
420
  - templates/skillsets/knowledge_creator/knowledge/quality_criteria/quality_criteria.md