diffbroker 0.16.0 → 0.17.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: 19527d7bf5bd6af3f94eec7673a0b60ebedc3080adc1308ba23e477b874af8f1
4
- data.tar.gz: 1a29bde8db351846148a7832d57f6cd2736e3d88941267f21a2699a05a982131
3
+ metadata.gz: 0301a413f14ac1a2d735cc4e7fd952d1d896117d1ef3e3e013508c38d4d1226c
4
+ data.tar.gz: f4bb42f5d0f1aff5bf1301143f34664d34b98e57c601999b2d8e5a07cf74a745
5
5
  SHA512:
6
- metadata.gz: cbf4077b51bd7a4dca8cb9ef8eafc53afe3ce0c2068d954f565e7b9d76cb524350b0c4254c417456e4dc0d8dcd0554ae895767cfcce46b9f4f4abe31a5d5222a
7
- data.tar.gz: df450cc45d5d36007c9f629e1c6b296425c49510240e652e89593fb0c02181d4f0547b27d423bfec70a7c381b80ba4c56551fea8523eaa28447669e5d392026c
6
+ metadata.gz: d38cc871bedc4af0fed78b15ae74a31c4bf4c7ba4e53117596db9ef3927d7aa709c0c1216db8c648ab24cae3a07253acfef6b1ca30978f5b61c1711d47eeea7e
7
+ data.tar.gz: 712a68a04f52259ddf152f714eb15c1655f66721306a7bc91c18e7e7ff5569028dc771ecbb91b61f076242333aae073dc2500fa7d3c16a2deb5aaf86dee1aa5c
@@ -3,7 +3,7 @@
3
3
  module Diffbroker
4
4
  module Agents
5
5
  # Claude Code. Autonomous: `claude -p … --output-format stream-json --verbose` (spec §4.2 step 5); the programmatic
6
- # credit pool pays for it (spec §2). Terminal: interactive `claude`.
6
+ # credit pool pays for it (spec §2).
7
7
  class Claude < Base
8
8
  PLUGIN_DIR = File.join(Diffbroker::ROOT, "plugin")
9
9
  # A bare `Read` in --allowedTools means every file on the machine, so a prompt-injected agent could read
@@ -43,10 +43,6 @@ module Diffbroker
43
43
  # The permissions above are passed again: a resumed session keeps its conversation, not its sandbox.
44
44
  def resume(task) = task["resume_session_id"] ? [ "--resume", task["resume_session_id"] ] : []
45
45
 
46
- def terminal_command(task, mcp_config:)
47
- [ @config.claude_command, "--mcp-config", mcp_config, *strict(task), "--plugin-dir", @plugin_dir ]
48
- end
49
-
50
46
  def parse_stream(line)
51
47
  event = parse_json(line) or return nil
52
48
 
@@ -49,8 +49,6 @@ module Diffbroker
49
49
  [ @config.codex_command, "exec", "--json", *args, *mcp_overrides(mcp_config), task.fetch("prompt") ]
50
50
  end
51
51
 
52
- def terminal_command(_task, mcp_config:) = [ @config.codex_command, *mcp_overrides(mcp_config) ]
53
-
54
52
  def parse_stream(line)
55
53
  event = parse_json(line) or return nil
56
54
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diffbroker
4
- # The user's own coding agents, run headless (autonomous) or interactively (terminal). Spec §4.2 steps 5–7.
4
+ # The user's own coding agents, run headless. Spec §4.2 steps 5–7.
5
5
  module Agents
6
6
  Progress = Data.define(:step, :session_id)
7
7
  # billing: "subscription" or "api" when the agent's login says how the run was paid, else nil.
@@ -31,7 +31,7 @@ module Diffbroker
31
31
  uninstall-hooks Remove those hooks
32
32
  help Show this help
33
33
 
34
- Config: ~/.config/diffbroker/config.yml (server, roots, scan_depth, terminal, worktree_root, agent,
34
+ Config: ~/.config/diffbroker/config.yml (server, roots, scan_depth, worktree_root, agent,
35
35
  claude_args, codex_args). Your agent runs as you, with your own login and credentials.
36
36
  Agent activity is visible only to you; only status is sent, never prompts, file contents or tool input.
37
37
  TEXT
@@ -52,8 +52,6 @@ module Diffbroker
52
52
 
53
53
  def run
54
54
  command = @argv.shift || "help"
55
- return task_ended if command == "task-ended"
56
- return publish_branch if command == "publish-branch"
57
55
  return hook if command == "hook"
58
56
 
59
57
  case command
@@ -136,7 +134,7 @@ module Diffbroker
136
134
  def run_daemon
137
135
  raise Error, "This machine is not paired. Run `diffbroker start` in a terminal to pair it." unless config.paired?
138
136
 
139
- @out.puts "Diff Broker companion #{VERSION} → #{config.server} (agent: #{config.agent}, terminal: #{config.terminal})"
137
+ @out.puts "Diff Broker companion #{VERSION} → #{config.server} (agent: #{config.agent})"
140
138
  heal_service_definition
141
139
  Daemon.build(config, out: @out).run
142
140
  rescue Daemon::Rejected
@@ -297,7 +295,6 @@ module Diffbroker
297
295
  @out.puts "Server: #{config.server}"
298
296
  @out.puts "Paired: #{config.paired? ? "yes (device #{config.device_id})" : "no (run `diffbroker start`)"}"
299
297
  @out.puts "Agent: #{config.agent}"
300
- @out.puts "Terminal: #{config.terminal}"
301
298
  daemon_status
302
299
  @out.puts "Hooks: #{hooks_installer.installed? ? "installed" : "not installed (diffbroker install-hooks)"}"
303
300
  @out.puts "Roots: #{config.roots.join(", ")} (plus the projects Claude Code and Codex know)"
@@ -386,37 +383,5 @@ module Diffbroker
386
383
  @out.puts "No Claude Code settings file; nothing to remove."
387
384
  end
388
385
  end
389
-
390
- # Called by the terminal run script after an update_branch agent exited successfully (not listed in the help: the
391
- # companion's own script is the only caller): pushes the finished rebase (BranchPublisher). Exit 1 when it could not.
392
- def publish_branch
393
- options = {}
394
- OptionParser.new do |parser|
395
- parser.on("--dir DIR") { options[:dir] = _1 }
396
- parser.on("--head-ref REF") { options[:head_ref] = _1 }
397
- parser.on("--expected SHA") { options[:expected_sha] = _1 }
398
- parser.on("--base REF") { options[:base_ref] = _1 }
399
- parser.on("--remote NAME") { options[:remote] = _1 }
400
- end.parse!(@argv)
401
- result = BranchPublisher.new(**options).publish!
402
- @out.puts result.message
403
- result.failed? ? 1 : 0
404
- rescue ArgumentError => e
405
- @err.puts "diffbroker: #{e.message}"
406
- 1
407
- end
408
-
409
- # Called by the terminal run script after the agent exits: silent, always exit 0.
410
- def task_ended
411
- id = @env["DIFFBROKER_TASK_ID"].to_s
412
- token = @env["DIFFBROKER_TASK_TOKEN"].to_s
413
- return 0 if id.empty? || token.empty?
414
-
415
- Api.new(server: @env["DIFFBROKER_SERVER"] || config.server, token:)
416
- .post("/agent_tasks/#{id}/events", { hook_event_name: "TaskEnded", exit_status: @argv.first.to_s })
417
- 0
418
- rescue StandardError # never break the user's terminal session over a status report
419
- 0
420
- end
421
386
  end
422
387
  end
@@ -4,10 +4,9 @@ require "yaml"
4
4
  require "fileutils"
5
5
 
6
6
  module Diffbroker
7
- # ~/.config/diffbroker/config.yml (settings) and credentials.yml (0600, device token). Spec §4.1: roots, terminal,
7
+ # ~/.config/diffbroker/config.yml (settings) and credentials.yml (0600, device token). Spec §4.1: roots,
8
8
  # worktree location, preferred agent, and the agents' command-line args for autonomous runs (spec §4.2 step 5).
9
9
  class Config
10
- TERMINALS = %w[terminal iterm2 ghostty warp tmux].freeze
11
10
  AGENTS = %w[claude codex].freeze
12
11
  DEFAULT_SERVER = "https://diff.broker"
13
12
  DEFAULT_CLAUDE_ARGS = %w[--permission-mode auto --permission-prompts none].freeze
@@ -71,7 +70,6 @@ module Diffbroker
71
70
  ignored: ignored_roots, hints: AgentProjects.paths(home:), home:)
72
71
  end
73
72
  def scan_depth = Integer(data["scan_depth"] || 3)
74
- def terminal = (data["terminal"] || (@platform == "macos" ? "terminal" : "tmux")).to_s
75
73
  def worktree_root = File.expand_path(data["worktree_root"] || File.join(home, ".local/share/diffbroker/worktrees"))
76
74
  def state_dir = File.expand_path(data["state_dir"] || "~/.local/state/diffbroker")
77
75
  def agent = (data["agent"] || "claude").to_s
@@ -112,7 +110,6 @@ module Diffbroker
112
110
  end
113
111
 
114
112
  def validate!
115
- raise Error, "Unknown terminal #{terminal.inspect}. Use one of: #{TERMINALS.join(", ")}" unless TERMINALS.include?(terminal)
116
113
  raise Error, "Unknown agent #{agent.inspect}. Use one of: #{AGENTS.join(", ")}" unless AGENTS.include?(agent)
117
114
 
118
115
  { "claude_args" => claude_args, "codex_args" => codex_args }.each do |key, args|
@@ -8,21 +8,21 @@ module Diffbroker
8
8
  # process environment. Spec §4.2 step 4: the Diff Broker MCP config lives outside the repository.
9
9
  class TaskFiles
10
10
  TOKEN_ENV = "DIFFBROKER_TASK_TOKEN"
11
- HOOK_EVENTS = %w[SessionStart Stop SessionEnd].freeze
12
- SETTINGS_PATH = File.join(".claude", "settings.local.json")
11
+ TASK_ID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
13
12
 
14
- def initialize(task_dir:, worktree:, server:, task_id:, runner: Runner.new)
13
+ # The private directory of one task under the state dir (the MCP config lives there, outside the repository).
14
+ def self.task_dir(state_dir, task_id)
15
+ raise Error, "Invalid task id #{task_id.inspect}" unless task_id.to_s.match?(TASK_ID)
16
+
17
+ File.join(state_dir, "tasks", task_id)
18
+ end
19
+
20
+ def initialize(task_dir:, server:)
15
21
  @task_dir = task_dir
16
- @worktree = worktree
17
22
  @server = server.to_s.chomp("/")
18
- raise Error, "Invalid task id #{task_id.inspect}" unless task_id.to_s.match?(RunScript::TASK_ID)
19
-
20
- @task_id = task_id
21
- @runner = runner
22
23
  end
23
24
 
24
25
  def mcp_url = "#{@server}/mcp/messages"
25
- def events_url = "#{@server}/agent_tasks/#{@task_id}/events"
26
26
 
27
27
  def mcp_config!
28
28
  FileUtils.mkdir_p(@task_dir, mode: 0o700)
@@ -35,56 +35,6 @@ module Diffbroker
35
35
  path
36
36
  end
37
37
 
38
- # Terminal mode with Claude Code (spec §4.2 step 6): HTTP hooks report start, stop and end.
39
- def claude_hooks!
40
- if @runner.git_success?(@worktree, "ls-files", "--error-unmatch", SETTINGS_PATH)
41
- raise Error, "The repository tracks #{SETTINGS_PATH}; Diff Broker will not edit tracked files."
42
- end
43
-
44
- path = File.join(@worktree, SETTINGS_PATH)
45
- if [ File.dirname(path), path ].any? { File.symlink?(_1) }
46
- raise Error, "#{SETTINGS_PATH} in #{@worktree} is a symlink; Diff Broker will not write through it."
47
- end
48
-
49
- FileUtils.mkdir_p(File.dirname(path))
50
- data = read_json(path)
51
- existing = data["hooks"] || {}
52
- raise Error, "#{path} has a \"hooks\" value that is not an object; fix it, then start the task again" unless existing.is_a?(Hash)
53
-
54
- data["hooks"] = existing.merge(hooks) { |_event, theirs, ours| Array(theirs).reject { ours?(_1) } + ours }
55
- File.write(path, "#{JSON.pretty_generate(data)}\n")
56
- exclude_settings
57
- end
58
-
59
38
  def cleanup! = FileUtils.rm_rf(@task_dir)
60
-
61
- private
62
-
63
- def hooks
64
- hook = { "type" => "http", "url" => events_url, "headers" => { "Authorization" => "Bearer ${#{TOKEN_ENV}}" },
65
- "allowedEnvVars" => [ TOKEN_ENV ], "timeout" => 10 }
66
- HOOK_EVENTS.to_h { [ _1, [ { "hooks" => [ hook ] } ] ] }
67
- end
68
-
69
- def ours?(group)
70
- group.is_a?(Hash) && Array(group["hooks"]).any? { _1.is_a?(Hash) && _1["url"].to_s.start_with?("#{@server}/agent_tasks/") }
71
- end
72
-
73
- def exclude_settings
74
- exclude = File.expand_path(@runner.git(@worktree, "rev-parse", "--git-path", "info/exclude").strip, @worktree)
75
- FileUtils.mkdir_p(File.dirname(exclude))
76
- content = File.exist?(exclude) ? File.read(exclude) : ""
77
- entry = "/#{SETTINGS_PATH}"
78
- return if content.lines(chomp: true).include?(entry)
79
-
80
- File.write(exclude, "#{content}#{"\n" unless content.empty? || content.end_with?("\n")}#{entry}\n")
81
- end
82
-
83
- def read_json(path)
84
- data = File.exist?(path) ? JSON.parse(File.read(path)) : {}
85
- data.is_a?(Hash) ? data : raise(JSON::ParserError)
86
- rescue JSON::ParserError
87
- raise Error, "#{path} is not valid JSON; fix or remove it, then start the task again"
88
- end
89
39
  end
90
40
  end
@@ -4,7 +4,8 @@ require "fileutils"
4
4
 
5
5
  module Diffbroker
6
6
  # Runs one agent task (spec §4.2): worktree → neutralize (untrusted only) → gh or MCP prompt → MCP config →
7
- # autonomous agent process or a terminal → progress and the final `finished` report.
7
+ # autonomous agent process → progress and the final `finished` report. An untrusted head runs like any other, with
8
+ # the PR's own agent configuration set aside (Neutralizer).
8
9
  class TaskRunner
9
10
  CANCEL_GRACE = 10
10
11
  PROGRESS_INTERVAL = 3
@@ -15,19 +16,16 @@ module Diffbroker
15
16
  Outcome = Struct.new(:status, :cost_usd, :error_code, :last_output, :no_result, :billing)
16
17
 
17
18
  def initialize(config:, report:, worktree: Worktree.new(root: config.worktree_root), neutralizer: Neutralizer.new,
18
- gh_check: GhCheck.new, launcher: Launcher.new(name: config.terminal), out: $stdout, grace: CANCEL_GRACE,
19
- progress_interval: PROGRESS_INTERVAL, run_script: RunScript.new(state_dir: config.state_dir),
20
- reaper: Reaper.new(root: config.worktree_root), sessions: SessionLedger.new(state_dir: config.state_dir))
19
+ gh_check: GhCheck.new, out: $stdout, grace: CANCEL_GRACE,
20
+ progress_interval: PROGRESS_INTERVAL, reaper: Reaper.new(root: config.worktree_root), sessions: SessionLedger.new(state_dir: config.state_dir))
21
21
  @config = config
22
22
  @report = report
23
23
  @worktree = worktree
24
24
  @neutralizer = neutralizer
25
25
  @gh_check = gh_check
26
- @launcher = launcher
27
26
  @out = out
28
27
  @grace = grace
29
28
  @progress_interval = progress_interval
30
- @run_script = run_script
31
29
  @reaper = reaper
32
30
  @sessions = sessions
33
31
  @handles = {}
@@ -42,19 +40,6 @@ module Diffbroker
42
40
  unless checkout
43
41
  return finished(id, "failed", 0, "no_checkout", "No local checkout of #{task["repo"]} under #{@config.roots.join(", ")}")
44
42
  end
45
- # The server never offers this (spec §5.1); refuse anyway rather than run a stranger's code, or publish what an
46
- # injected agent read, unattended.
47
- if task["untrusted"] && (task["code_changing"] || task["terminal_only"]) && task["mode"] != "terminal"
48
- return finished(id, "failed", 0, "untrusted_head", "This task runs only in a terminal on an untrusted head")
49
- end
50
-
51
- # Codex has no --strict-mcp-config equivalent: a headless run would load the user's own MCP servers next to a
52
- # prompt-injected PR. Untrusted heads run Codex only in a terminal, where the user watches and approves.
53
- if task["untrusted"] && task["mode"] != "terminal" && @config.agent == "codex"
54
- return finished(id, "failed", 0, "untrusted_head",
55
- "Codex does not run untrusted heads unattended (it would load your own MCP servers). Use Open in terminal, or set agent: claude.")
56
- end
57
-
58
43
  repo_path = checkout["path"]
59
44
  dir = @worktree.prepare(repo_path:, full_name: task["repo"], number: task["pr_number"], head_sha: task["head_sha"],
60
45
  kind: task["kind"], head_ref: task["head_ref"], branch: task["code_changing"] == true,
@@ -64,26 +49,23 @@ module Diffbroker
64
49
  restored = task["untrusted"] ? @neutralizer.neutralize!(dir, repo_path:, number: task["pr_number"], base_ref: base_ref(task), remote: GitContext.remote_for(repo_path, task["repo"])) : []
65
50
  task = task.merge("prompt" => prompt_for(task))
66
51
  agent = Agents.for(@config, worktree: dir)
67
- say(id, "#{task["kind"]} on #{task["repo"]}##{task["pr_number"]}: starting (#{agent.name}, #{task["mode"] == "terminal" ? "terminal" : "autonomous"})")
52
+ say(id, "#{task["kind"]} on #{task["repo"]}##{task["pr_number"]}: starting (#{agent.name})")
68
53
  say(id, "Worktree: #{dir}")
69
- files = TaskFiles.new(task_dir: @run_script.task_dir(id), worktree: dir, server: @config.server, task_id: id)
54
+ say(id, "Untrusted head: removed or restored from #{base_ref(task)}: #{restored.join(", ")}") if restored.any?
55
+ files = TaskFiles.new(task_dir: TaskFiles.task_dir(@config.state_dir, id), server: @config.server)
70
56
  mcp_config = files.mcp_config!
71
57
 
72
- if task["mode"] == "terminal"
73
- open_terminal(task, agent, files, mcp_config, dir, restored)
58
+ begin
59
+ run_autonomous(task, agent, mcp_config, dir)
60
+ ensure
61
+ files.cleanup!
62
+ end
63
+ stopped = @reaper.reap!(dir)
64
+ say(id, "Stopped #{stopped} leftover process#{"es" unless stopped == 1} in #{dir}") if stopped.positive?
65
+ if @worktree.remove!(dir, repo_path:)
66
+ say(id, "Removed worktree #{dir}")
74
67
  else
75
- begin
76
- run_autonomous(task, agent, mcp_config, dir)
77
- ensure
78
- files.cleanup!
79
- end
80
- stopped = @reaper.reap!(dir)
81
- say(id, "Stopped #{stopped} leftover process#{"es" unless stopped == 1} in #{dir}") if stopped.positive?
82
- if @worktree.remove!(dir, repo_path:)
83
- say(id, "Removed worktree #{dir}")
84
- else
85
- say(id, "Kept #{dir}: it has uncommitted work.")
86
- end
68
+ say(id, "Kept #{dir}: it has uncommitted work.")
87
69
  end
88
70
  rescue Error, SystemCallError, KeyError => e
89
71
  finished(id, "failed", 0, "agent_failed", e.message)
@@ -126,17 +108,6 @@ module Diffbroker
126
108
  !task["read_only"] && !gh.empty? && @gh_check.matches?(task["github_login"]) ? gh : task.fetch("prompt")
127
109
  end
128
110
 
129
- def open_terminal(task, agent, files, mcp_config, dir, restored)
130
- files.claude_hooks! if agent.name == "claude_code"
131
- lines = [ ("Untrusted head: this PR's own code and tests run on your machine." if task["untrusted"]),
132
- ("Removed or restored from #{base_ref(task)}: #{restored.join(", ")}." if restored.any?) ].compact
133
- script = @run_script.write!(task_id: task["task_id"], worktree: dir, token: task.fetch("token"), server: @config.server,
134
- argv: agent.terminal_command(task, mcp_config:), prompt: task["prompt"],
135
- notice: lines.empty? ? nil : lines.join("\n"), publish: publish_command(task, dir))
136
- @launcher.launch!(script:, cwd: dir, title: "#{task["repo"].split("/").last.downcase}-#{task["pr_number"]}")
137
- progress(task["task_id"], "opened in terminal", agent.name)
138
- end
139
-
140
111
  # An explain or explain_change task gets a verifier pass in a fresh process after a successful write (understanding
141
112
  # spec §6.2, activity spec §5). The server only accepts verify_explanation / verify_change_note once this process
142
113
  # announces itself with progress phase "verifier".
@@ -187,14 +158,6 @@ module Diffbroker
187
158
  Outcome.new("failed", outcome.cost_usd, "agent_failed", result.message, false, outcome.billing)
188
159
  end
189
160
 
190
- # The same push for a terminal run, executed by its script after the agent exits (`diffbroker publish-branch`).
191
- def publish_command(task, dir)
192
- return unless publishes_branch?(task)
193
-
194
- [ RbConfig.ruby, ServiceInstaller.launcher, "publish-branch", "--dir", dir, "--head-ref", task["head_ref"].to_s,
195
- "--expected", task["head_sha"].to_s, "--base", base_ref(task), "--remote", GitContext.remote_for(dir, task["repo"]) ]
196
- end
197
-
198
161
  def publishes_branch?(task) = task["kind"] == "update_branch" && task["cross_repository"] != true && !task["read_only"]
199
162
 
200
163
  def verify_after_write(task, write, agent, mcp_config, dir)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diffbroker
4
- VERSION = "0.16.0"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/diffbroker.rb CHANGED
@@ -29,7 +29,6 @@ module Diffbroker
29
29
  autoload :Hook, File.expand_path("diffbroker/hook", __dir__)
30
30
  autoload :HooksInstaller, File.expand_path("diffbroker/hooks_installer", __dir__)
31
31
  autoload :Lifecycle, File.expand_path("diffbroker/lifecycle", __dir__)
32
- autoload :Launcher, File.expand_path("diffbroker/launcher", __dir__)
33
32
  autoload :Maintenance, File.expand_path("diffbroker/maintenance", __dir__)
34
33
  autoload :LocalRelay, File.expand_path("diffbroker/local_relay", __dir__)
35
34
  autoload :AccessHelp, File.expand_path("diffbroker/access_help", __dir__)
@@ -40,7 +39,6 @@ module Diffbroker
40
39
  autoload :Reaper, File.expand_path("diffbroker/reaper", __dir__)
41
40
  autoload :Reconciler, File.expand_path("diffbroker/reconciler", __dir__)
42
41
  autoload :RepoScanner, File.expand_path("diffbroker/repo_scanner", __dir__)
43
- autoload :RunScript, File.expand_path("diffbroker/run_script", __dir__)
44
42
  autoload :Runner, File.expand_path("diffbroker/runner", __dir__)
45
43
  autoload :ServiceInstaller, File.expand_path("diffbroker/service_installer", __dir__)
46
44
  autoload :SessionLedger, File.expand_path("diffbroker/session_ledger", __dir__)
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "diffbroker",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Skills for working on Diff Broker agent tasks: the diffbroker MCP tools, write rules and report_task."
5
5
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diffbroker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Sidorov (@newstler)
@@ -53,7 +53,6 @@ files:
53
53
  - lib/diffbroker/git_context.rb
54
54
  - lib/diffbroker/hook.rb
55
55
  - lib/diffbroker/hooks_installer.rb
56
- - lib/diffbroker/launcher.rb
57
56
  - lib/diffbroker/lifecycle.rb
58
57
  - lib/diffbroker/local_relay.rb
59
58
  - lib/diffbroker/login.rb
@@ -64,7 +63,6 @@ files:
64
63
  - lib/diffbroker/reaper.rb
65
64
  - lib/diffbroker/reconciler.rb
66
65
  - lib/diffbroker/repo_scanner.rb
67
- - lib/diffbroker/run_script.rb
68
66
  - lib/diffbroker/runner.rb
69
67
  - lib/diffbroker/service_installer.rb
70
68
  - lib/diffbroker/session_ledger.rb
@@ -99,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
97
  - !ruby/object:Gem::Version
100
98
  version: '0'
101
99
  requirements: []
102
- rubygems_version: 4.0.6
100
+ rubygems_version: 4.0.20
103
101
  specification_version: 4
104
102
  summary: 'Diff Broker companion: runs your own Claude Code or Codex on your pull requests,
105
103
  on this machine'
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "shellwords"
4
- require "yaml"
5
- require "fileutils"
6
-
7
- module Diffbroker
8
- # Opens a visible terminal running the task's run script (spec §4.2 step 6). The user sees and drives the agent.
9
- class Launcher
10
- def initialize(name:, runner: Runner.new, platform: Diffbroker.platform,
11
- warp_dir: File.join(Dir.home, ".warp", "launch_configurations"))
12
- raise Error, "Unknown terminal #{name.inspect}. Use one of: #{Config::TERMINALS.join(", ")}" unless Config::TERMINALS.include?(name)
13
-
14
- @name = name
15
- @runner = runner
16
- @platform = platform
17
- @warp_dir = warp_dir
18
- end
19
-
20
- def launch!(script:, cwd:, title:)
21
- commands(script:, cwd:, title:).each { @runner.run!(*_1) }
22
- end
23
-
24
- def commands(script:, cwd:, title:)
25
- title = title.to_s.gsub(/[^\w.#-]/, "-")
26
- line = "/bin/bash #{Shellwords.escape(script)}"
27
-
28
- case @name
29
- when "terminal"
30
- [ [ "osascript", "-e", %(tell application "Terminal" to do script #{applescript(line)}),
31
- "-e", %(tell application "Terminal" to activate) ] ]
32
- when "iterm2"
33
- [ [ "osascript", "-e", %(tell application "iTerm" to create window with default profile command #{applescript(line)}),
34
- "-e", %(tell application "iTerm" to activate) ] ]
35
- when "ghostty"
36
- @platform == "macos" ? [ [ "open", "-na", "Ghostty", "--args", "-e", "/bin/bash", script ] ] : [ [ "ghostty", "-e", "/bin/bash", script ] ]
37
- when "warp"
38
- [ [ @platform == "macos" ? "open" : "xdg-open", "warp://launch/#{warp_config(line:, cwd:, title:)}" ] ]
39
- when "tmux"
40
- if @runner.success?("tmux", "has-session")
41
- [ [ "tmux", "new-window", "-n", title, "-c", cwd, line ] ]
42
- else
43
- [ [ "tmux", "new-session", "-d", "-s", "diffbroker", "-n", title, "-c", cwd, line ] ]
44
- end
45
- end
46
- end
47
-
48
- private
49
-
50
- # An AppleScript string literal: backslashes and double quotes escaped.
51
- def applescript(text) = %("#{text.gsub("\\") { "\\\\" }.gsub('"') { '\\"' }}")
52
-
53
- def warp_config(line:, cwd:, title:)
54
- FileUtils.mkdir_p(@warp_dir)
55
- file = "diffbroker-#{title}.yaml"
56
- config = { "name" => "Diff Broker #{title}",
57
- "windows" => [ { "tabs" => [ { "title" => title, "layout" => { "cwd" => cwd, "commands" => [ { "exec" => line } ] } } ] } ] }
58
- File.write(File.join(@warp_dir, file), config.to_yaml)
59
- file
60
- end
61
- end
62
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "shellwords"
5
- require "rbconfig"
6
-
7
- module Diffbroker
8
- # Terminal mode (spec §4.2 step 6): the script the terminal runs. The task token lives only here (0700, private
9
- # state dir, deleted on start) and in the agent's environment. The prompt travels in an environment variable, so
10
- # quoting never matters. When the agent exits, `diffbroker task-ended` tells Diff Broker.
11
- class RunScript
12
- TASK_ID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
13
-
14
- # RubyGems' stable wrapper (ServiceInstaller.launcher): a terminal task may outlive an upgrade's `gem cleanup`.
15
- def initialize(state_dir:, ruby: RbConfig.ruby, exe: ServiceInstaller.launcher)
16
- @state_dir = state_dir
17
- @ruby = ruby
18
- @exe = exe
19
- end
20
-
21
- def task_dir(task_id)
22
- raise Error, "Invalid task id #{task_id.inspect}" unless task_id.to_s.match?(TASK_ID)
23
-
24
- File.join(@state_dir, "tasks", task_id)
25
- end
26
-
27
- # publish: a command (argv) to run in this script, outside the agent, once the agent exited successfully: the push of
28
- # an update_branch rebase (BranchPublisher), which the agent's own hooks would refuse.
29
- def write!(task_id:, worktree:, token:, server:, argv:, prompt:, notice: nil, publish: nil)
30
- dir = task_dir(task_id)
31
- FileUtils.mkdir_p(dir, mode: 0o700)
32
- File.chmod(0o700, dir)
33
- path = File.join(dir, "run.sh")
34
- FileUtils.rm_f(path)
35
- File.write(path, render(task_id:, worktree:, token:, server:, argv:, prompt:, notice:, publish:), perm: 0o700)
36
- File.chmod(0o700, path)
37
- path
38
- end
39
-
40
- private
41
-
42
- def render(task_id:, worktree:, token:, server:, argv:, prompt:, notice:, publish:)
43
- inner = %(#{Shellwords.join(argv)} "$DIFFBROKER_PROMPT")
44
- publish_step = publish ? "if [ \"$status\" = \"0\" ]; then #{Shellwords.join(publish)}; fi" : nil
45
- <<~SH
46
- #!/bin/bash
47
- # Written by the Diff Broker companion for agent task #{task_id}. Deletes itself on start.
48
- rm -f -- "$0"
49
- #{"printf '%s\\n' #{Shellwords.escape(notice)}" if notice}
50
- export DIFFBROKER_TASK_TOKEN=#{Shellwords.escape(token)}
51
- export DIFFBROKER_TASK_ID=#{Shellwords.escape(task_id)}
52
- export DIFFBROKER_SERVER=#{Shellwords.escape(server)}
53
- export DIFFBROKER_PROMPT=#{Shellwords.escape(prompt)}
54
- cd #{Shellwords.escape(worktree)} || exit 1
55
- "${SHELL:-/bin/bash}" -lic #{Shellwords.escape(inner)}
56
- status=$?
57
- #{publish_step}
58
- #{Shellwords.escape(@ruby)} #{Shellwords.escape(@exe)} task-ended "$status"
59
- SH
60
- end
61
- end
62
- end