Class: KairosMcp::Daemon::CodeGenAct

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/daemon/code_gen_act.rb

Overview

CodeGenAct — code generation pipeline for daemon ACT phase.

Design (P3.2 v0.2 §9):

1. plan_edit: LLM generates {old_string, new_string} via invoker
2. simulate: EditKernel computes pre/post hash without I/O
3. classify: ScopeClassifier determines scope
4. gate: ApprovalGate stages proposal or auto-approves
5. apply: CAS + EditKernel + atomic write (with elevation if L0/L1)
6. record: chain_record for L0/L1

Defined Under Namespace

Classes: LlmContentPolicyViolation, PauseForApproval, PostHashMismatch, PreHashMismatch, ProposalTampered, ScopeDrift

Instance Method Summary collapse

Constructor Details

#initialize(workspace_root:, safety:, invoker:, approval_gate:, chain_recorder: nil, logger: nil) ⇒ CodeGenAct

Returns a new instance of CodeGenAct.

Parameters:

  • workspace_root (String)
  • safety (Object)

    Safety instance with push/pop_policy_override

  • invoker (#call)

    callable: (tool_name, args) → result Hash

  • approval_gate (ApprovalGate)
  • chain_recorder (#call, nil) (defaults to: nil)

    callable: (payload) → tx_id

  • logger (Object, nil) (defaults to: nil)


38
39
40
41
42
43
44
45
46
# File 'lib/kairos_mcp/daemon/code_gen_act.rb', line 38

def initialize(workspace_root:, safety:, invoker:,
               approval_gate:, chain_recorder: nil, logger: nil)
  @ws       = workspace_root
  @safety   = safety
  @invoker  = invoker
  @gate     = approval_gate
  @chain    = chain_recorder
  @logger   = logger
end

Instance Method Details

#resume(proposal_id) ⇒ Hash, :still_pending

Re-entry after pause.

Returns:

  • (Hash, :still_pending)


104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/kairos_mcp/daemon/code_gen_act.rb', line 104

def resume(proposal_id)
  status = @gate.status_of(proposal_id)
  return :rejected if status == :rejected
  return :expired  if status == :expired
  return :not_found if status == :not_found

  grant = @gate.consume_grant(proposal_id)
  return :still_pending if grant.nil?

  # Verify proposal integrity only after approval (decision exists)
  unless @gate.verify_proposal_integrity(proposal_id)
    raise ProposalTampered, "integrity check failed: #{proposal_id}"
  end

  proposal = symbolize_proposal(grant.proposal)
  abs = File.expand_path(proposal[:target][:path], @ws)
  apply_with_grant(proposal, grant, abs)
end

#run(decision, mandate) ⇒ Hash

Run the code-gen pipeline for a single edit.

Parameters:

  • decision (Hash)

    from DECIDE phase: { action:, target:, intent:, … }

  • mandate (Hash)

    current mandate

Returns:

  • (Hash)

    ACT result



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/kairos_mcp/daemon/code_gen_act.rb', line 53

def run(decision, mandate)
  target_path = decision[:target] || decision['target']
  abs = File.expand_path(target_path, @ws)

  # Scope classification
  scope_info = ScopeClassifier.classify(abs, workspace_root: @ws)

  # LLM content policy check
  check_llm_content_policy!(scope_info, mandate)

  # Read file and simulate
  content = File.binread(abs)
  plan = extract_edit_plan(decision)
  result = EditKernel.compute(content,
                              old_string:  plan[:old_string],
                              new_string:  plan[:new_string],
                              replace_all: plan[:replace_all])

  # Build proposal
  proposal_id = "prop_#{SecureRandom.hex(8)}"
  proposal = {
    proposal_id: proposal_id,
    mandate_id:  mandate[:id] || mandate['id'],
    target: {
      path:     target_path,
      pre_hash: result[:pre_hash]
    },
    edit: {
      old_string:         plan[:old_string],
      new_string:         plan[:new_string],
      replace_all:        plan[:replace_all],
      proposed_post_hash: result[:post_hash]
    },
    scope: scope_info
  }

  # Gate
  if scope_info[:auto_approve]
    @gate.auto_approve(proposal)
    grant = @gate.consume_grant(proposal_id)
    apply_with_grant(proposal, grant, abs)
  else
    @gate.stage(proposal)
    grant = @gate.consume_grant(proposal_id)
    raise PauseForApproval, proposal_id if grant.nil?
    apply_with_grant(proposal, grant, abs)
  end
end