Class: Space::Architect::Harness::ClaudeCodeHarness

Inherits:
Object
  • Object
show all
Defined in:
lib/space_architect/harness.rb

Constant Summary collapse

ALLOWED_TOOLS =
"Read,Edit,Write,Grep,Glob,Bash,WebSearch,WebFetch"
DISALLOWED_TOOLS =
[
  "Bash(git commit:*)", "Bash(git push:*)", "Bash(git reset:*)",
  "Bash(git merge:*)",  "Bash(git rebase:*)", "Bash(git checkout:*)",
  "Bash(git branch:*)"
].join(",")
TIMEOUT_EXIT_CODE =
124
LIVENESS_DELAY_SECONDS =

How long the liveness fiber waits before reading the run log's stream-json init event. Injectable via the run(liveness_delay:) kwarg so tests need not sleep seconds.

5.0

Instance Method Summary collapse

Constructor Details

#initialize(model:, max_turns:, bin: nil, allowed_tools: ALLOWED_TOOLS, disallowed_tools: DISALLOWED_TOOLS) ⇒ ClaudeCodeHarness

Returns a new instance of ClaudeCodeHarness.



48
49
50
51
52
53
54
55
# File 'lib/space_architect/harness.rb', line 48

def initialize(model:, max_turns:, bin: nil,
               allowed_tools: ALLOWED_TOOLS, disallowed_tools: DISALLOWED_TOOLS)
  @model            = model
  @max_turns        = max_turns
  @bin              = bin || ENV.fetch("ARCHITECT_CLAUDE_BIN", "claude")
  @allowed_tools    = allowed_tools
  @disallowed_tools = disallowed_tools
end

Instance Method Details

#run(prompt_path:, run_log_path:, chdir:, push_url: nil, push_token: nil, push_client: nil, timeout: nil, liveness_delay: LIVENESS_DELAY_SECONDS, err: $stderr) ⇒ Object



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
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/space_architect/harness.rb', line 63

def run(prompt_path:, run_log_path:, chdir:, push_url: nil, push_token: nil, push_client: nil, timeout: nil,
        liveness_delay: LIVENESS_DELAY_SECONDS, err: $stderr)
  prompt_path  = Pathname.new(prompt_path)
  run_log_path = Pathname.new(run_log_path)

  File.open(prompt_path, "r") do |prompt_io|
    File.open(run_log_path, "w") do |log|
      r, w = IO.pipe
      Sync do
        child = Async::Process::Child.new(*argv, chdir: chdir.to_s, in: prompt_io, out: w, err: log)
        w.close
        tasks = start_tee(r, log, push_url: push_url, push_token: push_token, push_client: push_client, err: err)
        timed_out    = false
        timeout_task = nil

        # Async::Task#with_timeout cannot do TERM→grace→KILL because
        # Async::Process::Child#wait_thread's ensure goes straight to KILL.
        # Instead: a concurrent fiber fires after the deadline and escalates.
        # transient: true so the reactor doesn't wait for it when main work finishes.
        if timeout && timeout > 0
          timeout_task = Async(transient: true) do
            sleep timeout
            timed_out = true
            Process.kill("TERM", -child.pid) rescue nil
            sleep 0.5
            Process.kill("KILL", -child.pid) rescue nil
          end
        end

        # Liveness self-check: after a bounded delay, read the run log's stream-json
        # init event and print ONE line naming the streamed model + confirming growth.
        # transient: true so it never keeps the reactor alive; best-effort so it never
        # raises into the run path. run_detached gets no such fiber.
        liveness_task = nil
        if liveness_delay && liveness_delay > 0
          liveness_task = Async(transient: true) do
            sleep liveness_delay
            emit_liveness(run_log_path, liveness_delay, err)
          end
        end

        status = child.wait
        timeout_task&.stop
        liveness_task&.stop

        tasks.each(&:wait)
        timed_out ? TIMEOUT_EXIT_CODE : status.exitstatus
      end
    end
  end
end

#run_detached(prompt_path:, run_log_path:, chdir:) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/space_architect/harness.rb', line 115

def run_detached(prompt_path:, run_log_path:, chdir:)
  prompt_path  = Pathname.new(prompt_path)
  run_log_path = Pathname.new(run_log_path)

  prompt_io = File.open(prompt_path, "r")
  log       = File.open(run_log_path, "w")
  begin
    pid = Process.spawn(*argv, chdir: chdir.to_s, pgroup: true,
                        in: prompt_io, out: log, err: log)
    Process.detach(pid)
  ensure
    prompt_io.close
    log.close
  end
  pid
end