kairos-chain 3.9.4 → 3.10.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: fa87c57bf88c7f0f225793d049592120e2872deccda39c3eba3a3af059663dac
4
- data.tar.gz: bf793a2f05c4e41bc43aa1f1d5a948985eeac39cee0c9c4c1c744d577e0c32ef
3
+ metadata.gz: 13fdfa15efd40f228b2fb789d51e1cbd722ce732bde40df4c8d25e5bf4108c56
4
+ data.tar.gz: 3aa43d1c26a56af6aa5c24e7ea9ca1d26a20f5c95984aec77f574874beeb810e
5
5
  SHA512:
6
- metadata.gz: 27ea12fe7352161972a0e81754a8012ba03da6e03c4f1db6fd4f51bb60d599f0ebc1777c54a8d8169b138e308b035628666d80dcccdf95ad1b82f78fb047295c
7
- data.tar.gz: 811bd5bf6f2339086196f1ea8edfe22371b3374bb1290ebbece3f8696c3a7c514baafbd24a1bf4cc7f7df0f8e29f0c825f1b36f2b22122345d3fcf704dab8141
6
+ metadata.gz: 80630f2c2488d3ab1b3864abc33f3f3711507c7ad1d6d166c21b06f33d8c044f51ec518ef22ae1bacc98e6c1a8d06dcff80afccb0d9b6b2b432dd38d7ad220ee
7
+ data.tar.gz: 27d701df365ec0c71b3b16b9190c78d0d590964192316d0d4a08218074ff289b5ca7edbd6fcca41311f9f0daeaed4b0a5ad46091bb36767afb1b2839e5dfab99
data/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ 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.10.0] - 2026-03-31
8
+
9
+ ### Added
10
+
11
+ - **introspection SkillSet** (v0.1.0) — New self-inspection SkillSet with 3 tools:
12
+ - `introspection_check`: Full inspection (L1 health + blockchain integrity + safety mechanisms + recommendations)
13
+ - `introspection_health`: L1 knowledge health scores using Synoptis TrustScorer (optional, falls back to staleness-only)
14
+ - `introspection_safety`: 4-layer safety mechanism visibility (L0 approval workflow, RBAC policies, agent safety gates, blockchain recording)
15
+ - **Dream SkillSet L1 dedup + confidence scoring** (v0.2.1) — `dream_scan` now checks promotion candidates against existing L1 knowledge (name similarity + tag Jaccard overlap) and scores candidates with 3-dimension confidence (recurrence, tag consistency, session diversity). New `include_l1_dedup` parameter.
16
+ - **`skills_promote` attestation integration** — Successful L2→L1 promotions now automatically issue Synoptis attestations (`claim: "promoted_from_l2"`, `actor_role: "automated"`). Graceful degradation when Synoptis is not loaded.
17
+ - **`Safety.registered_policy_names`** — New thread-safe public API for introspecting registered RBAC policies. Replaces `instance_variable_get(:@policies)` pattern.
18
+
19
+ ### Fixed
20
+
21
+ - **`system_upgrade` SkillSet-only install** — When specific SkillSet names are provided via `names` parameter but L0 templates are already up-to-date, `system_upgrade apply` now correctly installs/upgrades the requested SkillSets instead of returning "No upgrade needed". Previously, the L0 upgrade check (`UpgradeAnalyzer.upgrade_needed?`) gated all operations including SkillSet installs.
22
+
23
+ ### Design Process
24
+
25
+ - Design reviewed: 2 rounds x 3 LLMs (Claude Opus 4.6, Codex GPT-5.4, Cursor Composer-2)
26
+ - Implementation reviewed: 1 round x 3 LLMs per phase
27
+ - 8 P0/P1 bugs found and fixed during design review (before any code was written)
28
+ - Inspired by oh-my-claudecode analysis; independently designed with KairosChain philosophy
29
+
7
30
  ## [3.9.4] - 2026-03-30
8
31
 
9
32
  ### Added
@@ -25,6 +25,12 @@ module KairosMcp
25
25
  @policy_mutex.synchronize { @policies[name.to_sym] }
26
26
  end
27
27
 
28
+ # Thread-safe list of registered policy names.
29
+ # Used by introspection SkillSet for safety visibility.
30
+ def self.registered_policy_names
31
+ @policy_mutex.synchronize { @policies.keys.map(&:to_s) }
32
+ end
33
+
28
34
  # For testing only
29
35
  def self.clear_policies!
30
36
  @policy_mutex.synchronize { @policies = {} }
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require_relative 'base_tool'
4
5
  require_relative '../knowledge_provider'
5
6
  require_relative '../context_manager'
@@ -30,7 +31,8 @@ module KairosMcp
30
31
 
31
32
  def description
32
33
  'Promote knowledge between layers (L2→L1, L1→L0) with optional Persona Assembly for decision support. ' \
33
- 'Assembly generates a structured discussion from multiple perspectives before human decision.'
34
+ 'Assembly generates a structured discussion from multiple perspectives before human decision. ' \
35
+ 'For pattern detection and auto-scan, use dream_scan instead.'
34
36
  end
35
37
 
36
38
  def category
@@ -63,7 +65,7 @@ module KairosMcp
63
65
  end
64
66
 
65
67
  def related_tools
66
- %w[context_save knowledge_update skills_evolve skills_audit]
68
+ %w[context_save knowledge_update skills_evolve skills_audit attestation_issue]
67
69
  end
68
70
 
69
71
  def input_schema
@@ -72,7 +74,7 @@ module KairosMcp
72
74
  properties: {
73
75
  command: {
74
76
  type: 'string',
75
- description: 'Command: "analyze" (with assembly), "promote" (direct promotion), "status" (check requirements), or "suggest" (LLM suggests optimal personas for content)',
77
+ description: 'Command: "analyze" (with assembly), "promote" (direct promotion), "status" (check requirements), or "suggest" (LLM suggests optimal personas)',
76
78
  enum: %w[analyze promote status suggest]
77
79
  },
78
80
  source_name: {
@@ -122,7 +124,7 @@ module KairosMcp
122
124
  consensus_threshold: {
123
125
  type: 'number',
124
126
  description: 'Consensus threshold for early termination in discussion mode (default: 0.6 = 60%)'
125
- }
127
+ },
126
128
  },
127
129
  required: %w[command]
128
130
  }
@@ -576,6 +578,9 @@ module KairosMcp
576
578
  # Track promotion event for state commit (this triggers auto-commit on promotion)
577
579
  track_promotion_change(from_layer: 'L2', to_layer: 'L1', skill_id: target_name, reason: reason)
578
580
 
581
+ # Issue attestation AFTER L1 exists
582
+ issue_promotion_attestation(target_name, reason)
583
+
579
584
  action = existing ? 'updated' : 'created'
580
585
  output = "## Promotion Successful\n\n"
581
586
  output += "**Target**: #{target_name} (L1)\n"
@@ -681,6 +686,27 @@ module KairosMcp
681
686
  end
682
687
  end
683
688
 
689
+ # Issue attestation after successful promotion
690
+ def issue_promotion_attestation(target_name, reason)
691
+ return unless synoptis_available?
692
+
693
+ invoke_tool('attestation_issue', {
694
+ 'subject_ref' => "knowledge://#{target_name}",
695
+ 'claim' => 'promoted_from_l2',
696
+ 'evidence' => reason.to_s,
697
+ 'actor_role' => 'automated'
698
+ })
699
+ rescue StandardError => e
700
+ # Synoptis may not be loaded — silently skip
701
+ warn "[SkillsPromote] Attestation skipped: #{e.message}"
702
+ end
703
+
704
+ def synoptis_available?
705
+ @registry && true
706
+ rescue StandardError
707
+ false
708
+ end
709
+
684
710
  # Track promotion event for state commit auto-commit
685
711
  def track_promotion_change(from_layer:, to_layer:, skill_id:, reason: nil)
686
712
  return unless SkillsConfig.state_commit_enabled?
@@ -240,6 +240,12 @@ module KairosMcp
240
240
  analyzer = UpgradeAnalyzer.new
241
241
  analyzer.analyze
242
242
 
243
+ # When specific SkillSet names are requested, skip L0 upgrade check
244
+ # and go directly to SkillSet install/upgrade
245
+ if names && !names.empty? && !analyzer.upgrade_needed?
246
+ return handle_skillset_only_install(names)
247
+ end
248
+
243
249
  unless analyzer.upgrade_needed?
244
250
  return text_content("No upgrade needed. Data directory is already up to date.")
245
251
  end
@@ -437,6 +443,32 @@ module KairosMcp
437
443
  # =====================================================================
438
444
  # skillsets — List all available SkillSets with install status
439
445
  # =====================================================================
446
+ # Install/upgrade specific SkillSets without requiring a full L0 upgrade.
447
+ # Called when names are specified but L0 templates are already up to date.
448
+ def handle_skillset_only_install(names)
449
+ ss_mgr = KairosMcp::SkillSetManager.new
450
+ ss_results = ss_mgr.upgrade_apply(names: names)
451
+
452
+ if ss_results.empty?
453
+ return text_content("No SkillSet changes needed for: #{names.join(', ')}")
454
+ end
455
+
456
+ output = "# SkillSet Install/Upgrade\n\n"
457
+ ss_results.each do |r|
458
+ if r[:action] == 'installed'
459
+ output += " [INSTALLED] #{r[:name]} v#{r[:to]}\n"
460
+ else
461
+ output += " [UPGRADED] #{r[:name]} v#{r[:from]} → v#{r[:to]} (#{r[:files_updated]} files)\n"
462
+ end
463
+ end
464
+ output += "\nNo L0 template changes needed.\n"
465
+ output += "Restart the MCP server to load new SkillSet tools.\n"
466
+
467
+ text_content(output)
468
+ rescue StandardError => e
469
+ text_content("SkillSet install failed: #{e.message}")
470
+ end
471
+
440
472
  def handle_skillsets
441
473
  ss_mgr = KairosMcp::SkillSetManager.new
442
474
  available = ss_mgr.available_skillsets
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.9.4"
2
+ VERSION = "3.10.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -39,7 +39,9 @@ module KairosMcp
39
39
  properties: {
40
40
  goal_name: {
41
41
  type: 'string',
42
- description: 'Name of the goal (must exist as L1 knowledge or L2 context)'
42
+ description: 'Name of the goal. Create it with context_save (L2) first. ' \
43
+ 'L1 knowledge is only for reusable goal templates. ' \
44
+ 'The agent searches L2 contexts first, then falls back to L1.'
43
45
  },
44
46
  max_cycles: {
45
47
  type: 'integer',
@@ -25,8 +25,9 @@ module KairosMcp
25
25
  # @param scope [String] 'l2', 'l1', or 'all'
26
26
  # @param since_session [String, nil] Only scan sessions after this ID
27
27
  # @param include_archive_candidates [Boolean] Whether to detect stale L2
28
+ # @param include_l1_dedup [Boolean] Check promotion candidates against existing L1
28
29
  # @return [Hash] Structured scan result
29
- def scan(scope: 'l2', since_session: nil, include_archive_candidates: true)
30
+ def scan(scope: 'l2', since_session: nil, include_archive_candidates: true, include_l1_dedup: true)
30
31
  result = {
31
32
  scope: scope,
32
33
  scanned_at: Time.now.iso8601,
@@ -38,7 +39,8 @@ module KairosMcp
38
39
 
39
40
  if %w[l2 all].include?(scope)
40
41
  scan_l2(result, since_session: since_session,
41
- include_archive_candidates: include_archive_candidates)
42
+ include_archive_candidates: include_archive_candidates,
43
+ include_l1_dedup: include_l1_dedup)
42
44
  end
43
45
 
44
46
  if %w[l1 all].include?(scope)
@@ -54,7 +56,7 @@ module KairosMcp
54
56
  # L2 scanning
55
57
  # ---------------------------------------------------------------
56
58
 
57
- def scan_l2(result, since_session: nil, include_archive_candidates: true)
59
+ def scan_l2(result, since_session: nil, include_archive_candidates: true, include_l1_dedup: true)
58
60
  all_contexts = load_all_l2_contexts(since_session: since_session)
59
61
 
60
62
  # Partition: live contexts vs archived stubs
@@ -62,7 +64,20 @@ module KairosMcp
62
64
 
63
65
  # Promotion candidates: tag co-occurrence across sessions
64
66
  sessions_tags = build_sessions_tags(live)
65
- result[:promotion_candidates] = detect_promotion_candidates(sessions_tags)
67
+ candidates = detect_promotion_candidates(sessions_tags, all_sessions_tags: sessions_tags)
68
+
69
+ # L1 dedup: mark candidates that already exist as L1 knowledge
70
+ if include_l1_dedup
71
+ kp = knowledge_provider
72
+ existing_l1 = kp ? kp.list : []
73
+ candidates.each do |candidate|
74
+ match = find_l1_match(candidate[:tag], Array(candidate[:tag]), existing_l1)
75
+ candidate[:already_in_l1] = !match.nil?
76
+ candidate[:l1_match] = match if match
77
+ end
78
+ end
79
+
80
+ result[:promotion_candidates] = candidates
66
81
 
67
82
  # Consolidation candidates: name token overlap
68
83
  result[:consolidation_candidates] = detect_consolidation_candidates(live)
@@ -132,8 +147,11 @@ module KairosMcp
132
147
  # Detect tags that recur across min_recurrence+ distinct sessions.
133
148
  #
134
149
  # @param sessions_tags [Hash] { session_id => { context_name => [tags] } }
135
- # @return [Array<Hash>] promotion candidate hashes
136
- def detect_promotion_candidates(sessions_tags)
150
+ # @param all_sessions_tags [Hash] same as sessions_tags (for scoring)
151
+ # @return [Array<Hash>] promotion candidate hashes with confidence scoring
152
+ def detect_promotion_candidates(sessions_tags, all_sessions_tags: nil)
153
+ all_sessions_tags ||= sessions_tags
154
+
137
155
  tag_sessions = Hash.new { |h, k| h[k] = Set.new }
138
156
 
139
157
  sessions_tags.each do |session_id, contexts|
@@ -148,12 +166,15 @@ module KairosMcp
148
166
  .sort_by { |_tag, sids| -sids.size }
149
167
  .first(@max_candidates)
150
168
  .map do |tag, sids|
169
+ confidence = score_promotion_candidate(tag, sids, all_sessions_tags)
151
170
  {
152
171
  tag: tag,
153
172
  session_count: sids.size,
154
173
  sessions: sids.to_a,
155
174
  signal: 'tag_recurrence',
156
- strength: sids.size.to_f / @min_recurrence
175
+ strength: sids.size.to_f / @min_recurrence,
176
+ confidence: confidence,
177
+ already_in_l1: false
157
178
  }
158
179
  end
159
180
 
@@ -274,6 +295,80 @@ module KairosMcp
274
295
  tags
275
296
  end
276
297
 
298
+ # ---------------------------------------------------------------
299
+ # L1 dedup and confidence scoring (migrated from skills_promote auto_scan)
300
+ # ---------------------------------------------------------------
301
+
302
+ # Find an existing L1 entry that matches a candidate by name or tag overlap.
303
+ #
304
+ # @param candidate_name [String] suggested/tag name of the candidate
305
+ # @param candidate_tags [Array<String>] tags for the candidate
306
+ # @param existing_l1 [Array<Hash>] list from KnowledgeProvider#list
307
+ # @return [String, nil] matching L1 entry name or nil
308
+ def find_l1_match(candidate_name, candidate_tags, existing_l1)
309
+ existing_l1.each do |entry|
310
+ # Name match (normalized substring ratio)
311
+ name_sim = name_similarity(candidate_name, entry[:name])
312
+ return entry[:name] if name_sim > 0.8
313
+
314
+ # Tag overlap (Jaccard on name tokens vs entry tags)
315
+ entry_tags = Array(entry[:tags]).to_set
316
+ candidate_set = candidate_tags.to_set
317
+ sim = candidate_set.empty? && entry_tags.empty? ? 0.0 : (candidate_set & entry_tags).size.to_f / (candidate_set | entry_tags).size.to_f
318
+ return entry[:name] if sim > 0.8
319
+ end
320
+ nil
321
+ end
322
+
323
+ # Compute name similarity using token-level Jaccard.
324
+ #
325
+ # @param a [String]
326
+ # @param b [String]
327
+ # @return [Float] 0.0..1.0
328
+ def name_similarity(a, b)
329
+ a_parts = a.to_s.split('_').to_set
330
+ b_parts = b.to_s.split('_').to_set
331
+ return 0.0 if a_parts.empty? && b_parts.empty?
332
+ (a_parts & b_parts).size.to_f / [a_parts.size, b_parts.size].max
333
+ end
334
+
335
+ # Score a promotion candidate across 3 dimensions:
336
+ # - recurrence: how many sessions the tag appears in (max 40)
337
+ # - tag_consistency: co-occurrence consistency across sessions (max 30)
338
+ # - session_diversity: distinct sessions (max 30)
339
+ #
340
+ # @param tag [String] the recurring tag
341
+ # @param sessions [Set<String>] session IDs where this tag appears
342
+ # @param all_sessions_tags [Hash] { session_id => { context_name => [tags] } }
343
+ # @return [Integer] 0..100
344
+ def score_promotion_candidate(tag, sessions, all_sessions_tags)
345
+ # Recurrence: 3=10, 4=20, 5=30, 6+=40 (max 40 points)
346
+ recurrence_score = [[sessions.size - 2, 4].min * 10, 40].min
347
+ recurrence_score = [recurrence_score, 0].max
348
+
349
+ # Tag consistency: what fraction of sessions containing this tag
350
+ # also share the same co-occurring tags?
351
+ co_tags_per_session = sessions.map do |sid|
352
+ contexts = all_sessions_tags[sid] || {}
353
+ contexts.values.flatten.uniq.reject { |t| t == tag }
354
+ end.reject(&:empty?)
355
+
356
+ if co_tags_per_session.size >= 2
357
+ co_sets = co_tags_per_session.map(&:to_set)
358
+ intersection = co_sets.reduce(:&)
359
+ union = co_sets.reduce(:|)
360
+ tag_consistency = union.empty? ? 0.0 : intersection.size.to_f / union.size
361
+ else
362
+ tag_consistency = 0.0
363
+ end
364
+ tag_score = (tag_consistency * 30).round
365
+
366
+ # Session diversity: distinct sessions (max 30 points)
367
+ session_score = [[sessions.size - 1, 3].min * 10, 30].min
368
+
369
+ [recurrence_score + tag_score + session_score, 100].min
370
+ end
371
+
277
372
  # ---------------------------------------------------------------
278
373
  # Helpers
279
374
  # ---------------------------------------------------------------
@@ -42,6 +42,10 @@ module KairosMcp
42
42
  include_archive_candidates: {
43
43
  type: 'boolean',
44
44
  description: 'Whether to detect stale L2 contexts for archival. Default: true'
45
+ },
46
+ include_l1_dedup: {
47
+ type: 'boolean',
48
+ description: 'Check promotion candidates against existing L1 knowledge to mark duplicates. Default: true'
45
49
  }
46
50
  },
47
51
  required: []
@@ -55,7 +59,8 @@ module KairosMcp
55
59
  scan_result = scanner.scan(
56
60
  scope: arguments['scope'] || config.dig('scan', 'default_scope') || 'l2',
57
61
  since_session: arguments['since_session'],
58
- include_archive_candidates: arguments.fetch('include_archive_candidates', true)
62
+ include_archive_candidates: arguments.fetch('include_archive_candidates', true),
63
+ include_l1_dedup: arguments.fetch('include_l1_dedup', true)
59
64
  )
60
65
 
61
66
  # Record findings on blockchain if non-empty
@@ -131,7 +136,9 @@ module KairosMcp
131
136
  lines << "_No recurring patterns detected._"
132
137
  else
133
138
  promo.each do |c|
134
- lines << "- **#{c[:tag]}**: #{c[:session_count]} sessions (strength: #{c[:strength].round(2)})"
139
+ dedup_marker = c[:already_in_l1] ? " [already in L1: #{c[:l1_match]}]" : ''
140
+ confidence_str = c[:confidence] ? " (confidence: #{c[:confidence]})" : ''
141
+ lines << "- **#{c[:tag]}**: #{c[:session_count]} sessions (strength: #{c[:strength].round(2)})#{confidence_str}#{dedup_marker}"
135
142
  end
136
143
  end
137
144
  lines << ""
@@ -0,0 +1,4 @@
1
+ introspection:
2
+ health:
3
+ staleness_days: 180
4
+ report_format: "markdown"
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Introspection Guide
3
+ description: Usage guide for the introspection SkillSet — self-inspection, health scoring, and safety visibility
4
+ version: "0.1.0"
5
+ tags:
6
+ - introspection
7
+ - health
8
+ - safety
9
+ - blockchain
10
+ - maintenance
11
+ ---
12
+
13
+ # Introspection Guide
14
+
15
+ ## Overview
16
+
17
+ The introspection SkillSet provides self-inspection capabilities for KairosChain.
18
+ It examines knowledge health, blockchain integrity, and safety mechanisms to produce
19
+ actionable reports and recommendations.
20
+
21
+ ## Tools
22
+
23
+ ### introspection_check
24
+
25
+ Full self-inspection combining all domains. Returns a consolidated report with
26
+ recommendations.
27
+
28
+ ```
29
+ introspection_check() # All domains, markdown format
30
+ introspection_check(format: "json") # JSON output
31
+ introspection_check(domains: ["health"]) # Health only
32
+ introspection_check(domains: ["blockchain"]) # Blockchain only
33
+ ```
34
+
35
+ ### introspection_health
36
+
37
+ Focused L1 knowledge health scoring.
38
+
39
+ ```
40
+ introspection_health() # All entries
41
+ introspection_health(name: "my_knowledge") # Single entry
42
+ introspection_health(below_threshold: 0.5) # Only unhealthy entries
43
+ introspection_health(sort_by: "name") # Sort alphabetically
44
+ ```
45
+
46
+ ### introspection_safety
47
+
48
+ Safety mechanism visibility across all layers.
49
+
50
+ ```
51
+ introspection_safety() # Returns 4-layer safety report
52
+ ```
53
+
54
+ ## Health Scoring
55
+
56
+ Health scores range from 0.0 (unhealthy) to 1.0 (healthy).
57
+
58
+ When Synoptis TrustScorer is available:
59
+ - **Trust score** (70% weight): Based on attestation count and quality
60
+ - **Staleness score** (30% weight): Based on file modification time
61
+
62
+ When TrustScorer is not available:
63
+ - **Staleness score only** (100%): Linear decay over configurable threshold
64
+
65
+ ### Staleness Threshold
66
+
67
+ Default: 180 days. Configure in `config/introspection.yml`:
68
+
69
+ ```yaml
70
+ introspection:
71
+ health:
72
+ staleness_days: 90 # More aggressive freshness requirement
73
+ ```
74
+
75
+ ## Safety Layers
76
+
77
+ The safety report covers four layers:
78
+
79
+ 1. **L0 Approval Workflow**: Whether Kairos DSL approval_workflow skill is loaded
80
+ 2. **Runtime RBAC**: Registered Safety policies (can_modify_l0, etc.)
81
+ 3. **Agent Safety Gates**: Autonomous mode limits from agent.yml
82
+ 4. **Blockchain Recording**: Chain integrity and block count
83
+
84
+ ## Recommendations
85
+
86
+ The full check generates prioritized recommendations:
87
+
88
+ - **CRITICAL**: Blockchain integrity failure
89
+ - **HIGH**: Missing L0 approval workflow
90
+ - **MEDIUM**: Low health scores on individual knowledge entries
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module KairosMcp
6
+ module SkillSets
7
+ module Introspection
8
+ # HealthScorer calculates health scores for L1 knowledge entries.
9
+ #
10
+ # When Synoptis TrustScorer is available, health is a weighted composite
11
+ # of trust score (70%) and staleness score (30%). When TrustScorer is
12
+ # unavailable (Synoptis not loaded), health equals staleness only.
13
+ #
14
+ # Staleness is computed from File.mtime of the knowledge .md file,
15
+ # decaying linearly over a configurable threshold (default 180 days).
16
+ class HealthScorer
17
+ TRUST_WEIGHT = 0.70
18
+ STALENESS_WEIGHT = 0.30
19
+
20
+ def initialize(user_context: nil, config: {})
21
+ @user_context = user_context
22
+ @config = config
23
+ @trust_scorer = build_trust_scorer
24
+ end
25
+
26
+ # Score all L1 knowledge entries.
27
+ #
28
+ # @return [Hash] :overall_health, :entry_count, :trust_scorer_available, :entries
29
+ def score_l1
30
+ provider = ::KairosMcp::KnowledgeProvider.new(nil, user_context: @user_context)
31
+ summaries = provider.list
32
+
33
+ scored = summaries.map { |summary| score_entry_from_summary(summary, provider) }
34
+ .sort_by { |e| e[:health_score] }
35
+
36
+ overall = scored.empty? ? 0.0 : (scored.sum { |e| e[:health_score] } / scored.size).round(4)
37
+
38
+ {
39
+ overall_health: overall,
40
+ entry_count: scored.size,
41
+ trust_scorer_available: !@trust_scorer.nil?,
42
+ entries: scored
43
+ }
44
+ end
45
+
46
+ # Score a single L1 knowledge entry by name.
47
+ #
48
+ # @param name [String] Knowledge entry name
49
+ # @return [Hash] :entry or :error
50
+ def score_single(name)
51
+ provider = ::KairosMcp::KnowledgeProvider.new(nil, user_context: @user_context)
52
+ entry = provider.get(name)
53
+ return { error: "Knowledge '#{name}' not found" } unless entry
54
+
55
+ {
56
+ entry: score_entry_from_skill_entry(entry),
57
+ trust_scorer_available: !@trust_scorer.nil?
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ # Score from a summary hash (from provider.list).
64
+ # Must call provider.get(name) to obtain SkillEntry with md_file_path.
65
+ def score_entry_from_summary(summary, provider)
66
+ skill_entry = provider.get(summary[:name])
67
+ if skill_entry
68
+ score_entry_from_skill_entry(skill_entry)
69
+ else
70
+ # Fallback: no SkillEntry found (shouldn't happen normally)
71
+ {
72
+ name: summary[:name],
73
+ health_score: 0.5,
74
+ trust_score: 0.0,
75
+ trust_details: {},
76
+ attestation_count: 0,
77
+ staleness_score: 0.5,
78
+ tags: summary[:tags]
79
+ }
80
+ end
81
+ end
82
+
83
+ # Score from a SkillEntry object (has md_file_path).
84
+ def score_entry_from_skill_entry(entry)
85
+ trust = if @trust_scorer
86
+ @trust_scorer.calculate("knowledge://#{entry.name}")
87
+ else
88
+ { score: 0.0, details: {}, attestation_count: 0, active_count: 0 }
89
+ end
90
+
91
+ staleness = calculate_staleness(entry)
92
+
93
+ # When TrustScorer unavailable, health = staleness only
94
+ health = if @trust_scorer
95
+ (trust[:score] * TRUST_WEIGHT + staleness * STALENESS_WEIGHT).round(4)
96
+ else
97
+ staleness.round(4)
98
+ end
99
+
100
+ {
101
+ name: entry.name,
102
+ health_score: health,
103
+ trust_score: trust[:score],
104
+ trust_details: trust[:details],
105
+ attestation_count: trust[:attestation_count] || 0,
106
+ staleness_score: staleness.round(4),
107
+ tags: entry.tags
108
+ }
109
+ end
110
+
111
+ # Calculate staleness score from file modification time.
112
+ # Returns 1.0 for freshly modified, decays to 0.0 over threshold days.
113
+ def calculate_staleness(entry)
114
+ md_path = entry.respond_to?(:md_file_path) ? entry.md_file_path : nil
115
+ return 0.5 unless md_path && File.exist?(md_path)
116
+
117
+ age_days = (Time.now - File.mtime(md_path)) / 86400.0
118
+ threshold = @config.dig('introspection', 'health', 'staleness_days') || 180
119
+ [1.0 - (age_days / threshold.to_f), 0.0].max
120
+ rescue StandardError
121
+ 0.5
122
+ end
123
+
124
+ def build_trust_scorer
125
+ return nil unless defined?(::Synoptis::TrustScorer)
126
+ registry = ::Synoptis::Registry::FileRegistry.new(
127
+ storage_dir: File.join(::KairosMcp.storage_dir, 'synoptis')
128
+ )
129
+ ::Synoptis::TrustScorer.new(registry: registry)
130
+ rescue StandardError
131
+ nil
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module KairosMcp
6
+ module SkillSets
7
+ module Introspection
8
+ # SafetyInspector reports on all active safety mechanisms across layers:
9
+ # - L0: approval_workflow (Kairos DSL)
10
+ # - RBAC: registered Safety policies
11
+ # - Agent: autonomous mode safety gates from agent.yml
12
+ # - Blockchain: chain integrity and block count
13
+ class SafetyInspector
14
+ # Inspect all safety layers and return a structured report.
15
+ #
16
+ # @return [Hash] :layers with 4 sub-keys
17
+ def inspect_safety
18
+ {
19
+ layers: {
20
+ l0_approval_workflow: inspect_approval_workflow,
21
+ runtime_rbac: inspect_rbac_policies,
22
+ agent_safety_gates: inspect_agent_gates,
23
+ blockchain_recording: inspect_blockchain_health
24
+ }
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def inspect_approval_workflow
31
+ if defined?(::Kairos) && ::Kairos.respond_to?(:skill)
32
+ skill = ::Kairos.skill(:approval_workflow)
33
+ {
34
+ present: !skill.nil?,
35
+ version: skill&.respond_to?(:version) ? skill.version : nil,
36
+ status: skill ? 'active' : 'not_loaded'
37
+ }
38
+ else
39
+ { present: false, status: 'kairos_not_available' }
40
+ end
41
+ end
42
+
43
+ def inspect_rbac_policies
44
+ names = ::KairosMcp::Safety.registered_policy_names
45
+ {
46
+ registered_count: names.size,
47
+ policies: names,
48
+ multiuser_active: names.include?('can_modify_l0')
49
+ }
50
+ end
51
+
52
+ def inspect_agent_gates
53
+ agent_config_path = File.join(::KairosMcp.skillsets_dir, 'agent', 'config', 'agent.yml')
54
+ if File.exist?(agent_config_path)
55
+ config = YAML.safe_load(File.read(agent_config_path)) || {}
56
+ autonomous = config['autonomous'] || {}
57
+ {
58
+ present: true,
59
+ max_cycles: autonomous['max_cycles'],
60
+ timeout: autonomous['timeout'],
61
+ max_llm_calls: autonomous['max_llm_calls'],
62
+ risk_budget: autonomous['risk_budget']
63
+ }
64
+ else
65
+ { present: false, status: 'agent_skillset_not_loaded' }
66
+ end
67
+ rescue StandardError => e
68
+ { present: false, error: e.message }
69
+ end
70
+
71
+ def inspect_blockchain_health
72
+ chain = ::KairosMcp::KairosChain::Chain.new
73
+ blocks = chain.chain
74
+ {
75
+ block_count: blocks.size,
76
+ last_recorded: blocks.last&.timestamp&.iso8601,
77
+ integrity: chain.valid?
78
+ }
79
+ rescue StandardError => e
80
+ { block_count: 0, integrity: false, error: e.message }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Introspection SkillSet loader
4
+ # Loads all library classes for the introspection SkillSet.
5
+
6
+ require_relative 'introspection/health_scorer'
7
+ require_relative 'introspection/safety_inspector'
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "introspection",
3
+ "version": "0.1.0",
4
+ "description": "Self-inspection and maintenance. Calculates knowledge health scores, checks blockchain integrity, and visualizes safety mechanisms.",
5
+ "author": "Masaomi Hatakeyama",
6
+ "layer": "L1",
7
+ "depends_on": [],
8
+ "provides": ["health_scoring", "integrity_check", "safety_visibility"],
9
+ "tool_classes": [
10
+ "KairosMcp::SkillSets::Introspection::Tools::IntrospectionCheck",
11
+ "KairosMcp::SkillSets::Introspection::Tools::IntrospectionHealth",
12
+ "KairosMcp::SkillSets::Introspection::Tools::IntrospectionSafety"
13
+ ],
14
+ "config_files": ["config/introspection.yml"],
15
+ "knowledge_dirs": ["knowledge/introspection_guide"],
16
+ "min_core_version": "3.9.0"
17
+ }
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ module KairosMcp
7
+ module SkillSets
8
+ module Introspection
9
+ module Tools
10
+ # Full self-inspection tool combining health, blockchain, and safety domains.
11
+ # Produces a consolidated report with recommendations.
12
+ class IntrospectionCheck < ::KairosMcp::Tools::BaseTool
13
+ def name
14
+ 'introspection_check'
15
+ end
16
+
17
+ def description
18
+ 'Full self-inspection: knowledge health scores, blockchain integrity, ' \
19
+ 'and safety mechanism visibility. Uses Synoptis TrustScorer when available.'
20
+ end
21
+
22
+ def category
23
+ :introspection
24
+ end
25
+
26
+ def usecase_tags
27
+ %w[health blockchain safety introspection maintenance]
28
+ end
29
+
30
+ def related_tools
31
+ %w[introspection_health introspection_safety chain_verify]
32
+ end
33
+
34
+ def input_schema
35
+ {
36
+ type: 'object',
37
+ properties: {
38
+ domains: {
39
+ type: 'array',
40
+ items: { type: 'string', enum: %w[health blockchain safety] },
41
+ description: 'Domains to inspect (default: all)'
42
+ },
43
+ format: {
44
+ type: 'string',
45
+ enum: %w[markdown json],
46
+ description: 'Output format (default: markdown)'
47
+ }
48
+ }
49
+ }
50
+ end
51
+
52
+ def call(arguments)
53
+ domains = arguments['domains'] || %w[health blockchain safety]
54
+ fmt = arguments['format'] || 'markdown'
55
+
56
+ report = { inspected_at: Time.now.iso8601 }
57
+
58
+ if domains.include?('health')
59
+ report[:health] = health_scorer.score_l1
60
+ end
61
+
62
+ if domains.include?('blockchain')
63
+ report[:blockchain] = check_blockchain
64
+ end
65
+
66
+ if domains.include?('safety')
67
+ report[:safety] = safety_inspector.inspect_safety
68
+ end
69
+
70
+ report[:recommendations] = build_recommendations(report)
71
+
72
+ if fmt == 'json'
73
+ text_content(JSON.pretty_generate(report))
74
+ else
75
+ text_content(format_markdown(report))
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def health_scorer
82
+ @health_scorer ||= HealthScorer.new(
83
+ user_context: @safety&.current_user,
84
+ config: load_config
85
+ )
86
+ end
87
+
88
+ def safety_inspector
89
+ @safety_inspector ||= SafetyInspector.new
90
+ end
91
+
92
+ def check_blockchain
93
+ chain = ::KairosMcp::KairosChain::Chain.new
94
+ valid = chain.valid?
95
+ blocks = chain.chain
96
+ {
97
+ valid: valid,
98
+ block_count: blocks.size,
99
+ latest_timestamp: blocks.last&.timestamp&.iso8601,
100
+ status: valid ? 'healthy' : 'INTEGRITY_FAILURE'
101
+ }
102
+ rescue StandardError => e
103
+ { valid: false, error: e.message, status: 'error' }
104
+ end
105
+
106
+ def build_recommendations(report)
107
+ recs = []
108
+
109
+ # Low health scores
110
+ if report[:health]
111
+ report[:health][:entries]&.each do |entry|
112
+ if entry[:health_score] < 0.4
113
+ recs << {
114
+ priority: 'medium',
115
+ target: entry[:name],
116
+ message: "Low health score (#{entry[:health_score]}). Consider updating or adding attestations."
117
+ }
118
+ end
119
+ end
120
+ end
121
+
122
+ # Blockchain issues
123
+ if report[:blockchain] && !report[:blockchain][:valid]
124
+ recs << { priority: 'critical', target: 'blockchain', message: 'Blockchain integrity check failed.' }
125
+ end
126
+
127
+ # Safety gaps
128
+ if report[:safety]
129
+ unless report.dig(:safety, :layers, :l0_approval_workflow, :present)
130
+ recs << { priority: 'high', target: 'approval_workflow', message: 'L0 approval workflow not loaded.' }
131
+ end
132
+ end
133
+
134
+ recs
135
+ end
136
+
137
+ def load_config
138
+ config_path = File.join(
139
+ ::KairosMcp.skillsets_dir, 'introspection', 'config', 'introspection.yml'
140
+ )
141
+ return {} unless File.exist?(config_path)
142
+ YAML.safe_load(File.read(config_path)) || {}
143
+ rescue StandardError
144
+ {}
145
+ end
146
+
147
+ def format_markdown(report)
148
+ lines = []
149
+ lines << "# Introspection Report"
150
+ lines << ""
151
+ lines << "**Inspected at**: #{report[:inspected_at]}"
152
+ lines << ""
153
+
154
+ if report[:health]
155
+ lines << "## Knowledge Health"
156
+ lines << ""
157
+ lines << "- **Overall health**: #{report[:health][:overall_health]}"
158
+ lines << "- **Entry count**: #{report[:health][:entry_count]}"
159
+ lines << "- **TrustScorer available**: #{report[:health][:trust_scorer_available]}"
160
+ lines << ""
161
+
162
+ if report[:health][:entries]&.any?
163
+ lines << "| Name | Health | Trust | Staleness | Attestations |"
164
+ lines << "|------|--------|-------|-----------|--------------|"
165
+ report[:health][:entries].each do |e|
166
+ lines << "| #{e[:name]} | #{e[:health_score]} | #{e[:trust_score]} | #{e[:staleness_score]} | #{e[:attestation_count]} |"
167
+ end
168
+ lines << ""
169
+ end
170
+ end
171
+
172
+ if report[:blockchain]
173
+ lines << "## Blockchain"
174
+ lines << ""
175
+ lines << "- **Valid**: #{report[:blockchain][:valid]}"
176
+ lines << "- **Block count**: #{report[:blockchain][:block_count]}"
177
+ lines << "- **Latest timestamp**: #{report[:blockchain][:latest_timestamp] || 'N/A'}"
178
+ lines << "- **Status**: #{report[:blockchain][:status]}"
179
+ lines << ""
180
+ end
181
+
182
+ if report[:safety]
183
+ lines << "## Safety Mechanisms"
184
+ lines << ""
185
+ report[:safety][:layers]&.each do |layer_name, layer_data|
186
+ lines << "### #{layer_name}"
187
+ layer_data.each { |k, v| lines << "- **#{k}**: #{v}" }
188
+ lines << ""
189
+ end
190
+ end
191
+
192
+ if report[:recommendations]&.any?
193
+ lines << "## Recommendations"
194
+ lines << ""
195
+ report[:recommendations].each do |rec|
196
+ lines << "- [#{rec[:priority].upcase}] **#{rec[:target]}**: #{rec[:message]}"
197
+ end
198
+ lines << ""
199
+ end
200
+
201
+ lines.join("\n")
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module KairosMcp
6
+ module SkillSets
7
+ module Introspection
8
+ module Tools
9
+ # Calculate health scores for L1 knowledge entries.
10
+ # Uses Synoptis TrustScorer when available, falls back to staleness-only scoring.
11
+ class IntrospectionHealth < ::KairosMcp::Tools::BaseTool
12
+ def name
13
+ 'introspection_health'
14
+ end
15
+
16
+ def description
17
+ 'Calculate health scores for L1 knowledge entries. ' \
18
+ 'Uses Synoptis TrustScorer when available, falls back to staleness-only scoring.'
19
+ end
20
+
21
+ def category
22
+ :introspection
23
+ end
24
+
25
+ def usecase_tags
26
+ %w[health knowledge staleness trust introspection]
27
+ end
28
+
29
+ def related_tools
30
+ %w[introspection_check knowledge_list]
31
+ end
32
+
33
+ def input_schema
34
+ {
35
+ type: 'object',
36
+ properties: {
37
+ name: { type: 'string', description: 'Specific L1 knowledge name (optional, omit for all)' },
38
+ sort_by: { type: 'string', enum: %w[score name], description: 'Sort order (default: score)' },
39
+ below_threshold: { type: 'number', description: 'Only show entries below this score (0.0-1.0)' }
40
+ }
41
+ }
42
+ end
43
+
44
+ def call(arguments)
45
+ scorer = HealthScorer.new(user_context: @safety&.current_user, config: load_config)
46
+ result = if arguments['name']
47
+ scorer.score_single(arguments['name'])
48
+ else
49
+ scorer.score_l1
50
+ end
51
+
52
+ # Filter
53
+ if arguments['below_threshold'] && result[:entries]
54
+ result[:entries].select! { |e| e[:health_score] < arguments['below_threshold'] }
55
+ end
56
+
57
+ # Sort
58
+ if arguments['sort_by'] == 'name' && result[:entries]
59
+ result[:entries].sort_by! { |e| e[:name] }
60
+ end
61
+
62
+ text_content(JSON.pretty_generate(result))
63
+ end
64
+
65
+ private
66
+
67
+ def load_config
68
+ config_path = File.join(
69
+ KairosMcp.skillsets_dir, 'introspection', 'config', 'introspection.yml'
70
+ )
71
+ return {} unless File.exist?(config_path)
72
+ YAML.safe_load(File.read(config_path)) || {}
73
+ rescue StandardError
74
+ {}
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module KairosMcp
6
+ module SkillSets
7
+ module Introspection
8
+ module Tools
9
+ # Visualize all active safety mechanisms across layers:
10
+ # L0 approval workflow, RBAC policies, agent safety gates, blockchain health.
11
+ class IntrospectionSafety < ::KairosMcp::Tools::BaseTool
12
+ def name
13
+ 'introspection_safety'
14
+ end
15
+
16
+ def description
17
+ 'Visualize all active safety mechanisms across layers: ' \
18
+ 'L0 approval workflow, RBAC policies, agent safety gates, blockchain health.'
19
+ end
20
+
21
+ def category
22
+ :introspection
23
+ end
24
+
25
+ def usecase_tags
26
+ %w[safety rbac approval blockchain introspection]
27
+ end
28
+
29
+ def related_tools
30
+ %w[introspection_check chain_verify]
31
+ end
32
+
33
+ def input_schema
34
+ { type: 'object', properties: {} }
35
+ end
36
+
37
+ def call(_arguments)
38
+ inspector = SafetyInspector.new
39
+ result = inspector.inspect_safety
40
+ text_content(JSON.pretty_generate(result))
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ 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.9.4
4
+ version: 3.10.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-03-30 00:00:00.000000000 Z
11
+ date: 2026-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -299,6 +299,15 @@ files:
299
299
  - templates/skillsets/hestia/tools/meeting_publish_needs.rb
300
300
  - templates/skillsets/hestia/tools/philosophy_anchor.rb
301
301
  - templates/skillsets/hestia/tools/record_observation.rb
302
+ - templates/skillsets/introspection/config/introspection.yml
303
+ - templates/skillsets/introspection/knowledge/introspection_guide/introspection_guide.md
304
+ - templates/skillsets/introspection/lib/introspection.rb
305
+ - templates/skillsets/introspection/lib/introspection/health_scorer.rb
306
+ - templates/skillsets/introspection/lib/introspection/safety_inspector.rb
307
+ - templates/skillsets/introspection/skillset.json
308
+ - templates/skillsets/introspection/tools/introspection_check.rb
309
+ - templates/skillsets/introspection/tools/introspection_health.rb
310
+ - templates/skillsets/introspection/tools/introspection_safety.rb
302
311
  - templates/skillsets/knowledge_creator/config/knowledge_creator.yml
303
312
  - templates/skillsets/knowledge_creator/knowledge/creation_guide/creation_guide.md
304
313
  - templates/skillsets/knowledge_creator/knowledge/quality_criteria/quality_criteria.md
@@ -462,7 +471,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
462
471
  - !ruby/object:Gem::Version
463
472
  version: '0'
464
473
  requirements: []
465
- rubygems_version: 3.3.26
474
+ rubygems_version: 3.5.22
466
475
  signing_key:
467
476
  specification_version: 4
468
477
  summary: KairosChain - Self-referential MCP server for auditable skill self-management