kairos-chain 3.50.0 → 3.52.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/bin/kairos-chain +58 -18
  4. data/lib/kairos_mcp/version.rb +1 -1
  5. data/templates/knowledge/multi_llm_review_workflow/multi_llm_review_workflow.md +17 -10
  6. data/templates/skillsets/agent/bin/agent_step_worker.rb +183 -0
  7. data/templates/skillsets/agent/lib/agent/advance_gate.rb +357 -0
  8. data/templates/skillsets/agent/lib/agent/session.rb +12 -3
  9. data/templates/skillsets/agent/lib/agent/step_delegation.rb +344 -0
  10. data/templates/skillsets/agent/lib/agent.rb +2 -0
  11. data/templates/skillsets/agent/skillset.json +2 -1
  12. data/templates/skillsets/agent/test/test_agent_interruption_resilience.rb +729 -0
  13. data/templates/skillsets/agent/tools/agent_status.rb +42 -3
  14. data/templates/skillsets/agent/tools/agent_step.rb +299 -18
  15. data/templates/skillsets/agent/tools/agent_stop.rb +52 -14
  16. data/templates/skillsets/agent/tools/agent_wait.rb +191 -0
  17. data/templates/skillsets/chain_distillation/lib/chain_distillation/carrier_wiring.rb +187 -0
  18. data/templates/skillsets/chain_distillation/lib/chain_distillation/depositor.rb +463 -0
  19. data/templates/skillsets/chain_distillation/skillset.json +23 -6
  20. data/templates/skillsets/chain_distillation/test/test_chain_distillation.rb +656 -0
  21. data/templates/skillsets/chain_distillation/tools/cd_deposit.rb +69 -0
  22. data/templates/skillsets/chain_distillation/tools/cd_distill.rb +13 -1
  23. data/templates/skillsets/chain_distillation/tools/cd_revoke.rb +11 -1
  24. data/templates/skillsets/confidentiality_guard/config/profile.example.yml +3 -1
  25. data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/surfaces.rb +1 -0
  26. data/templates/skillsets/confidentiality_guard/skillset.json +1 -1
  27. data/templates/skillsets/confidentiality_guard/test/test_cg_guard.rb +61 -2
  28. data/templates/skillsets/multi_llm_review/config/multi_llm_review.yml +10 -5
  29. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 826aabda2632b42bb5e4c688f443616faf29b9b97daba55aa02fa6834bf1629d
4
- data.tar.gz: 2a743cfa76eb0d56db2220ed40c11ea0cc52f88c9b16ed8dfb84855d54b15981
3
+ metadata.gz: 33bc325db559dbb4c9bfbd94ea25fb3ebd783edee32cf6cf4fe4a357b0d58bf1
4
+ data.tar.gz: 538dc94bc2eb732cce8233161cff7e972d17bf62894bf88ffc9847fb680f1de8
5
5
  SHA512:
6
- metadata.gz: 0e0ab6e95d0f527e738053fe45e9899e761820809c0f26ab37dd55eacadf13b387b22737475a8685b894d095aa9ba4529df142ab3809adf917c2ff275b46f403
7
- data.tar.gz: 1272d3d2ffd8cc80615dc1990346758982cb64cb78d947965d754d3ae5d0e148524044c8197efbb836e0ecf067cfcae69f51aa90a934a71da691525c0b190da2
6
+ metadata.gz: 916eedc6aaa54819797ee50ca77039a77516cf85c821c71c22e410882d654e06c2f4fcc778bb92fb38631046c7d3bec02c2fee17127fe11a9a4e9a31150d525a
7
+ data.tar.gz: 1bfe55dad79465dc24e9d3d29556d0f6bed30d39675974b1d5833152235206fda25b31b82129a7d5e9754f33feedcbe1d7e8d455b873dabbc52f8f3c3b02a2f8
data/CHANGELOG.md CHANGED
@@ -4,6 +4,81 @@ 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.51.3] - 2026-07-23
8
+
9
+ ### Multi-LLM Review — knowledge template sync (v3.6.1)
10
+
11
+ Completes the 3.51.2 roster update: `templates/knowledge/multi_llm_review_workflow/`
12
+ is synced to v3.6.1 (gpt-5.6-sol roster). 3.51.2 updated the skillset config
13
+ template only, so `kairos-chain upgrade` reported a knowledge conflict against
14
+ instances already carrying the v3.6.1 L1 locally. No code change.
15
+
16
+ ## [3.51.2] - 2026-07-23
17
+
18
+ ### Multi-LLM Review — Codex roster update (gpt-5.6-sol)
19
+
20
+ Template config only, no code change. In
21
+ `templates/skillsets/multi_llm_review/config/multi_llm_review.yml`, the Codex
22
+ reviewer `gpt-5.4` is retired and replaced by `gpt-5.6-sol`
23
+ (`role_label: codex_gpt5.6-sol`). `gpt-5.5` stays as the calibrated anchor
24
+ for cross-generation comparison. 5.6-sol's reviewer bias profile is not yet
25
+ calibrated — record (a)/(b)/(c) breakdowns per round until a profile
26
+ accumulates. Roster size (6) and convergence rules are unchanged. The
27
+ matching L1 knowledge `multi_llm_review_workflow` was updated to v3.6.1
28
+ (instance-local, chain-recorded).
29
+
30
+ ## [3.51.1] - 2026-07-23
31
+
32
+ ### CLI — `skillset upgrade` transparency log
33
+
34
+ `kairos-chain skillset upgrade` (both check and `--apply`) now prints a
35
+ structured report mirroring `kairos-chain upgrade --apply`, so a no-op run is
36
+ visibly a completed check rather than a bare `All SkillSets are up to date.`
37
+ one-liner. Every CHECKED SkillSet (an installed one with a gem-template
38
+ counterpart) is listed with its status, followed by a summary:
39
+
40
+ - `[UPGRADE]` / `[UPGRADED]` — version/files changed (changed files listed on
41
+ check), and, on `--apply`, `vFROM -> vTO (N files)`.
42
+ - `[UNCHANGED]` — checked, no difference (the case that was previously invisible).
43
+ - `[NEW-CORE]` — a core SkillSet not yet installed, installed on `--apply`.
44
+ - `[OPTIONAL]` — a non-core new SkillSet, never auto-installed (explicit approval).
45
+ - `## Summary` — counts of upgraded/installed, unchanged, and optional.
46
+
47
+ Display-only: the upgrade decision (`upgrade_check` / `upgrade_apply`) is
48
+ unchanged; `core_only: true` behavior for the plain CLI form is preserved.
49
+
50
+ ## [3.51.0] - 2026-07-23
51
+
52
+ ### Agent SkillSet — interruption resilience A-1 + A-2
53
+
54
+ Addresses the failure mode where the orchestrator's API stalling leaves an
55
+ auto-mode agent loop waiting on input: the running MCP server is intact, but
56
+ the driver stops, and auto-permission is orthogonal to driver liveness.
57
+ Design v0.3.1 FROZEN (design-by-invariant, multi-LLM review 3R converged).
58
+
59
+ - **A-1 — AdvanceGate** (`lib/agent/advance_gate.rb`): every state-advancing
60
+ `agent_step` call passes through a per-session serialized, atomic, anchored
61
+ gate. INV-A2 (serialized atomic advance via `advance.lock` + tmp-rename
62
+ commit), INV-A3 (anchored at-most-once with a `seq:state:cycle` anchor,
63
+ committed outcomes replay on retry, side-effect intent bracket with
64
+ no-silent-drop), INV-A4 (`next_move` derivable from persisted state),
65
+ INV-A5 (adjudication as a gated judgment). `adjudicate` action added.
66
+ Impl review 4R converged, 49 probes.
67
+ - **A-2 — delegated step executor** (`lib/agent/step_delegation.rb`,
68
+ `bin/agent_step_worker.rb`): INV-A1 driver independence. `agent_step` with
69
+ `execution: "delegated"` returns a resumable handle while a detached
70
+ (setsid) worker re-enters the SAME gated `agent_step` under server-side
71
+ ownership — delegate → wait → collect, transplanted from the review
72
+ SkillSet. All correctness inherited from the A-1 gate; the delegation layer
73
+ is coordination/observability only. New `agent_wait` tool; `agent_status`
74
+ surfaces the handle. Impl review 6R converged, 81 probes total.
75
+ - Attended real-worker run with a live LLM-bearing step validated
76
+ (2026-07-23, isolated harness): the detached worker bootstrapped the full
77
+ ToolRegistry and ran ORIENT+DECIDE via the claude CLI; liveness, collect,
78
+ and collector-owned teardown all held.
79
+ - Native body path unchanged (selectable-off). All prior agent-suite tests
80
+ remain at the main baseline.
81
+
7
82
  ## [3.50.0] - 2026-07-23
8
83
 
9
84
  ### Chain Distillation SkillSet — slice 1 (CD-1..CD-6)
data/bin/kairos-chain CHANGED
@@ -141,18 +141,43 @@ when 'skillset'
141
141
  manual = upgrades.select { |u| u[:new_skillset] && !KairosMcp::SkillSetManager::CORE_SKILLSETS.include?(u[:name]) }
142
142
  applicable = upgrades - manual
143
143
 
144
- if upgrades.empty?
145
- puts "All SkillSets are up to date."
146
- elsif apply
144
+ # Diagnostic transparency: report the full set of CHECKED SkillSets — not
145
+ # only the ones that changed — so a no-op run is visibly a completed check
146
+ # rather than a bare one-liner, mirroring `kairos-chain upgrade --apply`.
147
+ # This is display-only; the upgrade decision itself (upgrade_check /
148
+ # upgrade_apply) is unchanged. "Checked" = an installed SkillSet that has a
149
+ # gem-template counterpart (exactly what upgrade_check compares).
150
+ templates_dir = File.join(KairosMcp.gem_root, 'templates', 'skillsets')
151
+ checkable = manager.all_skillsets
152
+ .select { |ss| File.directory?(File.join(templates_dir, ss.name)) }
153
+ .sort_by(&:name)
154
+ existing_upgrade_names = applicable.reject { |u| u[:new_skillset] }.map { |u| u[:name] }
155
+ new_core = applicable.select { |u| u[:new_skillset] }
156
+
157
+ if apply
147
158
  results = manager.upgrade_apply(core_only: true)
159
+ upgraded_names = results.map { |r| r[:name] }
160
+ puts "# Applying SkillSet Upgrade"
161
+ puts ""
162
+ puts "## SkillSets"
148
163
  results.each do |r|
149
- puts "Updated #{r[:name]}: v#{r[:from]} -> v#{r[:to]} (#{r[:files_updated]} files)"
164
+ puts " [UPGRADED] #{r[:name].ljust(22)} v#{r[:from]} -> v#{r[:to]} (#{r[:files_updated]} files)"
165
+ end
166
+ checkable.reject { |ss| upgraded_names.include?(ss.name) }.each do |ss|
167
+ puts " [UNCHANGED] #{ss.name.ljust(22)} v#{ss.version}"
150
168
  end
151
- puts "Done. #{results.size} SkillSet(s) upgraded."
169
+ manual.each do |u|
170
+ puts " [OPTIONAL] #{u[:name].ljust(22)} v#{u[:available_version]} (not installed)"
171
+ end
172
+ puts ""
173
+ puts "## Summary"
174
+ puts " Upgraded/installed: #{results.size}"
175
+ puts " Unchanged: #{checkable.reject { |ss| upgraded_names.include?(ss.name) }.size}"
176
+ puts " Optional (not installed): #{manual.size}"
152
177
 
153
178
  if manual.any?
154
179
  puts ""
155
- puts "Not installed (optional, requires explicit approval):"
180
+ puts "Optional SkillSets require explicit approval:"
156
181
  manual.each { |u| puts " kairos-chain skillset install #{u[:name]}" }
157
182
  end
158
183
 
@@ -174,24 +199,39 @@ when 'skillset'
174
199
  end
175
200
  end
176
201
  else
177
- if applicable.any?
178
- puts "SkillSet upgrades available:"
179
- puts ""
180
- applicable.each do |u|
202
+ upgrade_by_name = applicable.reject { |u| u[:new_skillset] }.each_with_object({}) { |u, h| h[u[:name]] = u }
203
+ puts "# SkillSet Upgrade Check"
204
+ puts ""
205
+ puts "## SkillSets"
206
+ checkable.each do |ss|
207
+ if (u = upgrade_by_name[ss.name])
181
208
  label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
182
- puts " #{u[:name]}: #{label}"
183
- u[:changed_files].each { |f| puts " Modified: #{f}" }
184
- puts ""
209
+ puts " [UPGRADE] #{ss.name.ljust(22)} #{label}"
210
+ u[:changed_files].each { |f| puts " #{f}" }
211
+ else
212
+ puts " [UNCHANGED] #{ss.name.ljust(22)} v#{ss.version}"
185
213
  end
186
- puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
187
214
  end
215
+ new_core.each do |u|
216
+ puts " [NEW-CORE] #{u[:name].ljust(22)} v#{u[:available_version]} (will install on --apply)"
217
+ end
218
+ manual.each do |u|
219
+ puts " [OPTIONAL] #{u[:name].ljust(22)} v#{u[:available_version]} (not installed)"
220
+ end
221
+ puts ""
222
+ puts "## Summary"
223
+ puts " Available upgrades: #{applicable.size}"
224
+ puts " Unchanged: #{checkable.size - existing_upgrade_names.size}"
225
+ puts " Optional (not installed): #{manual.size}"
188
226
 
227
+ if applicable.any?
228
+ puts ""
229
+ puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
230
+ end
189
231
  if manual.any?
190
- puts "" if applicable.any?
191
- puts "Optional SkillSets available (not auto-installed; require explicit approval):"
192
- manual.each { |u| puts " #{u[:name]} v#{u[:available_version]} — #{u[:description]}" }
193
232
  puts ""
194
- puts "Install with: kairos-chain skillset install <name>"
233
+ puts "Optional SkillSets (require explicit approval):"
234
+ manual.each { |u| puts " kairos-chain skillset install #{u[:name]} # #{u[:description]}" }
195
235
  end
196
236
  end
197
237
 
@@ -1,4 +1,4 @@
1
1
  module KairosMcp
2
- VERSION = "3.50.0"
2
+ VERSION = "3.52.0"
3
3
  CHANGELOG_URL = "https://github.com/masaomi/KairosChain_2026/blob/main/CHANGELOG.md"
4
4
  end
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: multi_llm_review_workflow
3
3
  description: "Multi-LLM review methodology and execution — workflow pattern, CLI tooling, consensus analysis, Persona Assembly. Applicable to design, implementation, documentation, or any artifact."
4
- version: "3.6"
4
+ version: "3.6.1"
5
5
  tags:
6
6
  - workflow
7
7
  - review
@@ -345,7 +345,7 @@ starting** and verify each against `config/multi_llm_review.yml`:
345
345
  - [ ] Your model (orchestrator): ___
346
346
  - [ ] Agent Team Personas model: = orchestrator model (NOT a different model)
347
347
  - [ ] Subprocess CLI models: Opus 4.6 AND Opus 4.8 (both, not either/or)
348
- - [ ] Codex models: gpt-5.5 (default) AND gpt-5.4 (both, not either/or)
348
+ - [ ] Codex models: gpt-5.6-sol (default) AND gpt-5.5 (both, not either/or)
349
349
  - [ ] Cursor model: default (composer-2.5, no --model flag)
350
350
  - [ ] Total reviewer count: 6 (or 5 after orchestrator exclusion from subprocess)
351
351
  - [ ] Convergence rule: 4/6 APPROVE (full) or 3/5 APPROVE (after exclusion)
@@ -356,7 +356,7 @@ starting** and verify each against `config/multi_llm_review.yml`:
356
356
  | Mistake | Correct behavior | Why it happens |
357
357
  |---------|-----------------|----------------|
358
358
  | Exclude orchestrator model from Agent Team Personas | Agent Team uses orchestrator model — they provide persona diversity, not epistemic diversity | LLM misreads "do not assign yourself as a reviewer" as applying to Agent Team; it applies only to subprocess CLI |
359
- | Run only Codex GPT-5.4, skip 5.5 | Run both — they catch different things (5.5 found §5 schema contradiction in Phase 2 Case A that no other reviewer caught) | Cost-saving heuristic; roster has both for a reason |
359
+ | Run only Codex GPT-5.6-sol, skip 5.5 | Run both — cross-generation entries catch different things (5.5 found §5 schema contradiction in Phase 2 Case A that no other reviewer caught) | Cost-saving heuristic; roster has both for a reason |
360
360
  | Use a smaller/cheaper model as Agent Team substitute | Use the orchestrator's own model with different personas | Confusing "model diversity" with "persona diversity" — Agent Team is the latter |
361
361
  | Run 3 reviewers instead of 6 (or 5 after exclusion) | Use the full roster from config | Ad-hoc "3 is enough" reasoning; config specifies 6 for empirical reasons |
362
362
 
@@ -580,7 +580,7 @@ which claude 2>/dev/null && echo "claude: available" || echo "claude: NOT FOUND"
580
580
 
581
581
  | Tool | Command | Prompt Input | Output Collection | Model |
582
582
  |------|---------|-------------|-------------------|-------|
583
- | **Codex** | `codex exec -m <model>` | stdin pipe: `cat prompt.md \| codex exec -` | `-o /path/output.md` | GPT-5.5 + GPT-5.4 (both roster entries, `-m` per entry) |
583
+ | **Codex** | `codex exec -m <model>` | stdin pipe: `cat prompt.md \| codex exec -` | `-o /path/output.md` | GPT-5.6-sol + GPT-5.5 (both roster entries, `-m` per entry) |
584
584
  | **Cursor Agent** | `agent -p` | File reference (stdin NOT supported) | stdout redirect: `> output.md` | Composer-2.5 (default) |
585
585
  | **Claude Code** | Agent tool (internal) | Direct prompt string | Write to workspace file | Fable 5 (session) |
586
586
  | **Claude CLI (4.6)** | `claude -p --model claude-opus-4-6 --bare` | stdin pipe: `cat prompt.md \| claude -p --model claude-opus-4-6 --bare` | stdout redirect: `> output.md` | Opus 4.6 |
@@ -597,7 +597,7 @@ Based on cross-evaluation experiment (7 models × 4 tasks + Nomic, 518 CLI calls
597
597
  | **Reviewer: Claude CLI** | Opus 4.6 / Opus 4.8 | (default; config `effort: medium`) | Evaluator quality is effort-independent (low≈high: 8.35 vs 8.16) — per 2026-04-29 policy reviewers stay at default |
598
598
  | **Coding sub-agent** | Opus 4.7 | `--effort medium` | Cost-effective default; use `high` for complex tasks |
599
599
  | **Design sub-agent** | Opus 4.7 | `--effort medium` | Cost-effective default; use `high` for complex tasks |
600
- | **Codex** | GPT-5.5 (default) | (no flag) | Fixed effort |
600
+ | **Codex** | GPT-5.6-sol / GPT-5.5 | (no flag) | Fixed effort |
601
601
  | **Cursor Agent** | Composer-2.5 | (no flag) | Fixed effort |
602
602
 
603
603
  Note (2026-06-10): the effort experiment data is from the Opus 4.6/4.7
@@ -898,11 +898,11 @@ Step 1: Generate review prompt
898
898
  Step 2: Detect environment and models
899
899
  - Run: which codex && which agent && which claude
900
900
  - Detect default models
901
- - Report: "Auto mode: Codex (gpt-5.5, gpt-5.4), Agent (composer-2.5), Claude Team (claude-fable-5), Claude CLI (opus-4.6, opus-4.8)"
901
+ - Report: "Auto mode: Codex (gpt-5.6-sol, gpt-5.5), Agent (composer-2.5), Claude Team (claude-fable-5), Claude CLI (opus-4.6, opus-4.8)"
902
902
 
903
903
  Step 3: Execute N reviews in parallel (default 6 reviewers)
904
904
  - Bash(background): cat prompt.md | codex exec -m gpt-5.5 -C workspace -o log/review_codex_gpt5.5.md -
905
- - Bash(background): cat prompt.md | codex exec -m gpt-5.4 -C workspace -o log/review_codex_gpt5.4.md -
905
+ - Bash(background): cat prompt.md | codex exec -m gpt-5.6-sol -C workspace -o log/review_codex_gpt5.6-sol.md -
906
906
  - Bash(background): agent -p --trust "Read prompt and review..." > log/review_cursor.md
907
907
  - Agent(background): Claude Team (orchestrator model, Fable 5) → write to log/review_claude_team_fable5.md
908
908
  - Bash(background): cat prompt.md | claude -p --model claude-opus-4-6 --bare > log/review_claude_opus4.6.md 2>log/review_claude_opus4.6.stderr.log
@@ -944,10 +944,10 @@ log/{artifact}_review{N}_consensus_{date}.md # Consensus analysis
944
944
  ```
945
945
 
946
946
  LLM identifiers: `claude_team_fable5`, `claude_cli_opus4.6`, `claude_cli_opus4.8`,
947
- `codex_gpt5.5`, `codex_gpt5.4`, `cursor_composer2.5`, `cursor_gpt5.4`,
947
+ `codex_gpt5.6-sol`, `codex_gpt5.5`, `cursor_composer2.5`, `cursor_gpt5.4`,
948
948
  `cursor_premium`
949
949
  (legacy, pre-2026-06-10: `claude_opus4.6`, `claude_team_opus4.6`, `claude_team_opus4.7`,
950
- `claude_cli_opus4.7`, `cursor_composer2`)
950
+ `claude_cli_opus4.7`, `cursor_composer2`; retired 2026-07-23: `codex_gpt5.4`)
951
951
 
952
952
  ## Internal Agent Team Review
953
953
 
@@ -997,6 +997,13 @@ Compression ratio: parallel agent raw → Assembly ≈ 2:1
997
997
  epistemic diversity — the personas that authored R1 findings all approved
998
998
  their own fixes in R2, while subprocess reviewers caught the seams.
999
999
  Design record: `docs/drafts/multi_llm_review_unknowns_pass_v0.3.1_FROZEN.md`
1000
+ - Roster update (v3.6.1, 2026-07-23): Codex gpt-5.6-sol replaces gpt-5.4
1001
+ (retired — its "everyday coding" register is covered by the frontier
1002
+ entries). gpt-5.5 retained as the calibrated anchor for cross-generation
1003
+ comparison. 5.6-sol's reviewer bias profile is uncalibrated — record
1004
+ (a)/(b)/(c) breakdowns per round until a profile accumulates in
1005
+ `multi_llm_reviewer_evaluation`; watch for agentic-coding drift into
1006
+ implementation detail during design-phase reviews ((c) inflation)
1000
1007
 
1001
1008
  **Key insight**: Design reviews and implementation reviews find
1002
- **categorically different bugs**. Both phases are necessary.
1009
+ **categorically different bugs**. Both phases are necessary.
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # Interruption resilience Slice A-2 — detached agent step worker (INV-A1).
5
+ #
6
+ # Spawned by StepDelegation#spawn_worker after agent_step was called with
7
+ # execution: "delegated". This worker is deliberately thin: it bootstraps a
8
+ # ToolRegistry (the same construction the MCP server uses) and re-enters the
9
+ # SAME gated agent_step path with the recorded arguments (the delegation-start
10
+ # anchor is already injected into those arguments). Every correctness property
11
+ # — per-session serialization, anchored at-most-once, side-effect intent
12
+ # bracket — is enforced by the AdvanceGate inside that call (Slice A-1), not by
13
+ # this script. If this worker dies, the driver re-issues the recorded call
14
+ # safely; if it double-runs, the gate serializes and replays.
15
+ #
16
+ # argv: <session_id> <session_dir>
17
+ # env: KAIROS_PROJECT_ROOT (chdir target)
18
+ # KAIROS_SERVER_LIB (lib dir to load kairos_mcp from)
19
+ # KAIROS_DATA_DIR (the server's effective data dir; makes the
20
+ # worker resolve the SAME .kairos)
21
+ #
22
+ # Exit codes: 0 success; 1 exception; 125 setsid failed; 130 signal.
23
+ #
24
+ # NB: bootstrap failures (LoadError/ScriptError from require) are caught too,
25
+ # so the driver always sees a result rather than a silently hung handle.
26
+
27
+ require 'json'
28
+ require 'time'
29
+ require 'fileutils'
30
+
31
+ session_id = ARGV[0] or abort 'usage: agent_step_worker.rb <session_id> <session_dir>'
32
+ session_dir = ARGV[1] or abort 'usage: agent_step_worker.rb <session_id> <session_dir>'
33
+
34
+ # Read the handle identity from the raw file first, so even a bootstrap
35
+ # failure can tag its error result with the delegation it belongs to.
36
+ def read_handle_identity(session_dir)
37
+ raw = JSON.parse(File.read(File.join(session_dir, 'delegation.json')))
38
+ { 'issue_anchor' => raw['issue_anchor'], 'action_key' => raw['action_key'],
39
+ 'step_token' => raw['step_token'] }
40
+ rescue StandardError
41
+ {}
42
+ end
43
+
44
+ def write_raw_result(session_dir, identity, outcome)
45
+ payload = identity.merge('outcome' => outcome)
46
+ tmp = File.join(session_dir, "delegation_result.json.tmp.#{Process.pid}")
47
+ File.write(tmp, JSON.generate(payload))
48
+ File.rename(tmp, File.join(session_dir, 'delegation_result.json'))
49
+ rescue StandardError
50
+ # nothing more we can do
51
+ end
52
+
53
+ boot_identity = read_handle_identity(session_dir)
54
+
55
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
56
+ begin
57
+ require 'agent/step_delegation'
58
+ rescue ScriptError, StandardError => e
59
+ # The delegation lib is co-located with this script; if even it cannot load
60
+ # we tag a raw result with the handle identity so status can surface it as
61
+ # 'ready' (an error outcome) rather than the driver waiting out the grace.
62
+ write_raw_result(session_dir, boot_identity,
63
+ { 'status' => 'error',
64
+ 'error' => "worker bootstrap failed: #{e.class}: #{e.message}" })
65
+ exit 1
66
+ end
67
+
68
+ delegation = KairosMcp::SkillSets::Agent::StepDelegation.new(session_dir)
69
+ my_token = boot_identity['step_token']
70
+
71
+ shutdown = { requested: false }
72
+ %w[TERM INT HUP].each do |sig|
73
+ Signal.trap(sig) { shutdown[:requested] = true }
74
+ end
75
+
76
+ begin
77
+ Process.setsid
78
+ rescue Errno::EPERM
79
+ # Already a session leader — acceptable; continue.
80
+ rescue StandardError => e
81
+ delegation.write_result({ 'status' => 'error',
82
+ 'error' => "worker setsid failed: #{e.message}" },
83
+ identity: boot_identity)
84
+ exit 125
85
+ end
86
+
87
+ heartbeat_thread = Thread.new do
88
+ loop do
89
+ begin
90
+ delegation.touch_heartbeat(my_token)
91
+ rescue StandardError
92
+ # A transient touch failure must not kill the heartbeat thread and
93
+ # make a live worker look crashed; retry on the next tick.
94
+ end
95
+ sleep KairosMcp::SkillSets::Agent::StepDelegation::HEARTBEAT_INTERVAL_SECONDS
96
+ end
97
+ end
98
+
99
+ # Self-timeout watchdog: a hung gated call would otherwise hold the advance
100
+ # lock forever. Exiting the process releases its flock; the driver then sees
101
+ # 'crashed' and re-issues safely. We deliberately write NO result here so the
102
+ # collector takes the crash path — if the gated advance had already committed,
103
+ # crashed_response recovers its outcome from the gate log; an error result
104
+ # would instead mask that committed advance.
105
+ timeout_s = KairosMcp::SkillSets::Agent::StepDelegation.worker_self_timeout_seconds
106
+ watchdog = Thread.new do
107
+ sleep timeout_s
108
+ exit!(124)
109
+ end
110
+
111
+ begin
112
+ Dir.chdir(ENV['KAIROS_PROJECT_ROOT']) if ENV['KAIROS_PROJECT_ROOT'] &&
113
+ Dir.exist?(ENV['KAIROS_PROJECT_ROOT'])
114
+ $LOAD_PATH.unshift(ENV['KAIROS_SERVER_LIB']) if ENV['KAIROS_SERVER_LIB']
115
+ # KAIROS_DATA_DIR was set in the worker env by spawn_worker so ToolRegistry
116
+ # / Session resolve the server's effective .kairos even under --data-dir.
117
+ require 'kairos_mcp/tool_registry'
118
+
119
+ pending = delegation.pending
120
+ raise "no pending delegation in #{session_dir}" unless pending
121
+
122
+ # Supersession guard: run ONLY if the live handle is still ours. If a fresh
123
+ # open_handle replaced it while we were starting up (e.g. we were declared
124
+ # 'crashed' on a stale heartbeat and the driver re-delegated), or we could
125
+ # not read our own identity at boot (my_token nil), do NOT run: the new
126
+ # worker owns this delegation, and the gate would replay/serialize our stale
127
+ # call anyway. Exiting leaves the current handle to its rightful worker.
128
+ if my_token.nil? || pending['step_token'] != my_token
129
+ exit 0
130
+ end
131
+
132
+ # Past the guard, boot_identity is non-empty and is our own handle identity
133
+ # (also driving the per-token heartbeat); write_result tags with it.
134
+ identity = boot_identity
135
+
136
+ args = (pending['arguments'] || {}).merge('session_id' => session_id)
137
+ args.delete('execution') # never recurse into another delegation
138
+
139
+ exit 130 if shutdown[:requested]
140
+
141
+ registry = KairosMcp::ToolRegistry.new
142
+ raw = registry.call_tool('agent_step', args)
143
+
144
+ # Normalize the MCP content shape to the response hash the inline call
145
+ # would have returned.
146
+ text = if raw.is_a?(Array) && raw.first.is_a?(Hash)
147
+ raw.first[:text] || raw.first['text']
148
+ end
149
+ response = begin
150
+ text ? JSON.parse(text) : { 'status' => 'error', 'error' => 'unrecognized tool result shape' }
151
+ rescue JSON::ParserError
152
+ { 'status' => 'error', 'error' => 'unparseable tool result', 'raw' => text.to_s[0, 500] }
153
+ end
154
+
155
+ # Write the result (tagged with our OWN startup identity) and leave the
156
+ # pending handle in place: teardown is the collector's job (agent_wait#collect
157
+ # clears result+handle atomically under delegation.lock), so the worker never
158
+ # races a concurrently-opened fresh delegation by clearing state it may no
159
+ # longer own or by mislabeling its result as a newer delegation's.
160
+ delegation.write_result(response, identity: identity)
161
+ exit 0
162
+ rescue SystemExit, SignalException
163
+ # A deliberate exit (including our own `exit 0`) or a signal is not a
164
+ # failure to report — let it propagate.
165
+ raise
166
+ rescue Exception => e # rubocop:disable Lint/RescueException
167
+ # Catch Exception, not just StandardError: LoadError/ScriptError from the
168
+ # bootstrap require are exactly the failure class the driver must see as a
169
+ # result rather than a silently hung handle. Leave teardown to the collector.
170
+ # Tag with our OWN startup identity (boot_identity is always our handle;
171
+ # the in-block `identity` local may be unassigned if we failed early, and
172
+ # a re-read of pending could belong to a superseding delegation).
173
+ begin
174
+ delegation.write_result({ 'status' => 'error', 'error' => "worker: #{e.class}: #{e.message}" },
175
+ identity: (boot_identity.empty? ? nil : boot_identity))
176
+ rescue StandardError
177
+ # best effort
178
+ end
179
+ exit 1
180
+ ensure
181
+ watchdog&.kill
182
+ heartbeat_thread&.kill
183
+ end