├── log └── .keep ├── script └── .keep ├── storage └── .keep ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep ├── vendor ├── .keep └── javascript │ └── .keep ├── .ruby-version ├── CLAUDE.md ├── lib └── tasks │ └── .keep ├── test ├── helpers │ └── .keep ├── mailers │ ├── .keep │ └── previews │ │ └── passwords_mailer_preview.rb ├── models │ ├── .keep │ ├── volume_test.rb │ ├── env_variable_test.rb │ ├── agent_specific_setting_test.rb │ ├── log_processor_test.rb │ ├── step │ │ └── error_test.rb │ ├── repo_state_test.rb │ ├── user_test.rb │ ├── log_processor │ │ └── text_test.rb │ ├── volume_mount_test.rb │ └── secret_test.rb ├── system │ └── .keep ├── controllers │ ├── .keep │ ├── dashboard_controller_test.rb │ ├── user_settings_controller_test.rb │ └── claude_oauth_controller_test.rb ├── integration │ └── .keep ├── fixtures │ ├── files │ │ └── .keep │ ├── secrets.yml │ ├── agent_specific_settings.yml │ ├── steps.yml │ ├── env_variables.yml │ ├── volumes.yml │ ├── users.yml │ ├── volume_mounts.yml │ ├── repo_states.yml │ ├── projects.yml │ └── runs.yml ├── application_system_test_case.rb ├── jobs │ └── run_job_test.rb ├── support │ └── docker_test_helper.rb └── test_helper.rb ├── worktrees └── .keep ├── app ├── assets │ ├── images │ │ ├── .keep │ │ └── logo.png │ └── stylesheets │ │ ├── step-raw.css │ │ ├── run-filter.css │ │ ├── step-error.css │ │ ├── glob-tool.css │ │ ├── grep-tool.css │ │ ├── read-tool.css │ │ ├── oauth-config.css │ │ ├── form-help.css │ │ ├── agent-actions.css │ │ ├── status-indicator.css │ │ ├── task-actions.css │ │ ├── task-form.css │ │ ├── project-actions.css │ │ ├── copy-button.css │ │ ├── step-init.css │ │ ├── instructions-preview.css │ │ ├── run-item.css │ │ ├── tool-call.css │ │ ├── prompt-truncate.css │ │ ├── run-form.css │ │ ├── action-button.css │ │ ├── system-step.css │ │ ├── application.css │ │ ├── branch-indicator.css │ │ ├── tool-result.css │ │ ├── write-tool.css │ │ ├── step-result.css │ │ ├── bash-tool.css │ │ ├── nav-pull-tab.css │ │ ├── docker-controls.css │ │ ├── run-spinner.css │ │ ├── panel-divider.css │ │ ├── chat-panel.css │ │ ├── auto-push.css │ │ ├── oauth-login.css │ │ ├── codex-layout.css │ │ ├── diff-controls.css │ │ ├── scroll-to-top.css │ │ ├── ls-tool.css │ │ ├── websearch-tool.css │ │ ├── header-overlay.css │ │ ├── form-errors.css │ │ ├── task-show-overlay.css │ │ ├── inline-edit.css │ │ ├── webfetch-tool.css │ │ ├── task-show.css │ │ └── flash-alert.css ├── models │ ├── concerns │ │ ├── .keep │ │ ├── line_number_formatting.rb │ │ └── docker_stream_processor.rb │ ├── step │ │ ├── error.rb │ │ ├── init.rb │ │ ├── system.rb │ │ ├── text.rb │ │ ├── thinking.rb │ │ ├── glob_tool.rb │ │ ├── grep_tool.rb │ │ ├── ls_tool.rb │ │ ├── bash_tool.rb │ │ ├── web_search_tool.rb │ │ ├── result.rb │ │ ├── read_tool.rb │ │ ├── web_fetch_tool.rb │ │ ├── task_tool.rb │ │ ├── write_tool.rb │ │ ├── multi_edit_tool.rb │ │ ├── edit_tool.rb │ │ ├── tool_result.rb │ │ ├── todo_write.rb │ │ └── tool_call.rb │ ├── session.rb │ ├── repo_state.rb │ ├── application_record.rb │ ├── current.rb │ ├── env_variable.rb │ ├── log_processor │ │ ├── text.rb │ │ └── claude_json.rb │ ├── volume.rb │ ├── secret.rb │ ├── user.rb │ ├── claude_oauth_setting.rb │ ├── volume_mount.rb │ ├── log_processor.rb │ ├── agent_specific_setting.rb │ ├── project.rb │ └── step.rb ├── controllers │ ├── concerns │ │ ├── .keep │ │ └── authentication.rb │ ├── admin_controller.rb │ ├── application_controller.rb │ ├── dashboard_controller.rb │ ├── sessions_controller.rb │ ├── account_settings_controller.rb │ ├── runs_controller.rb │ ├── user_settings_controller.rb │ ├── passwords_controller.rb │ ├── containers_controller.rb │ └── projects_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ └── mailer.html.erb │ ├── steps │ │ └── _step.html.erb │ ├── step │ │ ├── errors │ │ │ └── _error.html.erb │ │ ├── texts │ │ │ └── _text.html.erb │ │ ├── inits │ │ │ └── _init.html.erb │ │ ├── systems │ │ │ └── _system.html.erb │ │ ├── tool_results │ │ │ └── _tool_result.html.erb │ │ ├── bash_tools │ │ │ └── _bash_tool.html.erb │ │ ├── thinkings │ │ │ └── _thinking.html.erb │ │ ├── tool_calls │ │ │ ├── _tool_call.html.erb │ │ │ └── _tool_result_section.html.erb │ │ ├── edit_tools │ │ │ └── _edit_tool_result.html.erb │ │ ├── ls_tools │ │ │ └── _ls_tool.html.erb │ │ ├── write_tools │ │ │ ├── _write_tool_result.html.erb │ │ │ └── _write_tool.html.erb │ │ ├── multi_edit_tools │ │ │ └── _multi_edit_tool_result.html.erb │ │ ├── read_tools │ │ │ ├── _read_tool.html.erb │ │ │ └── _read_tool_result.html.erb │ │ ├── web_search_tools │ │ │ └── _web_search_tool.html.erb │ │ ├── web_fetch_tools │ │ │ └── _web_fetch_tool.html.erb │ │ ├── task_tools │ │ │ └── _task_tool.html.erb │ │ ├── results │ │ │ └── _result.html.erb │ │ ├── glob_tools │ │ │ └── _glob_tool.html.erb │ │ └── grep_tools │ │ │ └── _grep_tool.html.erb │ ├── application │ │ ├── _flash_messages.html.erb │ │ ├── _form_errors.html.erb │ │ └── _nav.html.erb │ ├── agents │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ └── index.html.erb │ ├── dashboard │ │ └── index.html.erb │ ├── passwords_mailer │ │ ├── reset.text.erb │ │ └── reset.html.erb │ ├── tasks │ │ ├── _runs_list.html.erb │ │ ├── _chat_panel.html.erb │ │ ├── index.html.erb │ │ ├── _chat_messages.html.erb │ │ ├── edit.html.erb │ │ ├── _task_list.html.erb │ │ ├── _description.html.erb │ │ ├── _run.html.erb │ │ ├── _run_form.html.erb │ │ ├── _header_content.html.erb │ │ ├── _auto_push_form.html.erb │ │ ├── new.html.erb │ │ └── _actions_header.html.erb │ ├── runs │ │ ├── create.turbo_stream.erb │ │ ├── _chat_item.html.erb │ │ └── _diff_panel.html.erb │ ├── projects │ │ ├── index.html.erb │ │ └── show.html.erb │ ├── pwa │ │ ├── manifest.json.erb │ │ └── service-worker.js │ ├── passwords │ │ ├── new.html.erb │ │ └── edit.html.erb │ ├── account_settings │ │ └── edit.html.erb │ ├── sessions │ │ └── new.html.erb │ ├── claude_oauth │ │ └── login_start.html.erb │ ├── user_settings │ │ └── show.html.erb │ └── shared │ │ └── _json_array_fields.html.erb ├── helpers │ ├── projects_helper.rb │ ├── claude_oauth_helper.rb │ └── agents_helper.rb ├── mailers │ ├── application_mailer.rb │ └── passwords_mailer.rb ├── jobs │ ├── run_job.rb │ ├── application_job.rb │ └── remove_docker_container_job.rb ├── tools │ ├── application_tool.rb │ └── rename_task_tool.rb ├── javascript │ ├── application.js │ └── controllers │ │ ├── hello_controller.js │ │ ├── application.js │ │ ├── index.js │ │ ├── display_toggle_controller.js │ │ ├── alert_controller.js │ │ ├── tabs_controller.js │ │ ├── nested_fields_controller.js │ │ ├── json_array_fields_controller.js │ │ ├── prompt_controller.js │ │ ├── prompt_truncate_controller.js │ │ ├── copy_controller.js │ │ └── auto_scroll_controller.js ├── resources │ └── application_resource.rb └── channels │ └── application_cable │ └── connection.rb ├── deploy └── .gitignore ├── public ├── icon.png └── robots.txt ├── .kamal ├── hooks │ ├── docker-setup.sample │ ├── post-proxy-reboot.sample │ ├── pre-proxy-reboot.sample │ ├── post-app-boot.sample │ ├── pre-app-boot.sample │ ├── post-deploy.sample │ ├── pre-connect.sample │ └── pre-build.sample └── secrets ├── bin ├── dev ├── rake ├── importmap ├── thrust ├── jobs ├── start ├── rails ├── brakeman ├── checks ├── rubocop ├── docker-entrypoint ├── kamal ├── lint_staged └── setup ├── config ├── initializers │ ├── mission_control_jobs.rb │ ├── docker.rb │ ├── test_encryption.rb │ ├── assets.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ └── content_security_policy.rb ├── environment.rb ├── boot.rb ├── importmap.rb ├── recurring.yml ├── queue.yml ├── cache.yml ├── cable.yml ├── locales │ └── en.yml ├── storage.yml ├── application.rb ├── database.yml └── routes.rb ├── db ├── migrate │ ├── 20250525021550_add_role_to_users.rb │ ├── 20250531043829_add_index_to_steps_type.rb │ ├── 20250602024248_add_ssh_key_to_users.rb │ ├── 20250530022252_remove_output_from_runs.rb │ ├── 20250602024607_add_git_config_to_users.rb │ ├── 20250602030052_add_home_path_to_agents.rb │ ├── 20250529063027_add_docker_host_to_agents.rb │ ├── 20250601034218_add_repo_path_to_projects.rb │ ├── 20250601071554_add_github_token_to_users.rb │ ├── 20250618004105_add_description_to_tasks.rb │ ├── 20250624013344_add_container_id_to_runs.rb │ ├── 20250601071738_add_user_to_tasks.rb │ ├── 20250621070652_add_target_branch_to_tasks.rb │ ├── 20250622234155_add_git_diff_to_repo_states.rb │ ├── 20250531203043_add_workplace_path_to_agents.rb │ ├── 20250602025218_add_ssh_mount_path_to_agents.rb │ ├── 20250608080712_add_mcp_sse_endpoint_to_agents.rb │ ├── 20250622044834_remove_docker_host_from_agents.rb │ ├── 20250624023931_add_parent_tool_use_id_to_steps.rb │ ├── 20250531210210_add_volume_name_to_volume_mounts.rb │ ├── 20250601100859_add_user_id_to_agents.rb │ ├── 20250625002158_remove_env_variables_from_agents.rb │ ├── 20250530025441_add_log_processor_to_agents.rb │ ├── 20250621025720_add_shrimp_mode_to_users.rb │ ├── 20250531210727_add_environment_variables_to_agents.rb │ ├── 20250601005216_add_discarded_at_to_agents.rb │ ├── 20250601005217_add_discarded_at_to_tasks.rb │ ├── 20250531033829_add_step_type_and_content_to_steps.rb │ ├── 20250531210350_change_volume_id_to_nullable_in_volume_mounts.rb │ ├── 20250601005215_add_discarded_at_to_projects.rb │ ├── 20250614232508_add_external_to_volumes.rb │ ├── 20250618000000_add_allow_github_token_access_to_users.rb │ ├── 20250601212625_add_user_instructions_feature.rb │ ├── 20250531020138_remove_prompt_and_setup_from_agents.rb │ ├── 20250531221419_rename_environment_variables_to_env_variables_on_agents.rb │ ├── 20250609021050_add_auto_push_to_tasks.rb │ ├── 20250530014329_create_steps.rb │ ├── 20250528010412_create_volumes.rb │ ├── 20250524221111_create_projects.rb │ ├── 20250524223752_create_sessions.rb │ ├── 20250528010426_create_volume_mounts.rb │ ├── 20250624044111_add_auto_task_naming_agent_to_users.rb │ ├── 20250524223751_create_users.rb │ ├── 20250601082414_create_repo_states.rb │ ├── 20250606060235_add_tool_grouping_to_steps.rb │ ├── 20250524221353_create_agents.rb │ ├── 20250602034140_create_project_secrets.rb │ ├── 20250525021419_create_tasks.rb │ ├── 20250525021302_create_runs.rb │ ├── 20250615021535_create_agent_specific_settings.rb │ ├── 20250617014540_add_cost_tracking_to_steps.rb │ ├── 20250625001629_create_env_variables.rb │ └── 20250621075100_add_docker_container_support.rb ├── cable_schema.rb └── cache_schema.rb ├── devenv.yaml ├── config.ru ├── .claude ├── bin │ ├── log │ ├── lint │ └── pre-push └── settings.json ├── .envrc ├── Rakefile ├── .github ├── dependabot.yml └── workflows │ └── deploy.yml ├── .rubocop.yml ├── .gitattributes ├── devenv.nix ├── .env.development ├── .gitignore ├── .dockerignore └── agents └── css.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.4 -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | AGENTS.md -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /worktrees/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /deploy/.gitignore: -------------------------------------------------------------------------------- 1 | secrets.env 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/models/step/error.rb: -------------------------------------------------------------------------------- 1 | class Step::Error < Step 2 | end 3 | -------------------------------------------------------------------------------- /app/models/step/init.rb: -------------------------------------------------------------------------------- 1 | class Step::Init < Step 2 | end 3 | -------------------------------------------------------------------------------- /app/models/step/system.rb: -------------------------------------------------------------------------------- 1 | class Step::System < Step 2 | end 3 | -------------------------------------------------------------------------------- /app/models/step/text.rb: -------------------------------------------------------------------------------- 1 | class Step::Text < Step 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/projects_helper.rb: -------------------------------------------------------------------------------- 1 | module ProjectsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/models/step/thinking.rb: -------------------------------------------------------------------------------- 1 | class Step::Thinking < Step 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/step-raw.css: -------------------------------------------------------------------------------- 1 | .step-raw { 2 | margin: 5px 0; 3 | } -------------------------------------------------------------------------------- /app/helpers/claude_oauth_helper.rb: -------------------------------------------------------------------------------- 1 | module ClaudeOauthHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/models/step/glob_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::GlobTool < Step::ToolCall 2 | end 3 | -------------------------------------------------------------------------------- /app/models/step/grep_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::GrepTool < Step::ToolCall 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/run-filter.css: -------------------------------------------------------------------------------- 1 | .run-filter { 2 | margin-bottom: 15px; 3 | } -------------------------------------------------------------------------------- /app/views/steps/_step.html.erb: -------------------------------------------------------------------------------- 1 |
<%= step.raw_response %>
-------------------------------------------------------------------------------- /app/views/step/errors/_error.html.erb: -------------------------------------------------------------------------------- 1 |
<%= error.content %>
-------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeDupuis/summoncircle/HEAD/public/icon.png -------------------------------------------------------------------------------- /app/models/session.rb: -------------------------------------------------------------------------------- 1 | class Session < ApplicationRecord 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /.kamal/hooks/docker-setup.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Docker set up on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /app/models/repo_state.rb: -------------------------------------------------------------------------------- 1 | class RepoState < ApplicationRecord 2 | belongs_to :step 3 | end 4 | -------------------------------------------------------------------------------- /.kamal/hooks/post-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooted kamal-proxy on $KAMAL_HOSTS" 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /app/views/step/texts/_text.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= markdown(text.content) %> 3 |
-------------------------------------------------------------------------------- /app/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeDupuis/summoncircle/HEAD/app/assets/images/logo.png -------------------------------------------------------------------------------- /.kamal/hooks/post-app-boot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-app-boot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["SOLID_QUEUE_IN_PUMA"] = "1" 4 | 5 | exec "./bin/rails", "server", *ARGV 6 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /test/fixtures/secrets.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | -------------------------------------------------------------------------------- /app/views/step/inits/_init.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 🚀 Session Initialized 3 |
-------------------------------------------------------------------------------- /test/fixtures/agent_specific_settings.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html -------------------------------------------------------------------------------- /app/views/application/_flash_messages.html.erb: -------------------------------------------------------------------------------- 1 | <%= flash_message notice, type: "success" %> 2 | <%= flash_message alert, type: "danger" %> -------------------------------------------------------------------------------- /bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /app/models/step/ls_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::LsTool < Step::ToolCall 2 | def path 3 | tool_inputs&.dig("path") || content 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/agents/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit Agent

2 | 3 | <%= render 'form', agent: @agent %> 4 | 5 | <%= link_to 'Back', agents_path %> 6 | -------------------------------------------------------------------------------- /app/views/agents/new.html.erb: -------------------------------------------------------------------------------- 1 |

New Agent

2 | 3 | <%= render 'form', agent: @agent %> 4 | 5 | <%= link_to 'Back', agents_path %> 6 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /app/models/step/bash_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::BashTool < Step::ToolCall 2 | def command 3 | tool_inputs&.dig("command") || content 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/step/web_search_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::WebSearchTool < Step::ToolCall 2 | def query 3 | tool_inputs&.dig("query") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/environment" 4 | require "solid_queue/cli" 5 | 6 | SolidQueue::Cli.start(ARGV) 7 | -------------------------------------------------------------------------------- /bin/start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["SOLID_QUEUE_IN_PUMA"] = "1" 4 | ENV["RAILS_ENV"] = "production" 5 | 6 | exec "./bin/rails", "server", *ARGV 7 | -------------------------------------------------------------------------------- /config/initializers/mission_control_jobs.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | MissionControl::Jobs.base_controller_class = "AdminController" 3 | end 4 | -------------------------------------------------------------------------------- /app/jobs/run_job.rb: -------------------------------------------------------------------------------- 1 | class RunJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(run_id) 5 | Run.find(run_id).execute! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/current.rb: -------------------------------------------------------------------------------- 1 | class Current < ActiveSupport::CurrentAttributes 2 | attribute :session 3 | delegate :user, to: :session, allow_nil: true 4 | end 5 | -------------------------------------------------------------------------------- /app/models/step/result.rb: -------------------------------------------------------------------------------- 1 | class Step::Result < Step 2 | def error? 3 | parsed_response.is_a?(Hash) && parsed_response["is_error"] == true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /app/views/dashboard/index.html.erb: -------------------------------------------------------------------------------- 1 |

Dashboard

2 | 3 | <%= render 'tasks/task_form' %> 4 | 5 |

Recent Tasks

6 | <%= render 'tasks/task_list' %> 7 | 8 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/views/step/systems/_system.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 🔧 System 3 |
<%= system.content %>
4 |
-------------------------------------------------------------------------------- /test/models/volume_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class VolumeTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /db/migrate/20250525021550_add_role_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRoleToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :role, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250531043829_add_index_to_steps_type.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToStepsType < ActiveRecord::Migration[8.0] 2 | def change 3 | add_index :steps, :type 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | allowUnfree: true 2 | inputs: 3 | nixpkgs: 4 | url: github:NixOS/nixpkgs/nixpkgs-unstable 5 | nixpkgs-ruby: 6 | url: github:bobvanderlinden/nixpkgs-ruby 7 | -------------------------------------------------------------------------------- /app/tools/application_tool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationTool < ActionTool::Base 4 | # write your custom logic to be shared across all tools here 5 | end 6 | -------------------------------------------------------------------------------- /app/views/passwords_mailer/reset.text.erb: -------------------------------------------------------------------------------- 1 | You can reset your password within the next 15 minutes on this password reset page: 2 | <%= edit_password_url(@user.password_reset_token) %> 3 | -------------------------------------------------------------------------------- /db/migrate/20250602024248_add_ssh_key_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSshKeyToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :ssh_key, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/env_variable_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class EnvVariableTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/tasks/_runs_list.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% runs.each do |run| %> 3 | <%= render "tasks/run", run: run %> 4 | <% end %> 5 |
6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /db/migrate/20250530022252_remove_output_from_runs.rb: -------------------------------------------------------------------------------- 1 | class RemoveOutputFromRuns < ActiveRecord::Migration[8.0] 2 | def change 3 | remove_column :runs, :output, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/step-error.css: -------------------------------------------------------------------------------- 1 | .step-error { 2 | margin: 5px 0; 3 | color: #d73a49; 4 | background-color: #ffeef0; 5 | border-left: 4px solid #d73a49; 6 | padding-left: 10px; 7 | } -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /app/models/step/read_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::ReadTool < Step::ToolCall 2 | include LineNumberFormatting 3 | 4 | def file_path 5 | tool_inputs&.dig("file_path") || content 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20250602024607_add_git_config_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddGitConfigToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :git_config, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250602030052_add_home_path_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddHomePathToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :home_path, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250529063027_add_docker_host_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddDockerHostToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :docker_host, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250601034218_add_repo_path_to_projects.rb: -------------------------------------------------------------------------------- 1 | class AddRepoPathToProjects < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :projects, :repo_path, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250601071554_add_github_token_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddGithubTokenToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :github_token, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250618004105_add_description_to_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToTasks < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :tasks, :description, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250624013344_add_container_id_to_runs.rb: -------------------------------------------------------------------------------- 1 | class AddContainerIdToRuns < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :runs, :container_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.claude/bin/log: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | input = $stdin.read 4 | 5 | File.open('.claude/hook_log.txt', 'a') do |file| 6 | file.puts "=== #{Time.now} ===" 7 | file.puts input 8 | file.puts "" 9 | end -------------------------------------------------------------------------------- /app/mailers/passwords_mailer.rb: -------------------------------------------------------------------------------- 1 | class PasswordsMailer < ApplicationMailer 2 | def reset(user) 3 | @user = user 4 | mail subject: "Reset your password", to: user.email_address 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/resources/application_resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationResource < ActionResource::Base 4 | # write your custom logic to be shared across all resources here 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250601071738_add_user_to_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddUserToTasks < ActiveRecord::Migration[8.0] 2 | def change 3 | add_reference :tasks, :user, null: false, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250621070652_add_target_branch_to_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddTargetBranchToTasks < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :tasks, :target_branch, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250622234155_add_git_diff_to_repo_states.rb: -------------------------------------------------------------------------------- 1 | class AddGitDiffToRepoStates < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :repo_states, :git_diff, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/agent_specific_setting_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AgentSpecificSettingTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20250531203043_add_workplace_path_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddWorkplacePathToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :workplace_path, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250602025218_add_ssh_mount_path_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddSshMountPathToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :ssh_mount_path, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/env_variable.rb: -------------------------------------------------------------------------------- 1 | class EnvVariable < ApplicationRecord 2 | belongs_to :envable, polymorphic: true 3 | 4 | validates :key, presence: true, uniqueness: { scope: [ :envable_type, :envable_id ] } 5 | end 6 | -------------------------------------------------------------------------------- /app/views/passwords_mailer/reset.html.erb: -------------------------------------------------------------------------------- 1 |

2 | You can reset your password within the next 15 minutes on 3 | <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>. 4 |

5 | -------------------------------------------------------------------------------- /db/migrate/20250608080712_add_mcp_sse_endpoint_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddMcpSseEndpointToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :mcp_sse_endpoint, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250622044834_remove_docker_host_from_agents.rb: -------------------------------------------------------------------------------- 1 | class RemoveDockerHostFromAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | remove_column :agents, :docker_host, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250624023931_add_parent_tool_use_id_to_steps.rb: -------------------------------------------------------------------------------- 1 | class AddParentToolUseIdToSteps < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :steps, :parent_tool_use_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/glob-tool.css: -------------------------------------------------------------------------------- 1 | .glob-tool { 2 | --bg-search-tool: var(--bg-glob-tool); 3 | --bg-file-header: var(--bg-glob-header); 4 | } 5 | 6 | .glob-tool .tool-icon { 7 | color: var(--color-glob-icon); 8 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/grep-tool.css: -------------------------------------------------------------------------------- 1 | .grep-tool { 2 | --bg-search-tool: var(--bg-grep-tool); 3 | --bg-file-header: var(--bg-grep-header); 4 | } 5 | 6 | .grep-tool .tool-icon { 7 | color: var(--color-grep-icon); 8 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/read-tool.css: -------------------------------------------------------------------------------- 1 | .read-tool { 2 | --bg-file-tool: var(--bg-read-tool); 3 | --bg-file-header: var(--bg-read-header); 4 | } 5 | 6 | .read-tool .tool-icon { 7 | color: var(--color-read-icon); 8 | } -------------------------------------------------------------------------------- /app/models/step/web_fetch_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::WebFetchTool < Step::ToolCall 2 | def url 3 | tool_inputs&.dig("url") 4 | end 5 | 6 | def prompt 7 | tool_inputs&.dig("prompt") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20250531210210_add_volume_name_to_volume_mounts.rb: -------------------------------------------------------------------------------- 1 | class AddVolumeNameToVolumeMounts < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :volume_mounts, :volume_name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250601100859_add_user_id_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddUserIdToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :user_id, :integer, default: 1000, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250625002158_remove_env_variables_from_agents.rb: -------------------------------------------------------------------------------- 1 | class RemoveEnvVariablesFromAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | remove_column :agents, :env_variables, :json 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/step/task_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::TaskTool < Step::ToolCall 2 | def description 3 | tool_inputs&.dig("description") 4 | end 5 | 6 | def prompt 7 | tool_inputs&.dig("prompt") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20250530025441_add_log_processor_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddLogProcessorToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :log_processor, :string, default: "Text" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20250621025720_add_shrimp_mode_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddShrimpModeToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :shrimp_mode, :boolean, default: true, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] 5 | end 6 | -------------------------------------------------------------------------------- /app/javascript/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.textContent = "Hello World!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/views/tasks/_chat_panel.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render "tasks/chat_messages", runs: task.runs.order(:created_at) %> 3 | <%= render "tasks/run_form", task: task, run: Run.new %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/tasks/index.html.erb: -------------------------------------------------------------------------------- 1 |

Tasks for <%= @project.name %>

2 | 3 | <%= link_to 'New Task', new_project_task_path(@project) %> 4 | 5 | <%= render 'task_list' %> 6 | 7 | <%= link_to 'Back', project_path(@project) %> 8 | -------------------------------------------------------------------------------- /db/migrate/20250531210727_add_environment_variables_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddEnvironmentVariablesToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :environment_variables, :json 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/oauth-config.css: -------------------------------------------------------------------------------- 1 | .oauth-config { 2 | & > .actions { 3 | display: flex; 4 | gap: 1rem; 5 | } 6 | 7 | & > .note { 8 | margin-top: 1rem; 9 | font-size: 0.9em; 10 | color: #666; 11 | } 12 | } -------------------------------------------------------------------------------- /db/migrate/20250601005216_add_discarded_at_to_agents.rb: -------------------------------------------------------------------------------- 1 | class AddDiscardedAtToAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :agents, :discarded_at, :datetime 4 | add_index :agents, :discarded_at 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250601005217_add_discarded_at_to_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddDiscardedAtToTasks < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :tasks, :discarded_at, :datetime 4 | add_index :tasks, :discarded_at 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250531033829_add_step_type_and_content_to_steps.rb: -------------------------------------------------------------------------------- 1 | class AddStepTypeAndContentToSteps < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :steps, :type, :string 4 | add_column :steps, :content, :text 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250531210350_change_volume_id_to_nullable_in_volume_mounts.rb: -------------------------------------------------------------------------------- 1 | class ChangeVolumeIdToNullableInVolumeMounts < ActiveRecord::Migration[8.0] 2 | def change 3 | change_column_null :volume_mounts, :volume_id, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export DIRENV_WARN_TIMEOUT=20s 2 | 3 | eval "$(devenv direnvrc)" 4 | 5 | # The use_devenv function supports passing flags to the devenv command 6 | # For example: use devenv --impure --option services.postgres.enable:bool true 7 | use devenv 8 | -------------------------------------------------------------------------------- /bin/checks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "Running checks..." 6 | 7 | echo "Running security analysis..." 8 | bin/brakeman --no-pager 9 | 10 | echo "Running tests..." 11 | bin/rails t 12 | 13 | echo "All checks passed!" 14 | -------------------------------------------------------------------------------- /db/migrate/20250601005215_add_discarded_at_to_projects.rb: -------------------------------------------------------------------------------- 1 | class AddDiscardedAtToProjects < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :projects, :discarded_at, :datetime 4 | add_index :projects, :discarded_at 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/controllers/admin_controller.rb: -------------------------------------------------------------------------------- 1 | class AdminController < ApplicationController 2 | before_action :require_admin 3 | 4 | private 5 | 6 | def require_admin 7 | redirect_to root_path unless Current.session&.user&.admin? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/form-help.css: -------------------------------------------------------------------------------- 1 | .form-help { 2 | color: #666; 3 | font-size: 0.9em; 4 | margin-top: 4px; 5 | display: block; 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | .form-help { 10 | color: #b3b3b3; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/views/step/tool_results/_tool_result.html.erb: -------------------------------------------------------------------------------- 1 | <% unless tool_result.tool_call.present? %> 2 |
3 | ⚙️ Tool Result 4 |
<%= tool_result.content %>
5 |
6 | <% end %> -------------------------------------------------------------------------------- /db/migrate/20250614232508_add_external_to_volumes.rb: -------------------------------------------------------------------------------- 1 | class AddExternalToVolumes < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :volumes, :external, :boolean, default: false 4 | add_column :volumes, :external_name, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250618000000_add_allow_github_token_access_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAllowGithubTokenAccessToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :allow_github_token_access, :boolean, default: true, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/agents/index.html.erb: -------------------------------------------------------------------------------- 1 |

Agents

2 | 3 | <%= link_to 'New Agent', new_agent_path %> 4 | 5 | 12 | -------------------------------------------------------------------------------- /app/views/runs/create.turbo_stream.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_stream.replace "runs-list", partial: "tasks/runs_list", locals: { runs: [@run] } %> 2 | <%= turbo_stream.replace "chat-messages", partial: "tasks/chat_messages", locals: { runs: @run.task.runs.order(:created_at) } %> 3 | -------------------------------------------------------------------------------- /db/migrate/20250601212625_add_user_instructions_feature.rb: -------------------------------------------------------------------------------- 1 | class AddUserInstructionsFeature < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :instructions, :text 4 | add_column :agents, :instructions_mount_path, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/log_processor/text.rb: -------------------------------------------------------------------------------- 1 | class LogProcessor::Text < LogProcessor 2 | def process(logs) 3 | [ 4 | { raw_response: logs, type: "Step::Text", content: logs }, 5 | { raw_response: logs, type: "Step::Result", content: logs } 6 | ] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20250531020138_remove_prompt_and_setup_from_agents.rb: -------------------------------------------------------------------------------- 1 | class RemovePromptAndSetupFromAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | remove_column :agents, :agent_prompt, :text 4 | remove_column :agents, :setup_script, :text 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250531221419_rename_environment_variables_to_env_variables_on_agents.rb: -------------------------------------------------------------------------------- 1 | class RenameEnvironmentVariablesToEnvVariablesOnAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | rename_column :agents, :environment_variables, :env_variables 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/step/write_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::WriteTool < Step::ToolCall 2 | include LineNumberFormatting 3 | 4 | def file_path 5 | tool_inputs&.dig("file_path") || content 6 | end 7 | 8 | def file_content 9 | tool_inputs&.dig("content") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/volume.rb: -------------------------------------------------------------------------------- 1 | class Volume < ApplicationRecord 2 | belongs_to :agent 3 | has_many :volume_mounts, dependent: :destroy 4 | 5 | validates :name, presence: true 6 | validates :path, presence: true 7 | validates :external_name, presence: true, if: :external? 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20250609021050_add_auto_push_to_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddAutoPushToTasks < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :tasks, :auto_push_enabled, :boolean, default: false, null: false 4 | add_column :tasks, :auto_push_branch, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Authentication 3 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. 4 | allow_browser versions: :modern 5 | end 6 | -------------------------------------------------------------------------------- /app/models/step/multi_edit_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::MultiEditTool < Step::ToolCall 2 | include LineNumberFormatting 3 | 4 | def file_path 5 | tool_inputs&.dig("file_path") || content 6 | end 7 | 8 | def edits 9 | tool_inputs&.dig("edits") || [] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/projects/index.html.erb: -------------------------------------------------------------------------------- 1 |

Projects

2 | 3 | <%= link_to 'New Project', new_project_path %> 4 | 5 | 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/agent-actions.css: -------------------------------------------------------------------------------- 1 | .agent-actions { 2 | .archive { 3 | color: var(--color-danger); 4 | text-decoration: none; 5 | } 6 | 7 | .archive:hover, 8 | .archive:focus { 9 | color: var(--color-danger-hover); 10 | text-decoration: underline; 11 | } 12 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/status-indicator.css: -------------------------------------------------------------------------------- 1 | .status-indicator { 2 | font-weight: normal; 3 | } 4 | 5 | .status-indicator.-success { 6 | color: green; 7 | } 8 | 9 | .status-indicator.-warning { 10 | color: orange; 11 | } 12 | 13 | .status-indicator.-muted { 14 | color: #666; 15 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/task-actions.css: -------------------------------------------------------------------------------- 1 | .task-actions { 2 | .archive { 3 | color: var(--color-danger); 4 | text-decoration: none; 5 | } 6 | 7 | .archive:hover, 8 | .archive:focus { 9 | color: var(--color-danger-hover); 10 | text-decoration: underline; 11 | } 12 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/task-form.css: -------------------------------------------------------------------------------- 1 | .task-form { 2 | & .selectors { 3 | display: flex; 4 | gap: 1rem; 5 | } 6 | 7 | & .selectors .field { 8 | flex: 1; 9 | } 10 | 11 | & .selectors .field select { 12 | width: 100%; 13 | box-sizing: border-box; 14 | } 15 | } -------------------------------------------------------------------------------- /test/fixtures/steps.yml: -------------------------------------------------------------------------------- 1 | one: 2 | run: one 3 | raw_response: "hello" 4 | 5 | two: 6 | run: two 7 | raw_response: "world" 8 | 9 | error_step: 10 | run: one 11 | type: "Step::Error" 12 | raw_response: '{"error": "Something went wrong"}' 13 | content: "Error: Something went wrong" -------------------------------------------------------------------------------- /app/assets/stylesheets/project-actions.css: -------------------------------------------------------------------------------- 1 | .project-actions { 2 | .archive { 3 | color: var(--color-danger); 4 | text-decoration: none; 5 | } 6 | 7 | .archive:hover, 8 | .archive:focus { 9 | color: var(--color-danger-hover); 10 | text-decoration: underline; 11 | } 12 | } -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /db/migrate/20250530014329_create_steps.rb: -------------------------------------------------------------------------------- 1 | class CreateSteps < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :steps do |t| 4 | t.references :run, null: false, foreign_key: true 5 | t.json :raw_response 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /config/initializers/docker.rb: -------------------------------------------------------------------------------- 1 | # Configure global Docker URL if DOCKER_HOST environment variable is set 2 | if ENV["DOCKER_HOST"].present? 3 | Docker.url = ENV["DOCKER_HOST"] 4 | Docker.options = { 5 | read_timeout: 600, 6 | write_timeout: 600, 7 | connect_timeout: 60 8 | } 9 | end 10 | -------------------------------------------------------------------------------- /app/views/tasks/_chat_messages.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if runs.any? %> 3 | <% runs.each do |run| %> 4 | <%= render "runs/chat_item", run: run %> 5 | <% end %> 6 | <% else %> 7 |

No conversation yet.

8 | <% end %> 9 |
10 | -------------------------------------------------------------------------------- /config/initializers/test_encryption.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.test? 2 | ActiveRecord::Encryption.configure( 3 | primary_key: "1c7e6aa54b2cfbf79cce0e3d82c34d51", 4 | deterministic_key: "5db7bb47b6394d1689d6a5c47d28fbcd", 5 | key_derivation_salt: "9e6f0e5b9e524fb3afa6077274baf432" 6 | ) 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap via controllers/**/*_controller 2 | import { application } from "controllers/application" 3 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 4 | eagerLoadControllersFrom("controllers", application) 5 | -------------------------------------------------------------------------------- /app/views/step/bash_tools/_bash_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | $ 4 | <%= bash_tool.command %> 5 |
6 | 7 | <%= render "step/tool_calls/tool_result_section", tool_call: bash_tool %> 8 |
-------------------------------------------------------------------------------- /app/views/step/thinkings/_thinking.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 💭 4 | Thinking 5 |
6 |
7 | <%= markdown(thinking.content) %> 8 |
9 |
-------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application" 4 | pin "@hotwired/turbo-rails", to: "turbo.min.js" 5 | pin "@hotwired/stimulus", to: "stimulus.min.js" 6 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" 7 | pin_all_from "app/javascript/controllers", under: "controllers" 8 | -------------------------------------------------------------------------------- /db/migrate/20250528010412_create_volumes.rb: -------------------------------------------------------------------------------- 1 | class CreateVolumes < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :volumes do |t| 4 | t.string :name 5 | t.string :path 6 | t.references :agent, null: false, foreign_key: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/helpers/agents_helper.rb: -------------------------------------------------------------------------------- 1 | module AgentsHelper 2 | def agent_type_options(current_setting) 3 | available_settings = AgentSpecificSetting.available_types 4 | options = [ [ "None", "" ] ] + available_settings.map { |s| [ s[:display_name], s[:type] ] } 5 | options_for_select(options, current_setting&.type) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/step/tool_calls/_tool_call.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 🔧 Tool Call: <%= tool_call.tool_name %> 3 |
<%= tool_call.content %>
4 | 5 | <%= render "step/tool_calls/tool_result_section", tool_call: tool_call %> 6 |
-------------------------------------------------------------------------------- /db/migrate/20250524221111_create_projects.rb: -------------------------------------------------------------------------------- 1 | class CreateProjects < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :projects do |t| 4 | t.string :name 5 | t.text :description 6 | t.string :repository_url 7 | t.text :setup_script 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20250524223752_create_sessions.rb: -------------------------------------------------------------------------------- 1 | class CreateSessions < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :sessions do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :ip_address 6 | t.string :user_agent 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/tasks/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "task_#{@task.id}_description" do %> 2 | <%= form_with(model: @task, url: task_path(@task), class: "form") do |form| %> 3 | <%= form.text_field :description, value: @task.description, class: "input", autofocus: true %> 4 | <%= form.submit "Save", class: "save" %> 5 | <% end %> 6 | <% end %> -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | -------------------------------------------------------------------------------- /test/mailers/previews/passwords_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer 2 | class PasswordsMailerPreview < ActionMailer::Preview 3 | # Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset 4 | def reset 5 | PasswordsMailer.reset(User.take) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/recurring.yml: -------------------------------------------------------------------------------- 1 | # production: 2 | # periodic_cleanup: 3 | # class: CleanSoftDeletedRecordsJob 4 | # queue: background 5 | # args: [ 1000, { batch_size: 500 } ] 6 | # schedule: every hour 7 | # periodic_command: 8 | # command: "SoftDeletedRecord.due.delete_all" 9 | # priority: 2 10 | # schedule: at 5am every day 11 | -------------------------------------------------------------------------------- /db/migrate/20250528010426_create_volume_mounts.rb: -------------------------------------------------------------------------------- 1 | class CreateVolumeMounts < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :volume_mounts do |t| 4 | t.references :volume, null: false, foreign_key: true 5 | t.references :task, null: false, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20250624044111_add_auto_task_naming_agent_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAutoTaskNamingAgentToUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :users, :auto_task_naming_agent_id, :integer 4 | add_index :users, :auto_task_naming_agent_id 5 | add_foreign_key :users, :agents, column: :auto_task_naming_agent_id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Omakase Ruby styling for Rails 2 | inherit_gem: { rubocop-rails-omakase: rubocop.yml } 3 | 4 | # Overwrite or add rules to create your own house style 5 | # 6 | # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` 7 | # Layout/SpaceInsideArrayLiteralBrackets: 8 | # Enabled: false 9 | 10 | AllCops: 11 | Exclude: 12 | - 'db/*_schema.rb' 13 | -------------------------------------------------------------------------------- /db/migrate/20250524223751_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :email_address, null: false 5 | t.string :password_digest, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :users, :email_address, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/copy-button.css: -------------------------------------------------------------------------------- 1 | .copy-button { 2 | &.-success { 3 | background-color: var(--color-success); 4 | color: var(--color-white); 5 | border-color: var(--color-success); 6 | } 7 | 8 | &.-error { 9 | background-color: var(--color-danger); 10 | color: var(--color-white); 11 | border-color: var(--color-danger); 12 | } 13 | } -------------------------------------------------------------------------------- /app/views/step/edit_tools/_edit_tool_result.html.erb: -------------------------------------------------------------------------------- 1 | <% if edit_tool.tool_result %> 2 |
3 | 4 | Edit completed 5 |
6 | <% else %> 7 |
8 |
9 | Editing file... 10 |
11 | <% end %> -------------------------------------------------------------------------------- /test/jobs/run_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RunJobTest < ActiveJob::TestCase 4 | test "calls execute! on the run" do 5 | run = runs(:one) 6 | 7 | # Mock the run to expect execute! to be called 8 | Run.expects(:find).with(run.id).returns(run) 9 | run.expects(:execute!) 10 | 11 | RunJob.perform_now(run.id) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/tasks/_task_list.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/step/ls_tools/_ls_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 📋 4 | List Directory 5 | <%= ls_tool.path %> 6 |
7 | 8 | <%= render "step/tool_calls/tool_result_section", tool_call: ls_tool %> 9 |
-------------------------------------------------------------------------------- /app/views/step/write_tools/_write_tool_result.html.erb: -------------------------------------------------------------------------------- 1 | <% if write_tool.tool_result %> 2 |
3 | 4 | File written successfully 5 |
6 | <% else %> 7 |
8 |
9 | Writing file... 10 |
11 | <% end %> -------------------------------------------------------------------------------- /config/queue.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | dispatchers: 3 | - polling_interval: 1 4 | batch_size: 500 5 | workers: 6 | - queues: "*" 7 | threads: 3 8 | processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> 9 | polling_interval: 0.1 10 | 11 | development: 12 | <<: *default 13 | 14 | test: 15 | <<: *default 16 | 17 | production: 18 | <<: *default 19 | -------------------------------------------------------------------------------- /test/fixtures/env_variables.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | with_env_vars_node_env: 4 | key: NODE_ENV 5 | value: development 6 | envable: with_env_vars 7 | envable_type: Agent 8 | 9 | with_env_vars_debug: 10 | key: DEBUG 11 | value: "true" 12 | envable: with_env_vars 13 | envable_type: Agent 14 | -------------------------------------------------------------------------------- /db/migrate/20250601082414_create_repo_states.rb: -------------------------------------------------------------------------------- 1 | class CreateRepoStates < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :repo_states do |t| 4 | t.text :uncommitted_diff 5 | t.text :target_branch_diff 6 | t.string :repository_path 7 | t.references :step, null: false, foreign_key: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | config/credentials/*.yml.enc diff=rails_credentials 9 | config/credentials.yml.enc diff=rails_credentials 10 | -------------------------------------------------------------------------------- /app/javascript/controllers/display_toggle_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["content"] 5 | 6 | toggle(event) { 7 | const isChecked = event.target.checked 8 | this.contentTargets.forEach(target => { 9 | target.style.display = isChecked ? '' : 'none' 10 | }) 11 | } 12 | } -------------------------------------------------------------------------------- /app/views/step/multi_edit_tools/_multi_edit_tool_result.html.erb: -------------------------------------------------------------------------------- 1 | <% if multi_edit_tool.tool_result %> 2 |
3 | 4 | All edits completed 5 |
6 | <% else %> 7 |
8 |
9 | Applying edits... 10 |
11 | <% end %> -------------------------------------------------------------------------------- /db/migrate/20250606060235_add_tool_grouping_to_steps.rb: -------------------------------------------------------------------------------- 1 | class AddToolGroupingToSteps < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :steps, :tool_call_id, :integer 4 | add_column :steps, :tool_use_id, :string 5 | 6 | add_index :steps, :tool_call_id 7 | add_index :steps, [ :run_id, :tool_use_id ], name: "index_steps_on_run_id_and_tool_use_id" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20250524221353_create_agents.rb: -------------------------------------------------------------------------------- 1 | class CreateAgents < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :agents do |t| 4 | t.string :name 5 | t.string :docker_image 6 | t.text :agent_prompt 7 | t.text :setup_script 8 | t.json :start_arguments 9 | t.json :continue_arguments 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/step/read_tools/_read_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 📖 4 | Read File 5 | <%= read_tool.file_path %> 6 |
7 | 8 | <%= render "step/read_tools/read_tool_result", read_tool: read_tool %> 9 |
-------------------------------------------------------------------------------- /.kamal/hooks/post-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample post-deploy hook 4 | # 5 | # These environment variables are available: 6 | # KAMAL_RECORDED_AT 7 | # KAMAL_PERFORMER 8 | # KAMAL_VERSION 9 | # KAMAL_HOSTS 10 | # KAMAL_ROLES (if set) 11 | # KAMAL_DESTINATION (if set) 12 | # KAMAL_RUNTIME 13 | 14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" 15 | -------------------------------------------------------------------------------- /config/cache.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | store_options: 3 | # Cap age of oldest cache entry to fulfill retention policies 4 | # max_age: <%= 60.days.to_i %> 5 | max_size: <%= 256.megabytes %> 6 | namespace: <%= Rails.env %> 7 | 8 | development: 9 | database: cache 10 | <<: *default 11 | 12 | test: 13 | <<: *default 14 | 15 | production: 16 | database: cache 17 | <<: *default 18 | -------------------------------------------------------------------------------- /app/views/step/tool_calls/_tool_result_section.html.erb: -------------------------------------------------------------------------------- 1 | <% if tool_call.tool_result %> 2 |
3 | ⚙️ Tool Result 4 |
<%= tool_call.tool_result.content %>
5 |
6 | <% else %> 7 |
8 |
9 | Waiting for result... 10 |
11 | <% end %> -------------------------------------------------------------------------------- /db/migrate/20250602034140_create_project_secrets.rb: -------------------------------------------------------------------------------- 1 | class CreateProjectSecrets < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :secrets do |t| 4 | t.references :project, null: false, foreign_key: true 5 | t.string :key, null: false 6 | t.text :value 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :secrets, [ :project_id, :key ], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/step-init.css: -------------------------------------------------------------------------------- 1 | .step-init { 2 | margin: 5px 0; 3 | padding: 5px; 4 | background: #e8f4fd; 5 | border-left: 3px solid #007acc; 6 | font-size: 0.9em; 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | .step-init { 11 | background: #0f2a3f; 12 | border-left: 3px solid #4da6d9; 13 | color: #e6e6e6; 14 | } 15 | } 16 | 17 | .step-init > .label { 18 | display: block; 19 | } -------------------------------------------------------------------------------- /app/views/step/web_search_tools/_web_search_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 🔍 4 | Web Search 5 | <%= web_search_tool.query %> 6 |
7 | 8 | <%= render "step/tool_calls/tool_result_section", tool_call: web_search_tool %> 9 |
-------------------------------------------------------------------------------- /db/migrate/20250525021419_create_tasks.rb: -------------------------------------------------------------------------------- 1 | class CreateTasks < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :tasks do |t| 4 | t.references :project, null: false, foreign_key: true 5 | t.references :agent, null: false, foreign_key: true 6 | t.string :status 7 | t.datetime :started_at 8 | t.datetime :archived_at 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/tasks/_description.html.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "task_#{task.id}_description" do %> 2 | 3 | 4 | 5 | <%= link_to task.description.presence || "Task ##{task.id}", edit_task_path(task), style: "cursor: pointer; text-decoration: none; color: inherit;" %> 6 | <% end %> -------------------------------------------------------------------------------- /db/migrate/20250525021302_create_runs.rb: -------------------------------------------------------------------------------- 1 | class CreateRuns < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :runs do |t| 4 | t.references :task, null: false, foreign_key: true 5 | t.text :prompt 6 | t.text :output 7 | t.integer :status, default: 0, null: false 8 | t.datetime :started_at 9 | t.datetime :completed_at 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/step/edit_tool.rb: -------------------------------------------------------------------------------- 1 | class Step::EditTool < Step::ToolCall 2 | include LineNumberFormatting 3 | 4 | def file_path 5 | tool_inputs&.dig("file_path") || content 6 | end 7 | 8 | def old_string 9 | tool_inputs&.dig("old_string") 10 | end 11 | 12 | def new_string 13 | tool_inputs&.dig("new_string") 14 | end 15 | 16 | def replace_all? 17 | tool_inputs&.dig("replace_all") == true 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20250615021535_create_agent_specific_settings.rb: -------------------------------------------------------------------------------- 1 | class CreateAgentSpecificSettings < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :agent_specific_settings do |t| 4 | t.references :agent, null: false, foreign_key: true 5 | t.string :type, null: false 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :agent_specific_settings, [ :agent_id, :type ], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/volumes.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | path: MyString 6 | agent: one 7 | 8 | two: 9 | name: MyString 10 | path: MyString 11 | agent: two 12 | 13 | claude_config: 14 | name: claude_config 15 | path: /home/claude/.config 16 | agent: claude 17 | external: true 18 | external_name: claude_config_volume 19 | -------------------------------------------------------------------------------- /db/migrate/20250617014540_add_cost_tracking_to_steps.rb: -------------------------------------------------------------------------------- 1 | class AddCostTrackingToSteps < ActiveRecord::Migration[8.0] 2 | def change 3 | add_column :steps, :cost_usd, :decimal, precision: 10, scale: 8 4 | add_column :steps, :input_tokens, :integer 5 | add_column :steps, :output_tokens, :integer 6 | add_column :steps, :cache_creation_tokens, :integer 7 | add_column :steps, :cache_read_tokens, :integer 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/instructions-preview.css: -------------------------------------------------------------------------------- 1 | .instructions-preview { 2 | background-color: #f5f5f5; 3 | padding: 10px; 4 | border-radius: 4px; 5 | white-space: pre-wrap; 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | .instructions-preview { 10 | background-color: #2d2d2d; 11 | color: #e6e6e6; 12 | } 13 | } 14 | 15 | .instructions-preview.-scrollable { 16 | max-height: 200px; 17 | overflow-y: auto; 18 | } 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/run-item.css: -------------------------------------------------------------------------------- 1 | .run-item { 2 | border: 1px solid #ccc; 3 | padding: 10px; 4 | margin: 10px 0; 5 | } 6 | 7 | .run-item > .status { 8 | margin-bottom: 5px; 9 | } 10 | 11 | .run-item > .prompt { 12 | margin-bottom: 10px; 13 | } 14 | 15 | .run-item > .title { 16 | font-weight: bold; 17 | margin-bottom: 5px; 18 | } 19 | 20 | .run-item > .output { 21 | background: #f0f0f0; 22 | padding: 10px; 23 | overflow: auto; 24 | } -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | identified_by :current_user 4 | 5 | def connect 6 | set_current_user || reject_unauthorized_connection 7 | end 8 | 9 | private 10 | def set_current_user 11 | if session = Session.find_by(id: cookies.signed[:session_id]) 12 | self.current_user = session.user 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20250625001629_create_env_variables.rb: -------------------------------------------------------------------------------- 1 | class CreateEnvVariables < ActiveRecord::Migration[8.0] 2 | def change 3 | create_table :env_variables do |t| 4 | t.string :key, null: false 5 | t.string :value 6 | t.references :envable, polymorphic: true, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :env_variables, [ :envable_type, :envable_id, :key ], unique: true, name: 'index_env_vars_on_envable_and_key' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/tool-call.css: -------------------------------------------------------------------------------- 1 | .tool-call { 2 | margin: 5px 0; 3 | padding: 5px; 4 | background: #fff3cd; 5 | border-left: 3px solid #ffc107; 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | .tool-call { 10 | background: #3d3411; 11 | border-left: 3px solid #d4a017; 12 | color: #e6e6e6; 13 | } 14 | } 15 | 16 | .tool-call > .label { 17 | display: block; 18 | margin-bottom: 5px; 19 | } 20 | 21 | .tool-call > .content { 22 | margin: 5px 0 0 0; 23 | } -------------------------------------------------------------------------------- /devenv.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, config, inputs, ... }: 2 | { 3 | 4 | cachix.enable = false; 5 | 6 | env = { 7 | LD_LIBRARY_PATH = "${config.devenv.profile}/lib"; 8 | }; 9 | 10 | packages = with pkgs; [ 11 | git 12 | libyaml 13 | sqlite-interactive 14 | bashInteractive 15 | openssl 16 | curl 17 | libxml2 18 | libxslt 19 | libffi 20 | docker 21 | ]; 22 | 23 | languages.ruby.enable = true; 24 | languages.ruby.versionFile = ./.ruby-version; 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | <% password_digest = BCrypt::Password.create("password") %> 2 | 3 | one: 4 | email_address: one@example.com 5 | password_digest: <%= password_digest %> 6 | role: admin 7 | 8 | two: 9 | email_address: two@example.com 10 | password_digest: <%= password_digest %> 11 | role: standard 12 | 13 | no_github_access: 14 | email_address: no_github@example.com 15 | password_digest: <%= password_digest %> 16 | role: standard 17 | allow_github_token_access: false 18 | -------------------------------------------------------------------------------- /app/controllers/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | class DashboardController < ApplicationController 2 | def index 3 | @tasks = Task.kept.includes(:agent, :project).order(created_at: :desc) 4 | @task = Task.new 5 | @task.agent_id = cookies[:preferred_agent_id] if cookies[:preferred_agent_id].present? 6 | @task.project_id = cookies[:preferred_project_id] if cookies[:preferred_project_id].present? 7 | @task.runs.build 8 | @projects = Project.kept 9 | @agents = Agent.kept 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/prompt-truncate.css: -------------------------------------------------------------------------------- 1 | .prompt-toggle { 2 | background: none; 3 | border: none; 4 | color: var(--link-color, #0066cc); 5 | cursor: pointer; 6 | text-decoration: underline; 7 | font-size: inherit; 8 | font-family: inherit; 9 | padding: 0; 10 | margin-left: 0.5rem; 11 | } 12 | 13 | .prompt-toggle:hover { 14 | color: var(--link-hover-color, #004499); 15 | } 16 | 17 | .prompt-toggle:focus { 18 | outline: 2px solid var(--focus-color, #0066cc); 19 | outline-offset: 2px; 20 | } -------------------------------------------------------------------------------- /test/controllers/dashboard_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DashboardControllerTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @user = users(:one) 6 | end 7 | 8 | test "root requires authentication" do 9 | get root_url 10 | assert_redirected_to new_session_path 11 | 12 | login @user 13 | get root_url 14 | assert_response :success 15 | assert_match projects_path, @response.body 16 | assert_match agents_path, @response.body 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/secret.rb: -------------------------------------------------------------------------------- 1 | class Secret < ApplicationRecord 2 | belongs_to :secretable, polymorphic: true 3 | 4 | encrypts :value, deterministic: false 5 | 6 | validates :key, presence: true, uniqueness: { scope: [ :secretable_type, :secretable_id ] } 7 | validates :value, presence: true, on: :create 8 | 9 | before_validation :skip_blank_value_on_update 10 | 11 | private 12 | 13 | def skip_blank_value_on_update 14 | if persisted? && value.blank? 15 | self.value = value_was 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=q6Iuf7LiJkpzBgREI6oh1w5PAGP8I7Aa 2 | MCP_AUTH_TOKEN=0fdec0799098a261cacfefb8d03c86f83c53b535c5f55c68047f911cbade56d6ba6cca28958cb4e60e252950a1ee273c322a31cad119f69f1b65989f414f26e9 3 | ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=AGLheXe57DJJNZxAiWWam7J16zTIl4eW 4 | SECRET_KEY_BASE=7673a7ec4efb3a2bf86ff8631ec2fbd0578a927194e88c30b17231b69ecf47d2904fd1f06e4980d27b0c222eccf260306fd085a08edc291d0fda418230da37fe 5 | ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=tvcLHWLLBKQo4ReCfPC0YslWzewuyzq0 6 | -------------------------------------------------------------------------------- /app/views/pwa/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Summoncircle", 3 | "icons": [ 4 | { 5 | "src": "/icon.png", 6 | "type": "image/png", 7 | "sizes": "512x512" 8 | }, 9 | { 10 | "src": "/icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512", 13 | "purpose": "maskable" 14 | } 15 | ], 16 | "start_url": "/", 17 | "display": "standalone", 18 | "scope": "/", 19 | "description": "Summoncircle.", 20 | "theme_color": "red", 21 | "background_color": "red" 22 | } 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/run-form.css: -------------------------------------------------------------------------------- 1 | .run-form { 2 | display: flex; 3 | align-items: flex-start; 4 | gap: var(--spacing-sm); 5 | } 6 | 7 | .run-form > .field { 8 | flex: 1; 9 | margin-bottom: 0; 10 | } 11 | 12 | .run-form > .field > .textarea { 13 | width: 100%; 14 | box-sizing: border-box; 15 | } 16 | 17 | .run-form > .field > .errors { 18 | color: red; 19 | margin-top: 5px; 20 | } 21 | 22 | .run-form > .field > .errors > .error { 23 | margin: 2px 0; 24 | } 25 | 26 | .run-form > .actions { 27 | margin-top: 0; 28 | } -------------------------------------------------------------------------------- /app/views/application/_form_errors.html.erb: -------------------------------------------------------------------------------- 1 | <% if instance.errors.any? %> 2 |
3 |

4 | <%= t( 5 | ".title", 6 | count: instance.errors.count, 7 | name: (defined?(name) ? name : instance.class.model_name.human).downcase, 8 | ) %> 9 |

10 | 11 | 16 |
17 | <% end %> -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc, :ssh_key 8 | ] 9 | -------------------------------------------------------------------------------- /app/models/log_processor/claude_json.rb: -------------------------------------------------------------------------------- 1 | class LogProcessor::ClaudeJson < LogProcessor 2 | include LogProcessor::Concerns::ClaudeJsonProcessing 3 | 4 | def process(logs) 5 | begin 6 | parsed_array = JSON.parse(logs.strip) 7 | 8 | if parsed_array.is_a?(Array) 9 | parsed_array.map { |item| process_item(item) } 10 | else 11 | [ process_item(parsed_array) ] 12 | end 13 | rescue JSON::ParserError 14 | [ { raw_response: logs, type: "Step::Error", content: logs } ] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/volume_mounts.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | volume: one 5 | task: one 6 | volume_name: "summoncircle_MyString_volume_12345678-1234-5678-9abc-123456789abc" 7 | 8 | workplace_one: 9 | volume: null 10 | task: one 11 | volume_name: "summoncircle_workplace_volume_abcdef12-3456-7890-abcd-ef1234567890" 12 | 13 | two: 14 | volume: two 15 | task: two 16 | volume_name: "summoncircle_MyOtherString_volume_87654321-4321-8765-cba9-987654321fed" 17 | -------------------------------------------------------------------------------- /app/views/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |

Forgot your password?

2 | 3 | <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> 4 | 5 | <% if @smtp_configured %> 6 | <%= form_with url: passwords_path do |form| %> 7 | <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
8 | <%= form.submit "Email reset instructions" %> 9 | <% end %> 10 | <% else %> 11 |

You should really be careful with theses...

12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Update your password

2 | 3 | <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> 4 | 5 | <%= form_with url: password_path(params[:token]), method: :put do |form| %> 6 | <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %>
7 | <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %>
8 | <%= form.submit "Save" %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/views/step/web_fetch_tools/_web_fetch_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 🌐 4 | Web Fetch 5 | <%= web_fetch_tool.url %> 6 |
7 | <% if web_fetch_tool.prompt.present? %> 8 |
9 | <%= web_fetch_tool.prompt %> 10 |
11 | <% end %> 12 | 13 | <%= render "step/tool_calls/tool_result_section", tool_call: web_fetch_tool %> 14 |
-------------------------------------------------------------------------------- /test/models/log_processor_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LogProcessorTest < ActiveSupport::TestCase 4 | test "class process method raises NotImplementedError" do 5 | logs = "Test log output" 6 | assert_raises(NotImplementedError) do 7 | LogProcessor.process(logs) 8 | end 9 | end 10 | 11 | test "instance process method raises NotImplementedError" do 12 | processor = LogProcessor.new 13 | logs = "Test log output" 14 | assert_raises(NotImplementedError) do 15 | processor.process(logs) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/action-button.css: -------------------------------------------------------------------------------- 1 | .action-button { 2 | padding: 8px 16px; 3 | color: white; 4 | text-decoration: none; 5 | border-radius: 4px; 6 | display: inline-block; 7 | border: none; 8 | cursor: pointer; 9 | transition: opacity 0.2s; 10 | } 11 | 12 | .action-button:link, 13 | .action-button:visited { 14 | color: white; 15 | text-decoration: none; 16 | } 17 | 18 | .action-button:hover { 19 | opacity: 0.9; 20 | color: white; 21 | } 22 | 23 | .action-button.-primary { 24 | background: #007bff; 25 | } 26 | 27 | .action-button.-success { 28 | background: #28a745; 29 | } -------------------------------------------------------------------------------- /app/views/step/task_tools/_task_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 🤖 4 | Sub-Agent Task 5 | <%= task_tool.description %> 6 |
7 | <% if task_tool.prompt.present? %> 8 |
9 | Instructions: 10 | <%= task_tool.prompt %> 11 |
12 | <% end %> 13 | 14 | <%= render "step/task_tools/task_tool_result", task_tool: task_tool %> 15 |
-------------------------------------------------------------------------------- /app/assets/stylesheets/system-step.css: -------------------------------------------------------------------------------- 1 | .system-step { 2 | margin: 5px 0; 3 | padding: 5px; 4 | background: #f0f0f0; 5 | border-left: 3px solid #999; 6 | font-size: 0.9em; 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | .system-step { 11 | background: #2d2d2d; 12 | border-left: 3px solid #bbb; 13 | color: #e6e6e6; 14 | } 15 | } 16 | 17 | .system-step > .label { 18 | display: block; 19 | margin-bottom: 5px; 20 | } 21 | 22 | .system-step > .message { 23 | margin: 5px 0; 24 | } 25 | 26 | .system-step > .content { 27 | margin: 5px 0 0 0; 28 | white-space: pre-wrap; 29 | } -------------------------------------------------------------------------------- /app/views/step/results/_result.html.erb: -------------------------------------------------------------------------------- 1 | <% icon_and_label = result.error? ? "❌ Result (Error)" : "✅ Result" %> 2 | 3 | <% if result.content.present? && result.run.steps.where.not(id: result.id).none? { |s| s.content == result.content } %> 4 |
"> 5 |
<%= icon_and_label %>
6 |
<%= result.content %>
7 |
8 | <% else %> 9 |
"> 10 |
<%= icon_and_label %>
11 |
12 | <% end %> -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | has_secure_password 3 | has_many :sessions, dependent: :destroy 4 | belongs_to :auto_task_naming_agent, class_name: "Agent", optional: true 5 | 6 | encrypts :github_token, deterministic: false 7 | encrypts :ssh_key, deterministic: false 8 | 9 | normalizes :email_address, with: ->(e) { e.strip.downcase } 10 | 11 | enum :role, { standard: 0, admin: 1 }, allow_nil: true 12 | 13 | def env_strings 14 | vars = [] 15 | vars << "GITHUB_TOKEN=#{github_token}" if github_token.present? && allow_github_token_access 16 | vars 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/tasks/_run.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | Status: <%= run.status %> 4 | <% if run.started_at %> 5 | | Started: <%= run.started_at.strftime("%Y-%m-%d %H:%M:%S") %> 6 | <% end %> 7 | <% if run.completed_at %> 8 | | Completed: <%= run.completed_at.strftime("%Y-%m-%d %H:%M:%S") %> 9 | <% end %> 10 | <% if run.total_cost > 0 %> 11 | | Cost: $<%= "%.6f" % run.total_cost %> 12 | <% end %> 13 |

14 | <%= render "runs/tabs", run: run %> 15 |
-------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "PreToolUse": [ 4 | { 5 | "matcher": "Bash|mcp__git__git_commit", 6 | "hooks": [ 7 | { 8 | "type": "command", 9 | "command": "$(git rev-parse --show-toplevel)/.claude/bin/lint" 10 | } 11 | ] 12 | }, 13 | { 14 | "matcher": "Bash|mcp__github__push_files", 15 | "hooks": [ 16 | { 17 | "type": "command", 18 | "command": "$(git rev-parse --show-toplevel)/.claude/bin/pre-push" 19 | } 20 | ] 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css. 3 | * 4 | * With Propshaft, assets are served efficiently without preprocessing steps. You can still include 5 | * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard 6 | * cascading order, meaning styles declared later in the document or manifest will override earlier ones, 7 | * depending on specificity. 8 | * 9 | * Consider organizing styles into separate files for maintainability. 10 | */ 11 | 12 | ._hidden { 13 | display: none !important; 14 | } 15 | -------------------------------------------------------------------------------- /test/models/step/error_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Step::ErrorTest < ActiveSupport::TestCase 4 | test "Step::Error inherits from Step" do 5 | assert Step::Error < Step 6 | end 7 | 8 | test "can create Step::Error instance" do 9 | run = runs(:one) 10 | step = Step::Error.create!( 11 | run: run, 12 | raw_response: '{"error": "Something went wrong"}', 13 | content: "Error: Something went wrong" 14 | ) 15 | 16 | assert step.persisted? 17 | assert_equal "Step::Error", step.type 18 | assert_equal "Error: Something went wrong", step.content 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20250621075100_add_docker_container_support.rb: -------------------------------------------------------------------------------- 1 | class AddDockerContainerSupport < ActiveRecord::Migration[8.0] 2 | def change 3 | # Add Docker container info to tasks 4 | add_column :tasks, :container_id, :string 5 | add_column :tasks, :container_name, :string 6 | add_column :tasks, :container_status, :string 7 | add_column :tasks, :docker_image_id, :string 8 | add_column :tasks, :container_host_port, :integer 9 | 10 | # Add Docker support to projects 11 | add_column :projects, :dev_dockerfile_path, :text 12 | add_column :projects, :dev_container_port, :integer, default: 3000 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/step/tool_result.rb: -------------------------------------------------------------------------------- 1 | class Step::ToolResult < Step 2 | belongs_to :tool_call, class_name: "Step", optional: true 3 | 4 | after_create_commit :link_to_tool_call 5 | 6 | private 7 | 8 | def link_to_tool_call 9 | return unless respond_to?(:tool_use_id) && tool_use_id.present? 10 | 11 | tool_call_types = [ Step::ToolCall ] + Step::ToolCall.descendants 12 | matching_tool_call = run.steps.where( 13 | type: tool_call_types.map(&:name), 14 | tool_use_id: tool_use_id 15 | ).first 16 | 17 | if matching_tool_call 18 | update_column(:tool_call_id, matching_tool_call.id) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/step/read_tools/_read_tool_result.html.erb: -------------------------------------------------------------------------------- 1 | <% if read_tool.tool_result %> 2 |
3 |
4 | <% read_tool.format_code_with_line_numbers(read_tool.tool_result.content).each do |line_data| %> 5 |
6 | <%= line_data[:number] %> 7 | <%= line_data[:content] %> 8 |
9 | <% end %> 10 |
11 |
12 | <% else %> 13 |
14 |
15 | Reading file... 16 |
17 | <% end %> -------------------------------------------------------------------------------- /app/assets/stylesheets/branch-indicator.css: -------------------------------------------------------------------------------- 1 | .branch-indicator { 2 | position: fixed; 3 | bottom: 20px; 4 | right: 20px; 5 | background: #2d3748; 6 | color: #e2e8f0; 7 | padding: 8px 12px; 8 | border-radius: 6px; 9 | font-size: 12px; 10 | font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; 11 | border: 1px solid #4a5568; 12 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 13 | z-index: 1000; 14 | opacity: 0.9; 15 | transition: opacity 0.2s ease; 16 | } 17 | 18 | .branch-indicator:hover { 19 | opacity: 1; 20 | } 21 | 22 | .branch-indicator::before { 23 | content: "⎇ "; 24 | margin-right: 4px; 25 | } -------------------------------------------------------------------------------- /test/fixtures/repo_states.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | uncommitted_diff: | 5 | diff --git a/app/models/user.rb b/app/models/user.rb 6 | index 1234567..890abcd 100644 7 | --- a/app/models/user.rb 8 | +++ b/app/models/user.rb 9 | @@ -1,5 +1,6 @@ 10 | class User < ApplicationRecord 11 | has_secure_password 12 | + has_many :projects 13 | end 14 | target_branch_diff: 15 | repository_path: /workspace/myproject 16 | step: one 17 | 18 | two: 19 | uncommitted_diff: "" 20 | target_branch_diff: 21 | repository_path: /workspace/anotherproject 22 | step: two 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/tool-result.css: -------------------------------------------------------------------------------- 1 | .tool-result.-pending { 2 | display: flex; 3 | align-items: center; 4 | gap: 0.5rem; 5 | padding: 0.75rem; 6 | margin-top: 0.5rem; 7 | background-color: var(--color-background-secondary); 8 | border-radius: var(--radius-md); 9 | font-size: 0.875rem; 10 | color: var(--color-text-secondary); 11 | } 12 | 13 | .tool-result.-pending > .spinner { 14 | width: 1rem; 15 | height: 1rem; 16 | border: 2px solid var(--color-border); 17 | border-top-color: var(--color-primary); 18 | border-radius: 50%; 19 | animation: spin 0.8s linear infinite; 20 | } 21 | 22 | .tool-call .tool-result { 23 | margin-top: 0.5rem; 24 | margin-left: 1rem; 25 | } -------------------------------------------------------------------------------- /app/models/claude_oauth_setting.rb: -------------------------------------------------------------------------------- 1 | class ClaudeOauthSetting < AgentSpecificSetting 2 | def oauth 3 | @oauth ||= ClaudeOauth.new(agent) 4 | end 5 | 6 | def credentials_exist? 7 | oauth.check_credentials_exist 8 | rescue => e 9 | Rails.logger.error "Failed to check OAuth credentials: #{e.message}" 10 | false 11 | end 12 | 13 | def token_expiry 14 | oauth.get_token_expiry 15 | rescue => e 16 | Rails.logger.error "Failed to get token expiry: #{e.message}" 17 | nil 18 | end 19 | 20 | def self.display_name 21 | "Claude OAuth" 22 | end 23 | 24 | def self.description 25 | "Enable OAuth authentication for Claude CLI access" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/javascript/controllers/alert_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["alert"] 5 | static classes = [ "closeBtn" ] 6 | 7 | connect() { 8 | if(!this.element.querySelector(`.${this.closeBtnClass}`)){ 9 | const $btn = document.createElement("button"); 10 | $btn.innerHTML = "×"; 11 | $btn.classList.add(this.closeBtnClass); 12 | $btn.setAttribute("aria-hidden", true); 13 | $btn.addEventListener("click", this.dismiss.bind(this)); 14 | 15 | this.element.prepend($btn); 16 | } 17 | } 18 | 19 | dismiss(event) { 20 | this.element.remove(); 21 | } 22 | } -------------------------------------------------------------------------------- /app/views/tasks/_run_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with model: [@task, run], id: "run-form", class: "run-form", data: { controller: "prompt", action: "turbo:submit-end->prompt#clearForm" } do |form| %> 2 |
3 | <%= form.text_area :prompt, required: true, rows: 4, class: "textarea", data: { prompt_target: "textarea", action: "keydown->prompt#keydown" } %> 4 | <% if run.errors.any? %> 5 |
6 | <% run.errors.full_messages.each do |message| %> 7 |
<%= message %>
8 | <% end %> 9 |
10 | <% end %> 11 |
12 |
13 | <%= form.submit "Run" %> 14 |
15 | <% end %> -------------------------------------------------------------------------------- /bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Enable jemalloc for reduced memory usage and latency. 4 | if [ -z "${LD_PRELOAD+x}" ]; then 5 | LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) 6 | export LD_PRELOAD 7 | fi 8 | 9 | # If running the rails server then create or migrate existing database 10 | if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then 11 | ./bin/rails db:prepare 12 | fi 13 | 14 | # If running the rails server and RAILS_BINDING is set, add binding parameter 15 | if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ] && [ -n "${RAILS_BINDING}" ]; then 16 | exec "${@}" -b "${RAILS_BINDING}" 17 | else 18 | exec "${@}" 19 | fi 20 | -------------------------------------------------------------------------------- /app/assets/stylesheets/write-tool.css: -------------------------------------------------------------------------------- 1 | .write-tool { 2 | --bg-file-tool: var(--bg-write-tool); 3 | --bg-file-header: var(--bg-write-header); 4 | } 5 | 6 | .write-tool .tool-icon { 7 | color: var(--color-write-icon); 8 | } 9 | 10 | .write-tool .file-content { 11 | padding: 0; 12 | } 13 | 14 | .write-tool .content-preview .code-block { 15 | max-height: 300px; 16 | overflow: auto; 17 | } 18 | 19 | .write-tool .truncated-indicator { 20 | padding: 0.5rem 1rem; 21 | text-align: center; 22 | background: var(--bg-truncated); 23 | border-top: 1px solid var(--border); 24 | } 25 | 26 | .write-tool .truncated { 27 | font-style: italic; 28 | color: var(--text-secondary); 29 | font-size: 0.875rem; 30 | } -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | allow_unauthenticated_access only: %i[ new create ] 3 | rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } 4 | 5 | def new 6 | end 7 | 8 | def create 9 | if user = User.authenticate_by(params.permit(:email_address, :password)) 10 | start_new_session_for user 11 | redirect_to after_authentication_url 12 | else 13 | redirect_to new_session_path, alert: "Try another email address or password." 14 | end 15 | end 16 | 17 | def destroy 18 | terminate_session 19 | redirect_to new_session_path 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/projects.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: MyString 5 | description: MyText 6 | repository_url: http://example.com/one.git 7 | setup_script: "" 8 | 9 | two: 10 | name: MyString 11 | description: MyText 12 | repository_url: http://example.com/two.git 13 | setup_script: "" 14 | 15 | without_repo: 16 | name: Test Project 17 | # No repository_url - should skip git clone 18 | 19 | with_repo: 20 | name: Test Project 21 | repository_url: https://github.com/test/repo.git 22 | 23 | with_repo_and_path: 24 | name: Test Project 25 | repository_url: https://github.com/test/repo.git 26 | repo_path: myapp 27 | -------------------------------------------------------------------------------- /app/views/step/glob_tools/_glob_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 🔍 4 | File Pattern Search 5 | <%= glob_tool.tool_inputs&.dig("pattern") || glob_tool.content %> 6 |
7 | 8 | <% if glob_tool.tool_inputs&.dig("path") %> 9 |
10 | In directory: 11 | <%= glob_tool.tool_inputs["path"] %> 12 |
13 | <% end %> 14 | 15 | <%= render "step/tool_calls/tool_result_section", tool_call: glob_tool %> 16 |
-------------------------------------------------------------------------------- /app/assets/stylesheets/step-result.css: -------------------------------------------------------------------------------- 1 | .step-result { 2 | margin: 5px 0; 3 | padding: 5px; 4 | background: #d4edda; 5 | border-left: 3px solid #28a745; 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | .step-result { 10 | background: #1e4d1e; 11 | border-left: 3px solid #4caf50; 12 | color: #e6e6e6; 13 | } 14 | } 15 | 16 | .step-result.-error { 17 | background: #f8d7da; 18 | border-left-color: #dc3545; 19 | } 20 | 21 | @media (prefers-color-scheme: dark) { 22 | .step-result.-error { 23 | background: #4d1e1e; 24 | border-left-color: #f44336; 25 | } 26 | } 27 | 28 | .step-result > .label { 29 | font-weight: bold; 30 | } 31 | 32 | .step-result > .content { 33 | margin: 5px 0 0 0; 34 | } -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /app/views/tasks/_header_content.html.erb: -------------------------------------------------------------------------------- 1 |
2 | Agent: <%= task.agent.name %> 3 | Project: <%= task.project.name %> 4 | <% if task.runs.any? && task.runs.any?(&:completed?) %> 5 | <% if task.total_cost > 0 %> 6 | Total Cost: $<%= "%.6f" % task.total_cost %> 7 | <% end %> 8 | <% end %> 9 |
10 | <% if task.runs.any? && task.runs.any?(&:completed?) %> 11 | <%= render "tasks/actions_header", task: task %> 12 | <% if task.user.github_token.present? %> 13 | <%= render "tasks/auto_push_form", task: task %> 14 | <% end %> 15 | <% if task.project.dev_dockerfile_path.present? %> 16 | <%= render "tasks/docker_controls", task: task %> 17 | <% end %> 18 | <% end %> -------------------------------------------------------------------------------- /app/views/application/_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | # Async adapter only works within the same process, so for manually triggering cable updates from a console, 2 | # and seeing results in the browser, you must do so from the web console (running inside the dev process), 3 | # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view 4 | # to make the web console appear. 5 | development: 6 | adapter: solid_cable 7 | connects_to: 8 | database: 9 | writing: cable 10 | polling_interval: 0.1.seconds 11 | message_retention: 1.day 12 | 13 | test: 14 | adapter: test 15 | 16 | production: 17 | adapter: solid_cable 18 | connects_to: 19 | database: 20 | writing: cable 21 | polling_interval: 0.1.seconds 22 | message_retention: 1.day 23 | -------------------------------------------------------------------------------- /app/javascript/controllers/tabs_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["tab", "panel"] 5 | 6 | switchTab(event) { 7 | const clickedTab = event.currentTarget 8 | const targetPanel = clickedTab.dataset.tabPanel 9 | 10 | this.tabTargets.forEach(tab => { 11 | tab.classList.remove("-active") 12 | }) 13 | 14 | this.panelTargets.forEach(panel => { 15 | panel.classList.remove("-active") 16 | }) 17 | 18 | clickedTab.classList.add("-active") 19 | 20 | const activePanel = this.panelTargets.find(panel => panel.dataset.panelId === targetPanel) 21 | if (activePanel) { 22 | activePanel.classList.add("-active") 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/tools/rename_task_tool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameTaskTool < ApplicationTool 4 | description "Rename a task's description" 5 | 6 | arguments do 7 | required(:new_name).filled(:string).description("New name/description for the task") 8 | end 9 | 10 | def call(new_name:) 11 | task_id = headers["x-task-id"] 12 | 13 | if task_id.blank? 14 | return "Error: No task ID provided in headers" 15 | end 16 | 17 | task = Task.find_by(id: task_id) 18 | 19 | if task.nil? 20 | return "Error: Task with ID #{task_id} not found" 21 | end 22 | 23 | old_name = task.description 24 | task.update!(description: new_name) 25 | 26 | "Successfully renamed task from '#{old_name}' to '#{new_name}'" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/models/repo_state_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RepoStateTest < ActiveSupport::TestCase 4 | test "belongs to step" do 5 | repo_state = repo_states(:one) 6 | assert_equal steps(:one), repo_state.step 7 | end 8 | 9 | test "can store uncommitted diff" do 10 | repo_state = repo_states(:one) 11 | assert repo_state.uncommitted_diff.present? 12 | assert repo_state.uncommitted_diff.include?("has_many :projects") 13 | end 14 | 15 | test "can have empty uncommitted diff" do 16 | repo_state = repo_states(:two) 17 | assert_equal "", repo_state.uncommitted_diff 18 | end 19 | 20 | test "stores repository path" do 21 | repo_state = repo_states(:one) 22 | assert_equal "/workspace/myproject", repo_state.repository_path 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/volume_mount.rb: -------------------------------------------------------------------------------- 1 | class VolumeMount < ApplicationRecord 2 | belongs_to :volume, optional: true 3 | belongs_to :task 4 | 5 | validates :volume_id, uniqueness: { scope: :task_id }, allow_nil: true 6 | 7 | before_validation :generate_volume_name 8 | 9 | def container_path 10 | volume&.path || task.agent.workplace_path 11 | end 12 | 13 | def bind_string 14 | "#{volume_name}:#{container_path}" 15 | end 16 | 17 | private 18 | 19 | def generate_volume_name 20 | return if volume_name.present? 21 | 22 | if volume&.external? 23 | self.volume_name = volume.external_name 24 | else 25 | base_name = volume&.name || "workplace" 26 | self.volume_name = "summoncircle_#{base_name}_volume_#{SecureRandom.uuid}" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/assets/stylesheets/bash-tool.css: -------------------------------------------------------------------------------- 1 | .bash-tool { 2 | background-color: var(--bg-terminal, #1e1e1e); 3 | color: var(--text-terminal, #d4d4d4); 4 | padding: 0.75rem 1rem; 5 | border-radius: 4px; 6 | font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; 7 | font-size: 0.875rem; 8 | line-height: 1.5; 9 | margin: 0.5rem 0; 10 | overflow-x: auto; 11 | } 12 | 13 | .bash-tool > .terminal-prompt { 14 | display: flex; 15 | align-items: center; 16 | gap: 0.5rem; 17 | } 18 | 19 | .bash-tool .prompt-symbol { 20 | color: var(--prompt-symbol, #569cd6); 21 | font-weight: bold; 22 | user-select: none; 23 | } 24 | 25 | .bash-tool .command { 26 | color: var(--command-text, #d4d4d4); 27 | flex: 1; 28 | white-space: pre-wrap; 29 | word-break: break-word; 30 | } -------------------------------------------------------------------------------- /app/views/tasks/_auto_push_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= form_with model: task, url: update_auto_push_task_path(task), method: :patch, class: "auto-push", data: { turbo_frame: "_top" } do |form| %> 3 | <%= form.label :auto_push_branch, "Auto Push:", class: "label" %> 4 |
5 | <%= form.select :auto_push_branch, 6 | options_for_select(task.branches, task.auto_push_branch), 7 | { include_blank: "Select branch..." }, 8 | { class: "select" } %> 9 |
10 | <%= link_to "↻", branches_task_path(task), 11 | data: { turbo_method: :get }, 12 | class: "button action-button -mini", 13 | title: "Refresh branches" %> 14 | <%= form.submit "Save", class: "button action-button -primary -mini" %> 15 | <% end %> 16 |
-------------------------------------------------------------------------------- /.claude/bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | 5 | # Check if stdin contains git commit commands 6 | if STDIN.tty? 7 | # No stdin, don't run lint 8 | exit 0 9 | else 10 | # Read stdin 11 | stdin_content = STDIN.read 12 | 13 | begin 14 | # Parse the JSON 15 | data = JSON.parse(stdin_content) 16 | 17 | # Check if it's a git commit command 18 | if data['tool_name'] == 'Bash' && data['tool_input'] && data['tool_input']['command'] && data['tool_input']['command'].match(/^git\s+commit/) 19 | system("$(git rev-parse --show-toplevel)/bin/lint_staged") 20 | elsif data['tool_name'] == 'mcp__git__git_commit' 21 | system("$(git rev-parse --show-toplevel)/bin/lint_staged") 22 | else 23 | # Not a git commit, don't run lint 24 | exit 0 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | test "role enum works" do 5 | user = User.new(email_address: "test@example.com", password: "secret", password_confirmation: "secret") 6 | assert_nil user.role 7 | 8 | user.role = :admin 9 | assert user.admin? 10 | 11 | user.role = :standard 12 | assert user.standard? 13 | end 14 | 15 | test "ssh_key is encrypted" do 16 | user = users(:one) 17 | ssh_key_content = "-----BEGIN OPENSSH PRIVATE KEY-----\ntest_key_content\n-----END OPENSSH PRIVATE KEY-----" 18 | 19 | user.update!(ssh_key: ssh_key_content) 20 | user.reload 21 | 22 | assert_equal ssh_key_content, user.ssh_key 23 | assert_not_equal ssh_key_content, user.read_attribute_before_type_cast(:ssh_key) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/account_settings_controller.rb: -------------------------------------------------------------------------------- 1 | class AccountSettingsController < ApplicationController 2 | def edit 3 | @user = Current.user 4 | end 5 | 6 | def update 7 | @user = Current.user 8 | 9 | # Filter out password fields if they're blank 10 | filtered_params = account_params 11 | if filtered_params[:password].blank? 12 | filtered_params.delete(:password) 13 | filtered_params.delete(:password_confirmation) 14 | end 15 | 16 | if @user.update(filtered_params) 17 | redirect_to user_settings_path, notice: "Account settings updated successfully" 18 | else 19 | render :edit, status: :unprocessable_entity 20 | end 21 | end 22 | 23 | private 24 | 25 | def account_params 26 | params.require(:user).permit(:email_address, :password, :password_confirmation) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.kamal/secrets: -------------------------------------------------------------------------------- 1 | SECRETS=$(kamal secrets fetch --adapter 1password --account $OP_ACCOUNT --from $OP_SECRETS KAMAL_REGISTRY_PASSWORD ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT MCP_AUTH_TOKEN SECRET_KEY_BASE) 2 | KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) 3 | SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE ${SECRETS}) 4 | ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY ${SECRETS}) 5 | ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY ${SECRETS}) 6 | ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT ${SECRETS}) 7 | MCP_AUTH_TOKEN=$(kamal secrets extract MCP_AUTH_TOKEN ${SECRETS}) 8 | -------------------------------------------------------------------------------- /app/controllers/runs_controller.rb: -------------------------------------------------------------------------------- 1 | class RunsController < ApplicationController 2 | before_action :set_task 3 | 4 | def create 5 | @run = @task.runs.build(run_params) 6 | 7 | respond_to do |format| 8 | if @run.save 9 | format.turbo_stream 10 | format.html { redirect_to task_path(@task), notice: "Run started successfully." } 11 | else 12 | format.turbo_stream { redirect_to task_path(@task), alert: "Failed to start run: #{@run.errors.full_messages.join(', ')}" } 13 | format.html { redirect_to task_path(@task), alert: "Failed to start run: #{@run.errors.full_messages.join(', ')}" } 14 | end 15 | end 16 | end 17 | 18 | private 19 | 20 | def set_task 21 | @task = Task.find(params[:task_id]) 22 | end 23 | 24 | def run_params 25 | params.require(:run).permit(:prompt) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /bin/kamal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'kamal' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("kamal", "kamal") 28 | -------------------------------------------------------------------------------- /app/models/log_processor.rb: -------------------------------------------------------------------------------- 1 | class LogProcessor 2 | include DockerStreamProcessor 3 | 4 | ALL = [ 5 | LogProcessor::Text, 6 | LogProcessor::ClaudeJson, 7 | LogProcessor::ClaudeStreamingJson 8 | ].freeze 9 | 10 | def self.process(logs) 11 | new.process(logs) 12 | end 13 | 14 | def process(logs) 15 | raise NotImplementedError, "Subclasses must implement #process" 16 | end 17 | 18 | def process_container(container, run) 19 | # Default behavior: wait for container, get logs, process them 20 | container.wait 21 | logs = container.logs(stdout: true, stderr: true) 22 | # Process Docker's binary stream format 23 | clean_logs = process_docker_stream(logs) 24 | 25 | step_data_list = process(clean_logs) 26 | step_data_list.each do |step_data| 27 | run.steps.create!(step_data) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/runs/_chat_item.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= markdown(run.prompt) %> 4 |
5 | 8 | <% result_step = run.steps.find { |s| s.is_a?(Step::Result) } %> 9 | <% error_step = run.steps.find { |s| s.is_a?(Step::Error) } %> 10 | <% if result_step %> 11 |
12 | <%= markdown(result_step.content) %> 13 |
14 | <% elsif error_step %> 15 |
16 |
<%= error_step.content %>
17 |
18 | <% elsif run.status == "running" %> 19 |
20 | Processing... 21 |
22 | <% end %> 23 |
24 | -------------------------------------------------------------------------------- /app/models/concerns/line_number_formatting.rb: -------------------------------------------------------------------------------- 1 | module LineNumberFormatting 2 | extend ActiveSupport::Concern 3 | 4 | def format_code_with_line_numbers(text) 5 | return "" if text.nil? 6 | 7 | lines = text.lines 8 | numbered_lines = [] 9 | 10 | lines.each_with_index do |line, index| 11 | # Check if line already has line numbers (format: " 123→content") 12 | if line =~ /^\s*(\d+)→(.*)$/ 13 | numbered_lines << { number: $1.to_i, content: $2 } 14 | else 15 | numbered_lines << { number: index + 1, content: line } 16 | end 17 | end 18 | 19 | numbered_lines 20 | end 21 | 22 | def strip_line_numbers(text) 23 | return "" if text.nil? 24 | 25 | text.lines.map do |line| 26 | if line =~ /^\s*\d+→(.*)$/ 27 | $1 28 | else 29 | line 30 | end 31 | end.join 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/views/account_settings/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Change Email/Password

2 | 3 | <%= form_with(model: @user, url: account_settings_path, method: :patch) do |form| %> 4 | <%= form_errors(@user) %> 5 | 6 |
7 | <%= form.label :email_address, "Email Address" %>
8 | <%= form.email_field :email_address %> 9 |
10 | 11 |
12 | <%= form.label :password, "New Password" %>
13 | <%= form.password_field :password, placeholder: "Leave blank to keep current password" %> 14 | Minimum 12 characters 15 |
16 | 17 |
18 | <%= form.label :password_confirmation, "Confirm New Password" %>
19 | <%= form.password_field :password_confirmation %> 20 |
21 | 22 |
23 | <%= form.submit "Update Account", class: "button" %> 24 |
25 | <% end %> 26 | 27 | <%= link_to 'Cancel', user_settings_path %> -------------------------------------------------------------------------------- /app/assets/stylesheets/nav-pull-tab.css: -------------------------------------------------------------------------------- 1 | .nav-pull-tab { 2 | position: absolute; 3 | bottom: -19px; 4 | left: 50%; 5 | transform: translateX(-50%); 6 | width: 80px; 7 | height: 24px; 8 | background-color: var(--bg); 9 | border-radius: 0 0 10px 10px; 10 | cursor: pointer; 11 | z-index: 999; 12 | transition: height 0.15s ease-in-out; 13 | box-shadow: 0 6px 8px -2px rgba(0, 0, 0, 0.15); 14 | } 15 | 16 | .nav-pull-tab::after { 17 | content: ''; 18 | position: absolute; 19 | left: 50%; 20 | bottom: 6px; 21 | transform: translateX(-50%); 22 | width: 30px; 23 | height: 3px; 24 | background-color: var(--text-light); 25 | border-radius: 2px; 26 | opacity: 0.4; 27 | } 28 | 29 | .nav-pull-tab:hover { 30 | height: 29px; 31 | box-shadow: 0 6px 12px -2px rgba(0, 0, 0, 0.2); 32 | } 33 | 34 | @media (max-width: 768px) { 35 | .nav-pull-tab { 36 | display: none; 37 | } 38 | } -------------------------------------------------------------------------------- /test/controllers/user_settings_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserSettingsControllerTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @user = users(:one) 6 | @agent = agents(:one) 7 | login @user 8 | end 9 | 10 | test "should unset auto task naming agent when selecting none" do 11 | @user.update!(auto_task_naming_agent: @agent) 12 | 13 | patch user_settings_path, params: { user: { auto_task_naming_agent_id: "" } } 14 | 15 | assert_redirected_to user_settings_path 16 | assert_nil @user.reload.auto_task_naming_agent_id 17 | end 18 | 19 | test "should set auto task naming agent when selecting an agent" do 20 | patch user_settings_path, params: { user: { auto_task_naming_agent_id: @agent.id } } 21 | 22 | assert_redirected_to user_settings_path 23 | assert_equal @agent.id, @user.reload.auto_task_naming_agent_id 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/docker-controls.css: -------------------------------------------------------------------------------- 1 | .docker-controls { 2 | display: flex; 3 | align-items: center; 4 | gap: 0.5rem; 5 | margin-left: 1rem; 6 | } 7 | 8 | .docker-controls .container-info { 9 | display: flex; 10 | align-items: center; 11 | gap: 0.5rem; 12 | } 13 | 14 | .docker-controls .container-status { 15 | font-size: 0.9rem; 16 | color: var(--text-secondary); 17 | background-color: var(--bg-secondary); 18 | padding: 0.25rem 0.5rem; 19 | border-radius: 4px; 20 | } 21 | 22 | .docker-controls .container-status.-error { 23 | color: var(--color-danger); 24 | background-color: var(--color-danger-bg, #fee); 25 | border: 1px solid var(--color-danger); 26 | } 27 | 28 | .docker-controls .button.-danger { 29 | background-color: var(--color-danger); 30 | color: white; 31 | } 32 | 33 | .docker-controls .button.-danger:hover { 34 | background-color: var(--color-danger-hover); 35 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/run-spinner.css: -------------------------------------------------------------------------------- 1 | .run-spinner { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | gap: 10px; 6 | padding: 20px; 7 | background: #f0f0f0; 8 | border-radius: 4px; 9 | text-align: center; 10 | color: #666; 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | .run-spinner { 15 | background: #2d2d2d; 16 | color: #e6e6e6; 17 | } 18 | } 19 | 20 | .run-spinner > .spinner { 21 | width: 20px; 22 | height: 20px; 23 | border: 2px solid #f3f3f3; 24 | border-top: 2px solid #3498db; 25 | border-radius: 50%; 26 | animation: spin 1s linear infinite; 27 | } 28 | 29 | @media (prefers-color-scheme: dark) { 30 | .run-spinner > .spinner { 31 | border: 2px solid #404040; 32 | border-top: 2px solid #64b5f6; 33 | } 34 | } 35 | 36 | @keyframes spin { 37 | 0% { transform: rotate(0deg); } 38 | 100% { transform: rotate(360deg); } 39 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/panel-divider.css: -------------------------------------------------------------------------------- 1 | .panel-divider { 2 | width: 4px; 3 | margin: 0 10px; 4 | cursor: col-resize; 5 | background: var(--border, #e0e0e0); 6 | display: block; 7 | position: relative; 8 | transition: background-color 0.2s ease; 9 | align-self: stretch; 10 | flex-shrink: 0; 11 | } 12 | 13 | .panel-divider:hover { 14 | background: var(--border-hover, #999); 15 | } 16 | 17 | .panel-divider:active { 18 | background: var(--border-active, #666); 19 | } 20 | 21 | @media (prefers-color-scheme: dark) { 22 | .panel-divider { 23 | background: var(--border-dark, #404040); 24 | } 25 | 26 | .panel-divider:hover { 27 | background: var(--border-hover-dark, #606060); 28 | } 29 | 30 | .panel-divider:active { 31 | background: var(--border-active-dark, #808080); 32 | } 33 | } 34 | 35 | @media (max-width: 768px) { 36 | .panel-divider { 37 | display: none; 38 | } 39 | } -------------------------------------------------------------------------------- /test/models/log_processor/text_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LogProcessor::TextTest < ActiveSupport::TestCase 4 | test "process returns text and result steps" do 5 | processor = LogProcessor::Text.new 6 | logs = "Multi-line\nlog output\nwith text" 7 | result = processor.process(logs) 8 | 9 | assert_equal 2, result.size 10 | assert_equal({ raw_response: logs, type: "Step::Text", content: logs }, result[0]) 11 | assert_equal({ raw_response: logs, type: "Step::Result", content: logs }, result[1]) 12 | end 13 | 14 | test "class method process works" do 15 | logs = "Test log output" 16 | result = LogProcessor::Text.process(logs) 17 | 18 | assert_equal 2, result.size 19 | assert_equal({ raw_response: logs, type: "Step::Text", content: logs }, result[0]) 20 | assert_equal({ raw_response: logs, type: "Step::Result", content: logs }, result[1]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/agent_specific_setting.rb: -------------------------------------------------------------------------------- 1 | class AgentSpecificSetting < ApplicationRecord 2 | belongs_to :agent 3 | 4 | validates :type, presence: true 5 | 6 | def to_partial_path 7 | "agent_specific_settings/#{self.class.name.underscore}" 8 | end 9 | 10 | # Get all available agent-specific setting types 11 | def self.available_types 12 | # Ensure all setting classes are loaded 13 | Rails.application.eager_load! if Rails.env.development? 14 | 15 | descendants.map do |klass| 16 | { 17 | type: klass.name, 18 | display_name: klass.display_name, 19 | description: klass.description 20 | } 21 | end 22 | end 23 | 24 | # Override in subclasses to provide a display name 25 | def self.display_name 26 | name.demodulize.underscore.humanize 27 | end 28 | 29 | # Override in subclasses to provide a description 30 | def self.description 31 | "Enable #{display_name}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /bin/lint_staged: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | staged_rb_files = `git diff --cached --name-only`.split("\n").select { |f| f.end_with?('.rb') } 4 | 5 | unless staged_rb_files.empty? 6 | puts "Running rubocop on staged Ruby files..." 7 | absolute_paths = staged_rb_files.map { |f| File.expand_path(f) } 8 | system("bundle exec rubocop -A #{absolute_paths.join(' ')}") 9 | 10 | # Re-add files that were modified by rubocop 11 | staged_rb_files.each do |file| 12 | system("git add #{file}") if File.exist?(file) 13 | end 14 | end 15 | 16 | # Strip trailing whitespace from all staged files 17 | staged_files = `git diff --cached --name-only --diff-filter=ACM`.split("\n") 18 | staged_files.each do |file| 19 | if File.exist?(file) 20 | content = File.read(file) 21 | new_content = content.gsub(/[ \t]+$/, '') 22 | if content != new_content 23 | File.write(file, new_content) 24 | system("git add #{file}") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/chat-panel.css: -------------------------------------------------------------------------------- 1 | 2 | .chat-panel { 3 | display: flex; 4 | flex-direction: column; 5 | gap: 10px; 6 | } 7 | 8 | #chat-messages { 9 | flex: 1; 10 | } 11 | 12 | @media (min-width: 769px) { 13 | .chat-panel { 14 | max-height: 100vh; 15 | } 16 | 17 | #chat-messages { 18 | overflow-y: auto; 19 | } 20 | } 21 | 22 | .chat-item { 23 | margin-bottom: 10px; 24 | } 25 | 26 | .chat-item > .user-message, 27 | .chat-item > .assistant-message { 28 | padding: 8px; 29 | border-radius: 4px; 30 | margin-bottom: 4px; 31 | } 32 | 33 | .chat-item > .user-message { 34 | background: #e6f2ff; 35 | } 36 | 37 | .chat-item > .assistant-message { 38 | background: #f0f0f0; 39 | } 40 | 41 | @media (prefers-color-scheme: dark) { 42 | .chat-item > .user-message { 43 | background: #0f2a3f; 44 | color: #e6e6e6; 45 | } 46 | 47 | .chat-item > .assistant-message { 48 | background: #2d2d2d; 49 | color: #e6e6e6; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/javascript/controllers/nested_fields_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["fields", "template"] 5 | 6 | connect() { 7 | this.index = this.element.querySelectorAll('.nested-field').length 8 | } 9 | 10 | add(event) { 11 | event.preventDefault() 12 | const content = this.templateTarget.innerHTML.replace(/__INDEX__/g, this.index) 13 | this.fieldsTarget.insertAdjacentHTML('beforeend', content) 14 | this.index++ 15 | } 16 | 17 | remove(event) { 18 | event.preventDefault() 19 | const field = event.target.closest('.nested-field') 20 | 21 | if (field) { 22 | const destroyInput = field.querySelector('input[name*="_destroy"]') 23 | 24 | if (destroyInput) { 25 | destroyInput.value = '1' 26 | field.style.display = 'none' 27 | } else { 28 | field.remove() 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/controllers/user_settings_controller.rb: -------------------------------------------------------------------------------- 1 | class UserSettingsController < ApplicationController 2 | def show 3 | @user = Current.user 4 | end 5 | 6 | def edit 7 | @user = Current.user 8 | end 9 | 10 | def update 11 | @user = Current.user 12 | user_update_params = user_params 13 | 14 | if user_update_params[:github_token].blank? 15 | user_update_params.delete(:github_token) 16 | end 17 | 18 | if user_update_params[:ssh_key].blank? 19 | user_update_params.delete(:ssh_key) 20 | end 21 | 22 | if @user.update(user_update_params) 23 | redirect_to user_settings_path, notice: "Settings updated successfully." 24 | else 25 | render :edit, status: :unprocessable_entity 26 | end 27 | end 28 | 29 | private 30 | 31 | def user_params 32 | params.require(:user).permit(:github_token, :ssh_key, :instructions, :git_config, :allow_github_token_access, :shrimp_mode, :auto_task_naming_agent_id) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/views/pwa/service-worker.js: -------------------------------------------------------------------------------- 1 | // Add a service worker for processing Web Push notifications: 2 | // 3 | // self.addEventListener("push", async (event) => { 4 | // const { title, options } = await event.data.json() 5 | // event.waitUntil(self.registration.showNotification(title, options)) 6 | // }) 7 | // 8 | // self.addEventListener("notificationclick", function(event) { 9 | // event.notification.close() 10 | // event.waitUntil( 11 | // clients.matchAll({ type: "window" }).then((clientList) => { 12 | // for (let i = 0; i < clientList.length; i++) { 13 | // let client = clientList[i] 14 | // let clientPath = (new URL(client.url)).pathname 15 | // 16 | // if (clientPath == event.notification.data.path && "focus" in client) { 17 | // return client.focus() 18 | // } 19 | // } 20 | // 21 | // if (clients.openWindow) { 22 | // return clients.openWindow(event.notification.data.path) 23 | // } 24 | // }) 25 | // ) 26 | // }) 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/auto-push.css: -------------------------------------------------------------------------------- 1 | .auto-push { 2 | display: inline-flex; 3 | align-items: center; 4 | gap: 8px; 5 | margin-top: 10px; 6 | } 7 | 8 | .auto-push > .label { 9 | margin: 0; 10 | font-weight: normal; 11 | } 12 | 13 | .auto-push > .selector { 14 | display: inline-block; 15 | } 16 | 17 | .auto-push .select { 18 | padding: 4px 8px; 19 | border-radius: 4px; 20 | border: 1px solid #ccc; 21 | } 22 | 23 | .auto-push > .button { 24 | padding: 4px 8px; 25 | font-size: 14px; 26 | } 27 | 28 | @media (max-width: 768px) { 29 | .auto-push { 30 | flex-wrap: wrap; 31 | gap: 12px; 32 | width: 100%; 33 | } 34 | 35 | .auto-push > .label { 36 | width: 100%; 37 | } 38 | 39 | .auto-push > .selector { 40 | flex: 1; 41 | min-width: 0; 42 | } 43 | 44 | .auto-push .select { 45 | width: 100%; 46 | } 47 | 48 | .auto-push > .button { 49 | flex-shrink: 0; 50 | } 51 | 52 | .auto-push > .button[type="submit"] { 53 | width: auto; 54 | } 55 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/oauth-login.css: -------------------------------------------------------------------------------- 1 | .oauth-login { 2 | & > .instructions { 3 | max-width: 600px; 4 | margin: 2rem 0; 5 | } 6 | 7 | & > .instructions > .url-container { 8 | margin: 2rem 0; 9 | padding: 1rem; 10 | background: #f5f5f5; 11 | border-radius: 4px; 12 | } 13 | 14 | & > .instructions > .url-container > .url { 15 | width: 100%; 16 | font-family: monospace; 17 | font-size: 0.9em; 18 | margin: 0.5rem 0; 19 | padding: 0.5rem; 20 | border: 1px solid #ddd; 21 | resize: vertical; 22 | } 23 | 24 | & .field { 25 | margin: 1rem 0; 26 | } 27 | 28 | & .field > label { 29 | display: block; 30 | margin-bottom: 0.5rem; 31 | font-weight: bold; 32 | } 33 | 34 | & .field input[type="text"] { 35 | width: 100%; 36 | padding: 0.5rem; 37 | font-size: 1rem; 38 | border: 1px solid #ddd; 39 | border-radius: 4px; 40 | } 41 | 42 | & .actions { 43 | margin-top: 2rem; 44 | display: flex; 45 | gap: 1rem; 46 | } 47 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/codex-layout.css: -------------------------------------------------------------------------------- 1 | .codex-layout { 2 | display: flex; 3 | gap: 0; 4 | position: relative; 5 | height: calc(100vh - 200px); 6 | align-items: stretch; 7 | } 8 | 9 | .codex-layout > .chat-panel { 10 | width: 40%; 11 | flex-shrink: 0; 12 | } 13 | 14 | .codex-layout > .log-panel { 15 | width: 60%; 16 | flex-shrink: 0; 17 | } 18 | 19 | 20 | @media (max-width: 768px) { 21 | .codex-layout { 22 | flex-direction: column; 23 | } 24 | 25 | 26 | .codex-layout > .chat-panel, 27 | .codex-layout > .log-panel { 28 | display: none; 29 | width: 100%; 30 | } 31 | 32 | .codex-layout > .chat-panel.-active, 33 | .codex-layout > .log-panel.-active { 34 | display: block; 35 | width: 100%; 36 | } 37 | 38 | .chat-tab { 39 | display: inline-block; 40 | } 41 | } 42 | 43 | @media (min-width: 769px) { 44 | .codex-layout > .log-panel { 45 | overflow-y: auto; 46 | } 47 | .chat-tab { 48 | display: none; 49 | } 50 | .codex-layout > .nav { 51 | display: none; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/models/step/todo_write.rb: -------------------------------------------------------------------------------- 1 | require "ostruct" 2 | 3 | class Step::TodoWrite < Step::ToolCall 4 | def to_partial_path 5 | "step/todo_writes/todo_write" 6 | end 7 | 8 | def todos 9 | todos_data = tool_inputs&.dig("todos") || [] 10 | todos_data.map do |todo| 11 | OpenStruct.new( 12 | id: todo["id"], 13 | content: todo["content"], 14 | status: todo["status"], 15 | priority: todo["priority"] 16 | ) 17 | end 18 | end 19 | 20 | def pending_todos 21 | todos.select { |t| t.status == "pending" } 22 | end 23 | 24 | def in_progress_todos 25 | todos.select { |t| t.status == "in_progress" } 26 | end 27 | 28 | def completed_todos 29 | todos.select { |t| t.status == "completed" } 30 | end 31 | 32 | def total_count 33 | todos.count 34 | end 35 | 36 | def completed_count 37 | completed_todos.count 38 | end 39 | 40 | def progress_percentage 41 | return 0 if total_count.zero? 42 | (completed_count.to_f / total_count * 100).round 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/views/step/grep_tools/_grep_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 🔎 4 | Content Search 5 | <%= grep_tool.tool_inputs&.dig("pattern") || grep_tool.content %> 6 |
7 | 8 |
9 | <% if grep_tool.tool_inputs&.dig("include") %> 10 |
11 | File filter: 12 | <%= grep_tool.tool_inputs["include"] %> 13 |
14 | <% end %> 15 | 16 | <% if grep_tool.tool_inputs&.dig("path") %> 17 |
18 | In directory: 19 | <%= grep_tool.tool_inputs["path"] %> 20 |
21 | <% end %> 22 |
23 | 24 | <%= render "step/tool_calls/tool_result_section", tool_call: grep_tool %> 25 |
-------------------------------------------------------------------------------- /app/assets/stylesheets/diff-controls.css: -------------------------------------------------------------------------------- 1 | .diff-controls { 2 | margin-bottom: 1rem; 3 | padding: 0.75rem; 4 | background-color: var(--surface-secondary); 5 | border-radius: 4px; 6 | border: 1px solid var(--border); 7 | display: flex; 8 | align-items: center; 9 | gap: 1rem; 10 | } 11 | 12 | .diff-toggle { 13 | display: flex; 14 | align-items: center; 15 | gap: 0.5rem; 16 | cursor: pointer; 17 | color: var(--text); 18 | } 19 | 20 | .diff-toggle input[type="checkbox"] { 21 | margin: 0; 22 | cursor: pointer; 23 | } 24 | 25 | .diff-toggle-label { 26 | cursor: pointer; 27 | user-select: none; 28 | } 29 | 30 | .diff-select { 31 | padding: 0.375rem 0.75rem; 32 | border: 1px solid var(--border); 33 | border-radius: 4px; 34 | background-color: var(--surface); 35 | color: var(--text); 36 | cursor: pointer; 37 | font-size: 0.875rem; 38 | } 39 | 40 | .diff-select:hover { 41 | border-color: var(--border-hover); 42 | } 43 | 44 | .diff-select:focus { 45 | outline: 2px solid var(--color-primary); 46 | outline-offset: 2px; 47 | } -------------------------------------------------------------------------------- /app/javascript/controllers/json_array_fields_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["fields", "hiddenField", "template"] 5 | static values = { fieldName: String } 6 | 7 | connect() { 8 | this.updateHiddenField() 9 | } 10 | 11 | add(event) { 12 | event.preventDefault() 13 | 14 | const template = this.templateTarget 15 | const clone = template.content.cloneNode(true) 16 | 17 | this.fieldsTarget.appendChild(clone) 18 | this.updateHiddenField() 19 | } 20 | 21 | remove(event) { 22 | event.preventDefault() 23 | event.target.closest('.nested-field').remove() 24 | this.updateHiddenField() 25 | } 26 | 27 | updateValue() { 28 | this.updateHiddenField() 29 | } 30 | 31 | updateHiddenField() { 32 | const inputs = this.fieldsTarget.querySelectorAll('input[type="text"]') 33 | const values = Array.from(inputs).map(input => input.value).filter(v => v !== '') 34 | this.hiddenFieldTarget.value = JSON.stringify(values) 35 | } 36 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/scroll-to-top.css: -------------------------------------------------------------------------------- 1 | .scroll-to-top { 2 | position: fixed; 3 | bottom: 20px; 4 | right: 20px; 5 | width: 50px; 6 | height: 50px; 7 | border-radius: 50%; 8 | background-color: var(--accent); 9 | color: white; 10 | border: none; 11 | font-size: 24px; 12 | cursor: pointer; 13 | opacity: 0; 14 | visibility: hidden; 15 | transition: opacity 0.3s, visibility 0.3s; 16 | z-index: 1000; 17 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 18 | } 19 | 20 | .scroll-to-top:hover { 21 | background-color: var(--accent-hover); 22 | transform: translateY(-2px); 23 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 24 | } 25 | 26 | .scroll-to-top.visible { 27 | opacity: 1; 28 | visibility: visible; 29 | } 30 | 31 | .panel-scroll-to-top { 32 | position: absolute; 33 | bottom: 20px; 34 | right: 20px; 35 | } 36 | 37 | @media (max-width: 768px) { 38 | .scroll-to-top { 39 | bottom: 15px; 40 | right: 15px; 41 | width: 45px; 42 | height: 45px; 43 | font-size: 20px; 44 | } 45 | 46 | .panel-scroll-to-top { 47 | display: none; 48 | } 49 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # Temporary files generated by your text editor or operating system 4 | # belong in git's global ignore instead: 5 | # `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | # Ignore pidfiles, but keep the directory. 20 | /tmp/pids/* 21 | !/tmp/pids/ 22 | !/tmp/pids/.keep 23 | 24 | # Ignore storage (uploaded files in development and any SQLite databases). 25 | /storage/* 26 | !/storage/.keep 27 | /tmp/storage/* 28 | !/tmp/storage/ 29 | !/tmp/storage/.keep 30 | 31 | /public/assets 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | 36 | # Devenv 37 | /.direnv 38 | /.envrc 39 | .devenv* 40 | devenv.local.nix 41 | vendor/bundle 42 | 43 | # Local worktrees used by parallel agents 44 | /worktrees/* 45 | !/worktrees/.keep 46 | 47 | .claude/hook_log.txt -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :nav do %> 2 |
3 | <% end %> 4 | 5 |
6 |
7 | <%= image_tag "logo.png", alt: "SummonCircle" %> 8 |
9 | How embarrassing... I don't have a logo for dark mode... 10 |
11 |
12 | 13 | <%= form_with url: session_path do |form| %> 14 | <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %> 15 | <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %> 16 | <%= form.submit "Sign in" %> 17 | 18 | <%= link_to "Forgot password?", new_password_path, class: "forgot-password" %> 19 | <% end %> 20 | 21 | <% if Rails.env.development? %> 22 |
23 | <%= button_to "Dev Sign In (admin)", session_path, params: { email_address: "dev@example.com", password: "password" } %> 24 |
25 | <% end %> 26 |
27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/ls-tool.css: -------------------------------------------------------------------------------- 1 | .ls-tool { 2 | background-color: var(--bg-ls-tool); 3 | color: var(--text); 4 | padding: 0.75rem 1rem; 5 | border-radius: 4px; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 7 | font-size: 0.875rem; 8 | line-height: 1.6; 9 | margin: 0.5rem 0; 10 | border-left: 3px solid var(--border-ls); 11 | } 12 | 13 | .ls-tool > .tool-header { 14 | display: flex; 15 | align-items: center; 16 | gap: 0.5rem; 17 | margin-bottom: 0.5rem; 18 | background-color: var(--bg-ls-header); 19 | margin: -0.75rem -1rem 0.75rem -1rem; 20 | padding: 0.5rem 1rem; 21 | border-radius: 4px 4px 0 0; 22 | } 23 | 24 | .ls-tool > .tool-header > .tool-icon { 25 | font-size: 1.1rem; 26 | color: var(--color-ls-icon); 27 | } 28 | 29 | .ls-tool > .tool-header > .tool-label { 30 | font-weight: 600; 31 | color: var(--color-ls-icon); 32 | } 33 | 34 | .ls-tool > .tool-header > .file-path { 35 | margin-left: auto; 36 | font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; 37 | font-size: 0.8125rem; 38 | color: var(--text-secondary); 39 | } -------------------------------------------------------------------------------- /app/views/tasks/new.html.erb: -------------------------------------------------------------------------------- 1 |

New Task

2 | 3 | <%= form_with(model: [@project, @task], data: { controller: "branch-select", branch_select_project_id_value: @project.id, branch_select_hidden_class: "_hidden" }) do |form| %> 4 | <%= form_errors(@task) %> 5 | 6 |
7 | <%= form.label :agent_id, "Agent" %>
8 | <%= form.collection_select :agent_id, Agent.kept, :id, :name %> 9 |
10 | 11 |
12 | <%= form.label :target_branch, "Target Branch" %>
13 | <%= form.select :target_branch, [], {}, { data: { branch_select_target: "select" } } %> 14 | Loading branches... 15 |
16 | 17 | <%= form.fields_for :runs do |run_form| %> 18 |
19 | <%= run_form.label :prompt %>
20 | <%= run_form.text_area :prompt, required: true, data: { prompt_target: "textarea", action: "keydown->prompt#keydown" } %> 21 |
22 | <% end %> 23 | 24 |
25 | <%= form.submit "Launch Task" %> 26 |
27 | <% end %> 28 | 29 | <%= link_to 'Back', project_tasks_path(@project) %> 30 | -------------------------------------------------------------------------------- /.claude/bin/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | 5 | # Check if stdin contains git push commands 6 | if STDIN.tty? 7 | # No stdin, don't run checks 8 | exit 0 9 | else 10 | # Read stdin 11 | stdin_content = STDIN.read 12 | 13 | begin 14 | # Parse the JSON 15 | data = JSON.parse(stdin_content) 16 | 17 | # Check if it's a git push command or mcp__github__push_files 18 | if data['tool_name'] == 'Bash' && data['tool_input'] && data['tool_input']['command'] && data['tool_input']['command'].match(/^git\s+push/) 19 | system("$(git rev-parse --show-toplevel)/bin/checks") 20 | unless $?.success? 21 | $stderr.puts "\nPre-push checks failed! Run 'bin/checks' to see what went wrong." 22 | exit 2 23 | end 24 | elsif data['tool_name'] == 'mcp__github__push_files' 25 | system("$(git rev-parse --show-toplevel)/bin/checks") 26 | unless $?.success? 27 | $stderr.puts "\nPre-push checks failed! Run 'bin/checks' to see what went wrong." 28 | exit 2 29 | end 30 | else 31 | # Not a git push, don't run checks 32 | exit 0 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | STDOUT.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /app/models/step/tool_call.rb: -------------------------------------------------------------------------------- 1 | class Step::ToolCall < Step 2 | has_one :tool_result, class_name: "Step::ToolResult", foreign_key: :tool_call_id 3 | 4 | def tool_id 5 | parsed = parsed_response 6 | return nil unless parsed.is_a?(Hash) 7 | 8 | content_array = parsed.dig("message", "content") 9 | return nil unless content_array.is_a?(Array) 10 | 11 | tool_use = content_array.find { |c| c["type"] == "tool_use" } 12 | tool_use&.dig("id") 13 | end 14 | 15 | def tool_name 16 | parsed = parsed_response 17 | return nil unless parsed.is_a?(Hash) 18 | 19 | content_array = parsed.dig("message", "content") 20 | return nil unless content_array.is_a?(Array) 21 | 22 | tool_use = content_array.find { |c| c["type"] == "tool_use" } 23 | tool_use&.dig("name") 24 | end 25 | 26 | def tool_inputs 27 | parsed = parsed_response 28 | return nil unless parsed.is_a?(Hash) 29 | 30 | content_array = parsed.dig("message", "content") 31 | return nil unless content_array.is_a?(Array) 32 | 33 | tool_use = content_array.find { |c| c["type"] == "tool_use" } 34 | tool_use&.dig("input") 35 | end 36 | 37 | def pending? 38 | tool_result.nil? 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class PasswordsController < ApplicationController 2 | allow_unauthenticated_access 3 | before_action :set_user_by_token, only: %i[ edit update ] 4 | 5 | def new 6 | @smtp_configured = Rails.application.config.action_mailer.smtp_settings.present? 7 | end 8 | 9 | def create 10 | if user = User.find_by(email_address: params[:email_address]) 11 | PasswordsMailer.reset(user).deliver_later 12 | end 13 | 14 | redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." 15 | end 16 | 17 | def edit 18 | end 19 | 20 | def update 21 | if @user.update(params.permit(:password, :password_confirmation)) 22 | redirect_to new_session_path, notice: "Password has been reset." 23 | else 24 | redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." 25 | end 26 | end 27 | 28 | private 29 | def set_user_by_token 30 | @user = User.find_by_password_reset_token!(params[:token]) 31 | rescue ActiveSupport::MessageVerifier::InvalidSignature 32 | redirect_to new_password_path, alert: "Password reset link is invalid or has expired." 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/websearch-tool.css: -------------------------------------------------------------------------------- 1 | .websearch-tool { 2 | background-color: var(--bg-websearch-tool); 3 | color: var(--text); 4 | padding: 0.75rem 1rem; 5 | border-radius: 4px; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 7 | font-size: 0.875rem; 8 | line-height: 1.6; 9 | margin: 0.5rem 0; 10 | border-left: 3px solid var(--border-websearch); 11 | } 12 | 13 | .websearch-tool > .tool-header { 14 | display: flex; 15 | align-items: center; 16 | gap: 0.5rem; 17 | margin-bottom: 0.5rem; 18 | background-color: var(--bg-websearch-header); 19 | margin: -0.75rem -1rem 0.75rem -1rem; 20 | padding: 0.5rem 1rem; 21 | border-radius: 4px 4px 0 0; 22 | } 23 | 24 | .websearch-tool > .tool-header > .tool-icon { 25 | font-size: 1.1rem; 26 | color: var(--color-websearch-icon); 27 | } 28 | 29 | .websearch-tool > .tool-header > .tool-label { 30 | font-weight: 600; 31 | color: var(--color-websearch-icon); 32 | } 33 | 34 | .websearch-tool > .tool-header > .query { 35 | margin-left: auto; 36 | font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; 37 | font-size: 0.8125rem; 38 | color: var(--text-secondary); 39 | font-weight: 500; 40 | } -------------------------------------------------------------------------------- /app/jobs/remove_docker_container_job.rb: -------------------------------------------------------------------------------- 1 | class RemoveDockerContainerJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(task) 5 | return unless task.container_id.present? 6 | 7 | container = Docker::Container.get(task.container_id) 8 | 9 | begin 10 | container.stop(t: 5) 11 | rescue => e 12 | Rails.logger.warn "Failed to stop container gracefully: #{e.message}" 13 | end 14 | 15 | container.delete(force: true) 16 | 17 | task.update!( 18 | container_id: nil, 19 | container_name: nil, 20 | container_status: nil 21 | ) 22 | 23 | broadcast_docker_status(task) 24 | rescue Docker::Error::NotFoundError 25 | task.update!( 26 | container_id: nil, 27 | container_name: nil, 28 | container_status: nil 29 | ) 30 | broadcast_docker_status(task) 31 | rescue => e 32 | Rails.logger.error "Failed to remove container: #{e.message}" 33 | raise 34 | end 35 | 36 | private 37 | 38 | def broadcast_docker_status(task) 39 | Turbo::StreamsChannel.broadcast_replace_to( 40 | task, 41 | target: "docker_controls", 42 | partial: "tasks/docker_controls", 43 | locals: { task: task } 44 | ) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: "ON" 29 | 30 | en: 31 | hello: "Hello world" 32 | application: 33 | form_errors: 34 | title: 35 | one: An error prohibited this %{name} from being saved 36 | other: "%{count} errors prohibited this %{name} from being saved" 37 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-connect.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-connect check 4 | # 5 | # Warms DNS before connecting to hosts in parallel 6 | # 7 | # These environment variables are available: 8 | # KAMAL_RECORDED_AT 9 | # KAMAL_PERFORMER 10 | # KAMAL_VERSION 11 | # KAMAL_HOSTS 12 | # KAMAL_ROLES (if set) 13 | # KAMAL_DESTINATION (if set) 14 | # KAMAL_RUNTIME 15 | 16 | hosts = ENV["KAMAL_HOSTS"].split(",") 17 | results = nil 18 | max = 3 19 | 20 | elapsed = Benchmark.realtime do 21 | results = hosts.map do |host| 22 | Thread.new do 23 | tries = 1 24 | 25 | begin 26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) 27 | rescue SocketError 28 | if tries < max 29 | puts "Retrying DNS warmup: #{host}" 30 | tries += 1 31 | sleep rand 32 | retry 33 | else 34 | puts "DNS warmup failed: #{host}" 35 | host 36 | end 37 | end 38 | 39 | tries 40 | end 41 | end.map(&:value) 42 | end 43 | 44 | retries = results.sum - hosts.size 45 | nopes = results.count { |r| r == max } 46 | 47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/header-overlay.css: -------------------------------------------------------------------------------- 1 | .header-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | z-index: 1000; 7 | transition: transform 0.15s ease-in-out; 8 | background-color: var(--bg); 9 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 10 | } 11 | 12 | .header-overlay .task-info { 13 | padding: 0.5rem 1rem; 14 | border-top: 1px solid var(--border); 15 | } 16 | 17 | .header-overlay .task-info h1 { 18 | margin: 0.5rem 0; 19 | font-size: 1.5rem; 20 | } 21 | 22 | .header-overlay .task-details { 23 | display: flex; 24 | gap: 2rem; 25 | font-size: 0.9rem; 26 | color: var(--text-light); 27 | margin-bottom: 0.5rem; 28 | } 29 | 30 | .header-overlay .task-details span { 31 | white-space: nowrap; 32 | } 33 | 34 | .header-overlay #auto_push_form { 35 | margin-top: 0.5rem; 36 | padding-bottom: 0.5rem; 37 | } 38 | 39 | .header-overlay .auto-push { 40 | margin-top: 0; 41 | } 42 | 43 | .task-actions-header { 44 | margin-top: 0.5rem; 45 | margin-bottom: 0.5rem; 46 | } 47 | 48 | .task-actions-header .button { 49 | padding: 0.5rem 1rem; 50 | font-size: 0.9rem; 51 | } 52 | 53 | @media (max-width: 768px) { 54 | .header-overlay { 55 | position: relative; 56 | transform: none !important; 57 | } 58 | } -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/support/docker_test_helper.rb: -------------------------------------------------------------------------------- 1 | module DockerTestHelper 2 | # Create Docker log output with proper binary stream format 3 | # Docker format: [stream_type(1)][reserved(3)][size(4)][data(size)] 4 | def docker_log_output(content, stream_type: 1) 5 | return "" if content.nil? 6 | 7 | # Stream type: 1 = stdout, 2 = stderr 8 | header = [ stream_type, 0, 0, 0 ].pack("C*") # 4 bytes 9 | size = [ content.bytesize ].pack("N") # 4 bytes big-endian 10 | header + size + content 11 | end 12 | 13 | # Mock a Docker container that returns output with proper headers 14 | def mock_container_with_output(output, status_code: 0) 15 | container = mock("container") 16 | container.expects(:start) 17 | container.expects(:wait).returns({ "StatusCode" => status_code }) 18 | container.expects(:logs).with(stdout: true, stderr: true).returns(docker_log_output(output)) 19 | container.expects(:delete).with(force: true) 20 | 21 | # Support for exec calls (used by SSH setup) 22 | container.expects(:exec).with(anything).at_least(0) 23 | 24 | container 25 | end 26 | 27 | # Mock a simple Docker container with "Success" output 28 | def mock_container(status_code: 0) 29 | mock_container_with_output("Success", status_code: status_code) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/step/write_tools/_write_tool.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 📝 4 | Write File 5 | <%= write_tool.file_path %> 6 |
7 | 8 | <% if write_tool.file_content %> 9 |
10 |
11 |
12 | <% lines = write_tool.format_code_with_line_numbers(write_tool.file_content) %> 13 | <% display_lines = lines.first(10) %> 14 | <% display_lines.each do |line_data| %> 15 |
16 | <%= line_data[:number] %> 17 | <%= line_data[:content] %> 18 |
19 | <% end %> 20 | <% if lines.size > 10 %> 21 |
22 | ... (<%= lines.size - 10 %> more lines) 23 |
24 | <% end %> 25 |
26 |
27 |
28 | <% end %> 29 | 30 | <%= render "step/write_tools/write_tool_result", write_tool: write_tool %> 31 |
-------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. 2 | 3 | # Ignore git directory. 4 | /.git/ 5 | /.gitignore 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all default key files. 14 | /config/master.key 15 | /config/credentials/*.key 16 | 17 | # Ignore all logfiles and tempfiles. 18 | /log/* 19 | /tmp/* 20 | !/log/.keep 21 | !/tmp/.keep 22 | 23 | # Ignore pidfiles, but keep the directory. 24 | /tmp/pids/* 25 | !/tmp/pids/.keep 26 | 27 | # Ignore storage (uploaded files in development and any SQLite databases). 28 | /storage/* 29 | !/storage/.keep 30 | /tmp/storage/* 31 | !/tmp/storage/.keep 32 | 33 | # Ignore assets. 34 | /node_modules/ 35 | /app/assets/builds/* 36 | !/app/assets/builds/.keep 37 | /public/assets 38 | 39 | # Ignore CI service files. 40 | /.github 41 | 42 | # Ignore Kamal files. 43 | /config/deploy*.yml 44 | /.kamal 45 | 46 | # Ignore development files 47 | /.devcontainer 48 | 49 | # Ignore Docker-related files 50 | /.dockerignore 51 | /Dockerfile* 52 | 53 | # Devenv 54 | /.direnv 55 | /.envrc 56 | .devenv* 57 | devenv.local.nix 58 | vendor/bundle 59 | 60 | # Local worktrees used by parallel agents 61 | /worktrees/* 62 | !/worktrees/.keep 63 | 64 | .claude/hook_log.txt -------------------------------------------------------------------------------- /app/models/concerns/docker_stream_processor.rb: -------------------------------------------------------------------------------- 1 | module DockerStreamProcessor 2 | extend ActiveSupport::Concern 3 | 4 | private 5 | 6 | def process_docker_stream(raw_logs) 7 | return "" if raw_logs.nil? || raw_logs.empty? 8 | 9 | # Force to binary encoding to handle the stream properly 10 | logs = raw_logs.dup.force_encoding(Encoding::ASCII_8BIT) 11 | output = [] 12 | offset = 0 13 | 14 | while offset < logs.bytesize 15 | # Need at least 8 bytes for the header 16 | break if offset + 8 > logs.bytesize 17 | 18 | # Read the 8-byte header 19 | # Format: stream_type(1) + reserved(3) + size(4) 20 | header = logs.byteslice(offset, 8) 21 | stream_type = header.getbyte(0) 22 | size = header.byteslice(4, 4).unpack1("N") # big-endian 32-bit 23 | 24 | # Move past the header 25 | offset += 8 26 | 27 | # Read the data chunk 28 | break if offset + size > logs.bytesize 29 | chunk = logs.byteslice(offset, size) 30 | 31 | # Add to output if it's stdout (stream_type 1) or stderr (stream_type 2) 32 | if stream_type == 1 || stream_type == 2 33 | output << chunk.force_encoding("UTF-8").scrub 34 | end 35 | 36 | # Move to next chunk 37 | offset += size 38 | end 39 | 40 | output.join.strip 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/fixtures/runs.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | task: one 5 | prompt: echo hello 6 | status: 2 # completed 7 | started_at: 2025-05-24 19:13:02 8 | completed_at: 2025-05-24 19:13:02 9 | 10 | two: 11 | task: one 12 | prompt: echo world 13 | status: 2 # completed 14 | started_at: 2025-05-24 19:13:03 15 | completed_at: 2025-05-24 19:13:03 16 | 17 | pending: 18 | task: one 19 | prompt: echo pending 20 | status: 0 # pending 21 | started_at: null 22 | completed_at: null 23 | 24 | # Runs for task_with_runs fixture 25 | existing_run: 26 | task: task_with_runs 27 | prompt: echo hello 28 | status: 2 # completed 29 | started_at: 2025-05-24 19:13:02 30 | completed_at: 2025-05-24 19:13:02 31 | 32 | another_run: 33 | task: task_with_runs 34 | prompt: echo world 35 | status: 2 # completed 36 | started_at: 2025-05-24 19:13:03 37 | completed_at: 2025-05-24 19:13:03 38 | 39 | pending_run: 40 | task: task_with_runs 41 | prompt: echo pending 42 | status: 0 # pending 43 | started_at: null 44 | completed_at: null 45 | 46 | mcp_existing_run: 47 | task: with_mcp_endpoint_has_runs 48 | prompt: echo first 49 | status: 2 # completed 50 | started_at: 2025-05-24 19:13:02 51 | completed_at: 2025-05-24 19:13:02 52 | -------------------------------------------------------------------------------- /.kamal/hooks/pre-build.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample pre-build hook 4 | # 5 | # Checks: 6 | # 1. We have a clean checkout 7 | # 2. A remote is configured 8 | # 3. The branch has been pushed to the remote 9 | # 4. The version we are deploying matches the remote 10 | # 11 | # These environment variables are available: 12 | # KAMAL_RECORDED_AT 13 | # KAMAL_PERFORMER 14 | # KAMAL_VERSION 15 | # KAMAL_HOSTS 16 | # KAMAL_ROLES (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | if [ -n "$(git status --porcelain)" ]; then 20 | echo "Git checkout is not clean, aborting..." >&2 21 | git status --porcelain >&2 22 | exit 1 23 | fi 24 | 25 | first_remote=$(git remote) 26 | 27 | if [ -z "$first_remote" ]; then 28 | echo "No git remote set, aborting..." >&2 29 | exit 1 30 | fi 31 | 32 | current_branch=$(git branch --show-current) 33 | 34 | if [ -z "$current_branch" ]; then 35 | echo "Not on a git branch, aborting..." >&2 36 | exit 1 37 | fi 38 | 39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) 40 | 41 | if [ -z "$remote_head" ]; then 42 | echo "Branch not pushed to remote, aborting..." >&2 43 | exit 1 44 | fi 45 | 46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then 47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /app/assets/stylesheets/form-errors.css: -------------------------------------------------------------------------------- 1 | .form-errors { 2 | color: var(--color-danger); 3 | margin-top: 0.5rem; 4 | margin-bottom: 1rem; 5 | } 6 | 7 | .form-errors > .title { 8 | font-size: 1.1rem; 9 | margin-bottom: 0.5rem; 10 | color: var(--color-danger); 11 | } 12 | 13 | .form-errors > .list { 14 | list-style-type: disc; 15 | padding-left: 1.5rem; 16 | margin: 0; 17 | } 18 | 19 | .form-errors > .list > .item { 20 | margin-bottom: 0.25rem; 21 | } 22 | 23 | /* Legacy support for existing error containers */ 24 | .errors, 25 | #error_explanation { 26 | color: var(--color-danger); 27 | margin-top: 0.5rem; 28 | margin-bottom: 1rem; 29 | } 30 | 31 | .errors h2, 32 | #error_explanation h2 { 33 | font-size: 1.1rem; 34 | margin-bottom: 0.5rem; 35 | color: var(--color-danger); 36 | } 37 | 38 | .errors ul, 39 | #error_explanation ul { 40 | list-style-type: disc; 41 | padding-left: 1.5rem; 42 | margin: 0; 43 | } 44 | 45 | .errors li, 46 | #error_explanation li { 47 | margin-bottom: 0.25rem; 48 | } 49 | 50 | /* Rails field_with_errors wrapper */ 51 | .field_with_errors { 52 | display: inline; 53 | } 54 | 55 | .field_with_errors label { 56 | color: var(--color-danger); 57 | } 58 | 59 | .field_with_errors input, 60 | .field_with_errors textarea, 61 | .field_with_errors select { 62 | border-color: var(--color-danger); 63 | } -------------------------------------------------------------------------------- /db/cable_schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[8.0].define(version: 1) do 14 | create_table "solid_cable_messages", force: :cascade do |t| 15 | t.binary "channel", limit: 1024, null: false 16 | t.binary "payload", limit: 536870912, null: false 17 | t.datetime "created_at", null: false 18 | t.integer "channel_hash", limit: 8, null: false 19 | t.index [ "channel" ], name: "index_solid_cable_messages_on_channel" 20 | t.index [ "channel_hash" ], name: "index_solid_cable_messages_on_channel_hash" 21 | t.index [ "created_at" ], name: "index_solid_cable_messages_on_created_at" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/task-show-overlay.css: -------------------------------------------------------------------------------- 1 | /* Layout adjustments specific to tasks#show when using header overlay */ 2 | 3 | @media (min-width: 769px) { 4 | body.tasks.show main { 5 | padding-top: 0 !important; 6 | height: 100vh; 7 | overflow: hidden; 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | body.tasks.show .codex-layout { 13 | flex: 1; 14 | overflow: hidden; 15 | display: flex; 16 | gap: 0; 17 | padding: 1rem; 18 | height: 100%; 19 | } 20 | 21 | body.tasks.show .chat-panel, 22 | body.tasks.show .log-panel { 23 | overflow-y: auto; 24 | height: 100%; 25 | padding: 0 16px; 26 | } 27 | 28 | body.tasks.show .task-actions { 29 | position: fixed; 30 | bottom: 1rem; 31 | right: 1rem; 32 | background-color: var(--bg); 33 | padding: 0.5rem; 34 | border-radius: 4px; 35 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 36 | } 37 | } 38 | 39 | @media (max-width: 768px) { 40 | body.tasks.show main { 41 | height: auto !important; 42 | overflow: visible !important; 43 | } 44 | 45 | body.tasks.show .codex-layout { 46 | height: auto !important; 47 | padding: 1rem !important; 48 | } 49 | 50 | body.tasks.show .chat-panel.-active, 51 | body.tasks.show .log-panel.-active { 52 | height: auto; 53 | overflow: visible; 54 | } 55 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | branches: [main] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | deploy: 12 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 13 | runs-on: ubuntu-latest 14 | environment: production 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | with: 20 | repository: ${{ github.event.workflow_run.head_repository.full_name }} 21 | ref: ${{ github.event.workflow_run.head_sha }} 22 | 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: .ruby-version 27 | bundler-cache: true 28 | 29 | - name: Tailscale 30 | uses: tailscale/github-action@v3 31 | with: 32 | oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 33 | oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 34 | tags: tag:gh-action-summoncircle 35 | 36 | - name: Install 1Password CLI 37 | uses: 1password/install-cli-action@v1 38 | 39 | - name: Deploy with Kamal 40 | env: 41 | OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} 42 | OP_ACCOUNT: ${{ secrets.OP_ACCOUNT }} 43 | OP_SECRETS: ${{ secrets.OP_SECRETS }} 44 | run: bin/kamal deploy -------------------------------------------------------------------------------- /app/views/tasks/_actions_header.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to 'Archive', task_path(task), 3 | data: { turbo_method: :delete, turbo_confirm: 'Are you sure you want to archive this task?' }, 4 | class: 'button archive' %> 5 | <%= link_to 'Download Repository', task_repository_download_path(task), class: 'button', data: { turbo: false } %> 6 | <% if (repo_state = task.latest_repo_state) %> 7 | <% diff_content = repo_state.git_diff.presence || repo_state.target_branch_diff.presence || repo_state.uncommitted_diff %> 8 | <% if diff_content.present? %> 9 |
10 | 11 | 14 |
15 |
16 | 17 | 20 |
21 | <% end %> 22 | <% end %> 23 |
-------------------------------------------------------------------------------- /db/cache_schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[8.0].define(version: 1) do 14 | create_table "solid_cache_entries", force: :cascade do |t| 15 | t.binary "key", limit: 1024, null: false 16 | t.binary "value", limit: 536870912, null: false 17 | t.datetime "created_at", null: false 18 | t.integer "key_hash", limit: 8, null: false 19 | t.integer "byte_size", limit: 4, null: false 20 | t.index [ "byte_size" ], name: "index_solid_cache_entries_on_byte_size" 21 | t.index [ "key_hash", "byte_size" ], name: "index_solid_cache_entries_on_key_hash_and_byte_size" 22 | t.index [ "key_hash" ], name: "index_solid_cache_entries_on_key_hash", unique: true 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/views/claude_oauth/login_start.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Claude OAuth Login

3 | 4 |
5 |

To configure OAuth for <%= @agent.name %>:

6 | 7 |
    8 |
  1. Copy the URL below and paste it into your browser
  2. 9 |
  3. Log in to Anthropic and authorize the application
  4. 10 |
  5. After authorization, you'll be redirected to a page with an authorization code
  6. 11 |
  7. Copy the authorization code and paste it in the form below
  8. 12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 | 20 | <%= form_with url: oauth_login_finish_agent_path(@agent), method: :post, local: true do |form| %> 21 |
22 | <%= form.label :code, "Authorization Code:" %> 23 | <%= form.text_field :code, placeholder: "Paste the authorization code here", required: true %> 24 |
25 | 26 |
27 | <%= form.submit "Complete OAuth Login", class: "button -primary" %> 28 | <%= link_to "Cancel", @agent, class: "button" %> 29 |
30 | <% end %> 31 |
32 |
-------------------------------------------------------------------------------- /test/models/volume_mount_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class VolumeMountTest < ActiveSupport::TestCase 4 | test "generates volume_name for regular volume mount" do 5 | volume_mount = VolumeMount.create!( 6 | volume: volumes(:one), 7 | task: tasks(:two) 8 | ) 9 | 10 | assert_match(/summoncircle_.*_volume_/, volume_mount.volume_name) 11 | end 12 | 13 | test "generates volume_name for workplace mount" do 14 | volume_mount = VolumeMount.create!( 15 | volume: nil, 16 | task: tasks(:one) 17 | ) 18 | 19 | assert_match(/summoncircle_workplace_volume_/, volume_mount.volume_name) 20 | end 21 | 22 | test "container_path returns volume path for regular mount" do 23 | volume_mount = volume_mounts(:one) 24 | assert_equal volume_mount.volume.path, volume_mount.container_path 25 | end 26 | 27 | test "container_path returns workplace_path for workplace mount" do 28 | tasks(:one).agent.update!(workplace_path: "/workspace") 29 | volume_mount = VolumeMount.create!( 30 | volume: nil, 31 | task: tasks(:one) 32 | ) 33 | 34 | assert_equal "/workspace", volume_mount.container_path 35 | end 36 | 37 | test "bind_string returns volume_name:container_path" do 38 | volume_mount = volume_mounts(:one) 39 | expected = "#{volume_mount.volume_name}:#{volume_mount.container_path}" 40 | assert_equal expected, volume_mount.bind_string 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/assets/stylesheets/inline-edit.css: -------------------------------------------------------------------------------- 1 | .inline-edit h1 .form { 2 | display: inline-flex; 3 | align-items: center; 4 | gap: 0.5rem; 5 | font-size: 0.5em; 6 | } 7 | 8 | .inline-edit h1 .form > .input { 9 | font-size: inherit; 10 | font-weight: inherit; 11 | font-family: inherit; 12 | line-height: inherit; 13 | padding: 0.2rem 0.4rem; 14 | border: 1px solid #ccc; 15 | border-radius: 4px; 16 | min-width: 200px; 17 | box-sizing: border-box; 18 | } 19 | 20 | .inline-edit h1 .form > .save { 21 | font-size: inherit; 22 | font-weight: normal; 23 | font-family: inherit; 24 | padding: 0.2rem 0.5rem; 25 | background-color: var(--color-primary); 26 | color: var(--color-white); 27 | border: 1px solid var(--color-primary); 28 | border-radius: 4px; 29 | cursor: pointer; 30 | box-sizing: border-box; 31 | } 32 | 33 | .inline-edit h1 .form > .save:hover { 34 | background-color: var(--color-primary-hover); 35 | } 36 | 37 | .inline-edit h1 .form > .save:disabled { 38 | opacity: 0.6; 39 | cursor: not-allowed; 40 | } 41 | 42 | @media (prefers-color-scheme: dark) { 43 | .inline-edit h1 .form > .input { 44 | background-color: #2d3748; 45 | border-color: #4a5568; 46 | color: #e2e8f0; 47 | } 48 | 49 | .inline-edit h1 .form > .save { 50 | background-color: var(--color-primary-dark); 51 | } 52 | 53 | .inline-edit h1 .form > .save:hover { 54 | background-color: var(--color-primary-dark-hover); 55 | } 56 | } -------------------------------------------------------------------------------- /test/controllers/claude_oauth_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ClaudeOauthControllerTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @agent = agents(:one) 6 | @user = users(:one) 7 | login @user 8 | end 9 | 10 | test "should get login_start" do 11 | # Mock the oauth to avoid Docker calls in tests 12 | ClaudeOauth.any_instance.stubs(:login_start).returns("https://claude.ai/oauth/authorize?test=1") 13 | 14 | get oauth_login_start_agent_url(@agent) 15 | assert_response :success 16 | end 17 | 18 | test "should redirect login_finish without code" do 19 | post oauth_login_finish_agent_url(@agent) 20 | assert_redirected_to @agent 21 | assert_equal "No authorization code provided", flash[:alert] 22 | end 23 | 24 | test "should handle login_finish with code" do 25 | ClaudeOauth.any_instance.stubs(:login_finish).returns(true) 26 | 27 | post oauth_login_finish_agent_url(@agent, code: "test_code") 28 | assert_redirected_to @agent 29 | assert_equal "OAuth login successful!", flash[:notice] 30 | end 31 | 32 | test "should refresh tokens" do 33 | ClaudeOauth.any_instance.stubs(:check_credentials_exist).returns(true) 34 | ClaudeOauth.any_instance.stubs(:refresh_token).returns(true) 35 | 36 | post oauth_refresh_agent_url(@agent) 37 | assert_redirected_to @agent 38 | assert_equal "OAuth tokens refreshed successfully!", flash[:notice] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/assets/stylesheets/webfetch-tool.css: -------------------------------------------------------------------------------- 1 | .webfetch-tool { 2 | background-color: var(--bg-webfetch-tool); 3 | color: var(--text); 4 | padding: 0.75rem 1rem; 5 | border-radius: 4px; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 7 | font-size: 0.875rem; 8 | line-height: 1.6; 9 | margin: 0.5rem 0; 10 | border-left: 3px solid var(--border-webfetch); 11 | } 12 | 13 | .webfetch-tool > .tool-header { 14 | display: flex; 15 | align-items: center; 16 | gap: 0.5rem; 17 | margin-bottom: 0.5rem; 18 | background-color: var(--bg-webfetch-header); 19 | margin: -0.75rem -1rem 0.75rem -1rem; 20 | padding: 0.5rem 1rem; 21 | border-radius: 4px 4px 0 0; 22 | } 23 | 24 | .webfetch-tool > .tool-header > .tool-icon { 25 | font-size: 1.1rem; 26 | color: var(--color-webfetch-icon); 27 | } 28 | 29 | .webfetch-tool > .tool-header > .tool-label { 30 | font-weight: 600; 31 | color: var(--color-webfetch-icon); 32 | } 33 | 34 | .webfetch-tool > .tool-header > .url { 35 | margin-left: auto; 36 | font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; 37 | font-size: 0.8125rem; 38 | color: var(--color-webfetch-url); 39 | text-decoration: underline; 40 | } 41 | 42 | .webfetch-tool > .prompt { 43 | background-color: var(--bg-webfetch-prompt); 44 | padding: 0.5rem; 45 | border-radius: 3px; 46 | font-style: italic; 47 | font-size: 0.8125rem; 48 | margin-bottom: 0.5rem; 49 | } -------------------------------------------------------------------------------- /app/controllers/containers_controller.rb: -------------------------------------------------------------------------------- 1 | class ContainersController < ApplicationController 2 | before_action :set_task 3 | 4 | def create 5 | # Always set building state 6 | @task.update!(container_status: "building") 7 | BuildDockerContainerJob.perform_later(@task) 8 | flash[:notice] = "Building container..." 9 | 10 | respond_to do |format| 11 | format.turbo_stream { 12 | render turbo_stream: [ 13 | turbo_stream.replace("docker_controls", partial: "tasks/docker_controls", locals: { task: @task }), 14 | turbo_stream.prepend("flash-messages", partial: "application/flash_messages") 15 | ] 16 | } 17 | format.html { redirect_to @task } 18 | end 19 | end 20 | 21 | def destroy 22 | # Immediately set removing state 23 | @task.update!(container_status: "removing") 24 | RemoveDockerContainerJob.perform_later(@task) 25 | flash[:notice] = "Removing container..." 26 | 27 | respond_to do |format| 28 | format.turbo_stream { 29 | render turbo_stream: [ 30 | turbo_stream.replace("docker_controls", partial: "tasks/docker_controls", locals: { task: @task }), 31 | turbo_stream.prepend("flash-messages", partial: "application/flash_messages") 32 | ] 33 | } 34 | format.html { redirect_to @task } 35 | end 36 | end 37 | 38 | private 39 | 40 | def set_task 41 | @task = Task.find(params[:task_id]) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/models/project.rb: -------------------------------------------------------------------------------- 1 | class Project < ApplicationRecord 2 | include Discard::Model 3 | 4 | has_many :tasks, dependent: :destroy 5 | has_many :secrets, as: :secretable, dependent: :destroy 6 | has_many :env_variables, as: :envable, dependent: :destroy, class_name: "EnvVariable" 7 | 8 | accepts_nested_attributes_for :secrets, allow_destroy: true, reject_if: :all_blank 9 | accepts_nested_attributes_for :env_variables, allow_destroy: true, reject_if: :all_blank 10 | 11 | validates :name, presence: true 12 | validate :valid_repository_url 13 | 14 | def secrets_hash 15 | secrets.pluck(:key, :value).to_h 16 | end 17 | 18 | def secret_values 19 | secrets.pluck(:value) 20 | end 21 | 22 | def env_strings 23 | vars = [] 24 | vars += env_variables.map { |env_var| "#{env_var.key}=#{env_var.value}" } 25 | vars += secrets.map { |secret| "#{secret.key}=#{secret.value}" } 26 | vars 27 | end 28 | 29 | private 30 | 31 | def valid_repository_url 32 | return if repository_url.blank? 33 | return if valid_git_url?(repository_url) 34 | 35 | errors.add(:repository_url, "must be a valid HTTP, HTTPS, or SSH git URL") 36 | end 37 | 38 | def valid_git_url?(url) 39 | return false if url.blank? 40 | 41 | ssh_url_pattern = /\A(ssh:\/\/)?git@[\w\.-]+:[\w\.\/-]+\.git\z/ 42 | http_url_pattern = /\Ahttps?:\/\/.+\z/ 43 | 44 | url.match?(ssh_url_pattern) || url.match?(http_url_pattern) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/projects_controller.rb: -------------------------------------------------------------------------------- 1 | class ProjectsController < ApplicationController 2 | def index 3 | @projects = Project.kept 4 | end 5 | 6 | def show 7 | @project = Project.find(params[:id]) 8 | end 9 | 10 | def edit 11 | @project = Project.find(params[:id]) 12 | end 13 | 14 | def update 15 | @project = Project.find(params[:id]) 16 | if @project.update(project_params) 17 | redirect_to @project, notice: "Project was successfully updated." 18 | else 19 | render :edit, status: :unprocessable_entity 20 | end 21 | end 22 | 23 | def destroy 24 | @project = Project.find(params[:id]) 25 | @project.discard 26 | redirect_to projects_url, notice: "Project was successfully archived." 27 | end 28 | 29 | def new 30 | @project = Project.new 31 | end 32 | 33 | def create 34 | @project = Project.new(project_params) 35 | if @project.save 36 | redirect_to @project, notice: "Project was successfully created." 37 | else 38 | render :new, status: :unprocessable_entity 39 | end 40 | end 41 | 42 | private 43 | 44 | def project_params 45 | params.require(:project).permit(:name, :description, :repository_url, :setup_script, :repo_path, :dev_dockerfile_path, :dev_container_port, 46 | secrets_attributes: [ :id, :key, :value, :_destroy ], 47 | env_variables_attributes: [ :id, :key, :value, :_destroy ]) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /agents/css.md: -------------------------------------------------------------------------------- 1 | # RSCSS Commandments 2 | 1. **Think in components, not `
` soup** 3 | Every top-level piece of UI is a *component* with a **two-word dashed** class name. 4 | ```html 5 |
6 | ``` 7 | 2. **Elements: single word + direct child** 8 | Use one bare word for each child and the `>` combinator so styles don't leak. 9 | ```css 10 | .comment-box > .title { … } 11 | ``` 12 | 3. **Variants: add a leading dash** 13 | ```css 14 | .comment-box { 15 | &.-featured { … } /* component variant */ 16 | & > .title.-small { … } /* element variant */ 17 | } 18 | ``` 19 | 4. **One component per file** 20 | File: `app/assets/stylesheets/comment-box.css`. 21 | 5. **Never nest more than one level** 22 | Two hops max: `.outer > .inner`. Deeper = maintenance nightmare. 23 | 6. **Nested components stay independent** 24 | Give the inner thing its own dashed class (`.avatar-card` inside `.comment-box`). 25 | 7. **Compose variants, don't concatenate** 26 | Need *big* **and** *warning*? `class=".-big -warning"` (two classes), not `.-big-warning`. 27 | 8. **Helpers/utilities: generic, single-purpose** 28 | `.is-hidden`, `.u-text-center`, `.js-hook`—one job each, no style bleed. 29 | 9. **Describe purpose, not paint color** 30 | `.alert` or `.-error` > `.red-text`. Designers will thank you. 31 | 10. **When in doubt, reread this list** 32 | TL;DR: **Component → Element → Dash variant → Shallow nesting**. 33 | -------------------------------------------------------------------------------- /app/javascript/controllers/prompt_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["textarea", "form"] 5 | 6 | connect() { 7 | this.updateTextareaAttributes() 8 | } 9 | 10 | keydown(event) { 11 | if (event.key === "Enter") { 12 | if (this.isMobile()) { 13 | return 14 | } else { 15 | if (event.shiftKey) { 16 | return 17 | } else { 18 | event.preventDefault() 19 | this.submitForm() 20 | } 21 | } 22 | } 23 | } 24 | 25 | submitForm() { 26 | const form = this.textareaTarget.closest("form") 27 | if (form) { 28 | form.requestSubmit() 29 | } 30 | } 31 | 32 | clearForm() { 33 | this.textareaTarget.value = "" 34 | } 35 | 36 | isMobile() { 37 | const userAgent = navigator.userAgent.toLowerCase() 38 | const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 39 | const smallScreen = window.matchMedia("(max-width: 768px)").matches 40 | 41 | const mobileUserAgents = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/ 42 | 43 | return hasTouch && (smallScreen || mobileUserAgents.test(userAgent)) 44 | } 45 | 46 | updateTextareaAttributes() { 47 | if (this.isMobile()) { 48 | this.textareaTarget.setAttribute('enterkeyhint', 'enter') 49 | } else { 50 | this.textareaTarget.setAttribute('enterkeyhint', 'send') 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/views/projects/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= @project.name %>

2 | 3 |

<%= simple_format(@project.description) %>

4 | 5 | <% if @project.repository_url.present? %> 6 |

7 | Repository URL: <%= safe_external_link(@project.repository_url, @project.repository_url) %> 8 |

9 | <% end %> 10 | 11 | <% if @project.repo_path.present? %> 12 |

13 | Repository Path: <%= @project.repo_path %> 14 |

15 | <% end %> 16 | 17 |

Actions

18 |

19 | <%= link_to 'View Tasks', project_tasks_path(@project), class: 'button' %> | 20 | <%= link_to 'New Task', new_project_task_path(@project), class: 'button' %> 21 |

22 | 23 |

Setup Script

24 |
<%= @project.setup_script %>
25 | 26 | <% if @project.env_variables.any? %> 27 |

Environment Variables

28 |

Configured variables: <%= @project.env_variables.pluck(:key).join(', ') %>

29 | <% end %> 30 | 31 | <% if @project.secrets.any? %> 32 |

Project Secrets

33 |

Configured secret keys: <%= @project.secrets.pluck(:key).join(', ') %>

34 |

Values are encrypted and hidden for security.

35 | <% end %> 36 | 37 |
38 | <%= link_to 'Edit', edit_project_path(@project), class: 'button' %> | 39 | <%= link_to 'Archive', project_path(@project), 40 | data: { turbo_method: :delete, turbo_confirm: 'Are you sure you want to archive this project?' }, 41 | class: 'archive' %> 42 |
43 | -------------------------------------------------------------------------------- /app/javascript/controllers/prompt_truncate_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["content", "toggle"] 5 | static values = { maxLength: Number } 6 | 7 | connect() { 8 | this.maxLengthValue = this.maxLengthValue || 200 9 | this.checkLength() 10 | } 11 | 12 | checkLength() { 13 | const content = this.contentTarget.textContent 14 | if (content.length > this.maxLengthValue) { 15 | this.truncate() 16 | } else { 17 | this.hideToggle() 18 | } 19 | } 20 | 21 | truncate() { 22 | const content = this.contentTarget.textContent 23 | this.fullContent = content 24 | this.truncatedContent = content.substring(0, this.maxLengthValue) + "..." 25 | this.contentTarget.textContent = this.truncatedContent 26 | this.showToggle() 27 | this.toggleTarget.textContent = "Show more" 28 | this.isExpanded = false 29 | } 30 | 31 | toggle() { 32 | if (this.isExpanded) { 33 | this.contentTarget.textContent = this.truncatedContent 34 | this.toggleTarget.textContent = "Show more" 35 | this.isExpanded = false 36 | } else { 37 | this.contentTarget.textContent = this.fullContent 38 | this.toggleTarget.textContent = "Show less" 39 | this.isExpanded = true 40 | } 41 | } 42 | 43 | showToggle() { 44 | this.toggleTarget.style.display = "inline" 45 | } 46 | 47 | hideToggle() { 48 | this.toggleTarget.style.display = "none" 49 | } 50 | } -------------------------------------------------------------------------------- /app/views/user_settings/show.html.erb: -------------------------------------------------------------------------------- 1 |

User Settings

2 | 3 |

4 | <%= link_to 'Edit Settings', edit_user_settings_path, class: 'button' %> 5 | <%= link_to 'Change Email/Password', edit_account_settings_path, class: 'button' %> 6 |

7 | 8 |
9 | Email: <%= @user.email_address %> 10 |
11 | 12 |
13 | Role: <%= @user.role&.humanize || 'Standard' %> 14 |
15 | 16 |
17 | GitHub Token: 18 | <% if @user.github_token.present? %> 19 | •••••••••••••••• 20 | <% else %> 21 | Not set 22 | <% end %> 23 |
24 | 25 |
26 | Allow tasks to access GitHub token: 27 | <%= @user.allow_github_token_access ? "Yes" : "No" %> 28 |
29 | 30 |
31 | Global Agent Instructions: 32 | <% if @user.instructions.present? %> 33 |
<%= @user.instructions %>
34 | Configure mount path in individual agent settings. 35 | <% else %> 36 | Not set 37 | <% end %> 38 |
39 | 40 |
41 | Git Configuration: 42 | <% if @user.git_config.present? %> 43 |
<%= @user.git_config %>
44 | Mounted as ~/.gitconfig in agent containers. 45 | <% else %> 46 | Not set 47 | <% end %> 48 |
49 | 50 |
51 | Shrimp Mode 🍤: <%= @user.shrimp_mode? ? "Enabled" : "Disabled" %> 52 |
-------------------------------------------------------------------------------- /app/controllers/concerns/authentication.rb: -------------------------------------------------------------------------------- 1 | module Authentication 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_action :require_authentication 6 | helper_method :authenticated? 7 | end 8 | 9 | class_methods do 10 | def allow_unauthenticated_access(**options) 11 | skip_before_action :require_authentication, **options 12 | end 13 | end 14 | 15 | private 16 | def authenticated? 17 | resume_session 18 | end 19 | 20 | def require_authentication 21 | resume_session || request_authentication 22 | end 23 | 24 | def resume_session 25 | Current.session ||= find_session_by_cookie 26 | end 27 | 28 | def find_session_by_cookie 29 | Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id] 30 | end 31 | 32 | def request_authentication 33 | session[:return_to_after_authenticating] = request.url 34 | redirect_to new_session_path 35 | end 36 | 37 | def after_authentication_url 38 | session.delete(:return_to_after_authenticating) || root_url 39 | end 40 | 41 | def start_new_session_for(user) 42 | user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| 43 | Current.session = session 44 | cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } 45 | end 46 | end 47 | 48 | def terminate_session 49 | Current.session.destroy 50 | cookies.delete(:session_id) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/models/step.rb: -------------------------------------------------------------------------------- 1 | class Step < ApplicationRecord 2 | belongs_to :run 3 | has_many :repo_states, dependent: :destroy 4 | has_many :child_steps, -> { order(:id) }, class_name: "Step", primary_key: :tool_use_id, foreign_key: :parent_tool_use_id 5 | 6 | scope :top_level, -> { where(parent_tool_use_id: nil) } 7 | 8 | validates :raw_response, presence: true 9 | 10 | def parsed_response 11 | JSON.parse(raw_response) 12 | rescue JSON::ParserError 13 | raw_response 14 | end 15 | 16 | def content 17 | filter_sensitive_info(super) 18 | end 19 | 20 | def raw_response 21 | filter_sensitive_info(super) 22 | end 23 | 24 | private 25 | 26 | def filter_sensitive_info(message) 27 | return message unless message.present? 28 | 29 | filtered_message = message.dup 30 | 31 | github_token = run&.task&.user&.github_token 32 | if github_token.present? 33 | filtered_message = filtered_message.gsub(github_token, "[FILTERED]") 34 | end 35 | 36 | ssh_key = run&.task&.user&.ssh_key 37 | if ssh_key.present? 38 | ssh_key_lines = ssh_key.lines.map(&:strip).reject(&:empty?) 39 | ssh_key_lines.each do |line| 40 | filtered_message = filtered_message.gsub(line, "[FILTERED]") 41 | end 42 | end 43 | 44 | all_secret_values = Secret.pluck(:value) 45 | all_secret_values.each do |secret_value| 46 | next unless secret_value.present? 47 | 48 | filtered_message = filtered_message.gsub(secret_value, "[FILTERED]") 49 | end 50 | 51 | filtered_message 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/views/runs/_diff_panel.html.erb: -------------------------------------------------------------------------------- 1 |
5 |
6 | <% if repo_state.target_branch_diff.present? %> 7 | 11 | <% end %> 12 | 16 |
17 |
18 | 24 |
25 |
-------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Summoncircle 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 8.0 13 | 14 | # Please, add to the `ignore` list any other `lib` subdirectories that do 15 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 16 | # Common ones are `templates`, `generators`, or `middleware`, for example. 17 | config.autoload_lib(ignore: %w[assets tasks]) 18 | 19 | # Configuration for the application, engines, and railties goes here. 20 | # 21 | # These settings can be overridden in specific environments using the files 22 | # in config/environments, which are processed later. 23 | # 24 | # config.time_zone = "Central Time (US & Canada)" 25 | # config.eager_load_paths << Rails.root.join("extras") 26 | 27 | # Disable Mission Control Jobs default HTTP basic auth 28 | config.mission_control.jobs.http_basic_auth_enabled = false 29 | 30 | config.active_record.encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"] 31 | config.active_record.encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"] 32 | config.active_record.encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/models/secret_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SecretTest < ActiveSupport::TestCase 4 | test "validates presence of key" do 5 | secret = Secret.new(secretable: projects(:one), value: "secret_value") 6 | assert_not secret.valid? 7 | assert_includes secret.errors[:key], "can't be blank" 8 | end 9 | 10 | test "validates presence of value" do 11 | secret = Secret.new(secretable: projects(:one), key: "API_KEY") 12 | assert_not secret.valid? 13 | assert_includes secret.errors[:value], "can't be blank" 14 | end 15 | 16 | test "validates uniqueness of key within project" do 17 | project = projects(:one) 18 | Secret.create!(secretable: project, key: "API_KEY", value: "secret1") 19 | 20 | duplicate_secret = Secret.new(secretable: project, key: "API_KEY", value: "secret2") 21 | assert_not duplicate_secret.valid? 22 | assert_includes duplicate_secret.errors[:key], "has already been taken" 23 | end 24 | 25 | test "allows same key in different projects" do 26 | project1 = projects(:one) 27 | project2 = projects(:two) 28 | 29 | Secret.create!(secretable: project1, key: "API_KEY", value: "secret1") 30 | secret2 = Secret.new(secretable: project2, key: "API_KEY", value: "secret2") 31 | 32 | assert secret2.valid? 33 | end 34 | 35 | test "encrypts value" do 36 | secret = Secret.create!(secretable: projects(:one), key: "API_KEY", value: "secret_value") 37 | 38 | assert_not_equal "secret_value", secret.read_attribute_before_type_cast(:value) 39 | assert_equal "secret_value", secret.value 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/views/shared/_json_array_fields.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

<%= title %>

3 | <% if local_assigns[:help_text] %> 4 | <%= help_text %> 5 | <% end %> 6 | 7 | <%= form.hidden_field field_name, data: { json_array_fields_target: "hiddenField" } %> 8 | 9 |
10 | <% (form.object.send(field_name) || []).each do |value| %> 11 |
12 | 16 | 17 |
18 | <% end %> 19 |
20 | 21 | 22 | 23 | 32 |
-------------------------------------------------------------------------------- /app/assets/stylesheets/task-show.css: -------------------------------------------------------------------------------- 1 | html body.tasks.show { 2 | max-width: 100% !important; 3 | width: 100% !important; 4 | margin: 0 !important; 5 | padding: 0 !important; 6 | display: block !important; 7 | grid-template-columns: none !important; 8 | } 9 | 10 | 11 | @media (max-width: 768px) { 12 | html body.tasks.show { 13 | height: auto; 14 | overflow: auto; 15 | } 16 | } 17 | 18 | html body.tasks.show > * { 19 | grid-column: unset !important; 20 | } 21 | 22 | html body.tasks.show header { 23 | max-width: 100% !important; 24 | width: 100% !important; 25 | margin: 0 !important; 26 | grid-column: unset !important; 27 | padding-bottom: 0 !important; 28 | } 29 | 30 | html body.tasks.show header nav { 31 | max-width: 100% !important; 32 | width: 100% !important; 33 | margin: 0 !important; 34 | } 35 | 36 | html body.tasks.show header nav ul { 37 | max-width: 100% !important; 38 | padding: 0 1rem !important; 39 | } 40 | 41 | html body.tasks.show main { 42 | max-width: 100% !important; 43 | width: 100% !important; 44 | padding: 0 !important; 45 | margin: 0 !important; 46 | } 47 | 48 | 49 | body.tasks.show .codex-layout { 50 | margin: 0; 51 | max-width: 100% !important; 52 | width: 100% !important; 53 | padding: 0 1rem; 54 | } 55 | 56 | body.tasks.show .chat-panel, 57 | body.tasks.show .log-panel { 58 | max-width: 100% !important; 59 | } 60 | 61 | body.tasks.show .task-actions { 62 | margin: 1rem; 63 | } 64 | 65 | @media (max-width: 768px) { 66 | body.tasks.show .codex-layout > .chat-panel, 67 | body.tasks.show .codex-layout > .log-panel { 68 | width: 100% !important; 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /app/javascript/controllers/copy_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["source"] 5 | static classes = ["success", "error"] 6 | 7 | async copy(event) { 8 | event.preventDefault() 9 | 10 | const button = event.currentTarget 11 | // For template elements, we need to access the content property 12 | const content = this.sourceTarget.content ? 13 | this.sourceTarget.content.textContent.trim() : 14 | this.sourceTarget.textContent.trim() 15 | 16 | if (!content) { 17 | this.showFeedback(button, 'Nothing to copy', false) 18 | return 19 | } 20 | 21 | try { 22 | await navigator.clipboard.writeText(content) 23 | this.showFeedback(button, 'Copied!', true) 24 | } catch (error) { 25 | console.error('Error copying to clipboard:', error) 26 | this.showFeedback(button, 'Failed to copy', false) 27 | } 28 | } 29 | 30 | showFeedback(button, message, success) { 31 | const originalText = button.textContent 32 | button.textContent = message 33 | button.disabled = true 34 | 35 | if (success && this.hasSuccessClass) { 36 | button.classList.add(this.successClass) 37 | } else if (!success && this.hasErrorClass) { 38 | button.classList.add(this.errorClass) 39 | } 40 | 41 | setTimeout(() => { 42 | button.textContent = originalText 43 | button.disabled = false 44 | if (this.hasSuccessClass) button.classList.remove(this.successClass) 45 | if (this.hasErrorClass) button.classList.remove(this.errorClass) 46 | }, 2000) 47 | } 48 | } -------------------------------------------------------------------------------- /app/javascript/controllers/auto_scroll_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.scrollContainer = this.findScrollContainer() 6 | this.useWindow = this.scrollContainer === window 7 | this.follow = true 8 | this.boundCheck = this.check.bind(this) 9 | this.boundMaybe = this.maybe.bind(this) 10 | this.scrollContainer.addEventListener("scroll", this.boundCheck) 11 | this.observer = new MutationObserver(this.boundMaybe) 12 | this.observer.observe(this.element, { childList: true, subtree: true }) 13 | this.scroll() 14 | } 15 | 16 | disconnect() { 17 | this.scrollContainer.removeEventListener("scroll", this.boundCheck) 18 | if (this.observer) this.observer.disconnect() 19 | } 20 | 21 | check() { 22 | const el = this.useWindow ? document.documentElement : this.scrollContainer 23 | const bottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 5 24 | this.follow = bottom 25 | } 26 | 27 | maybe() { 28 | if (this.follow) this.scroll() 29 | } 30 | 31 | scroll() { 32 | if (this.useWindow) { 33 | window.scrollTo(0, document.documentElement.scrollHeight) 34 | } else { 35 | this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight 36 | } 37 | } 38 | 39 | findScrollContainer() { 40 | let el = this.element 41 | while (el && el !== document.body) { 42 | const overflow = getComputedStyle(el).overflowY 43 | if (overflow === "auto" || overflow === "scroll") { 44 | return el 45 | } 46 | el = el.parentElement 47 | } 48 | return window 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/assets/stylesheets/flash-alert.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-success-light: #d4edda; 3 | --color-success-dark: #155724; 4 | --color-danger-light: #f8d7da; 5 | --color-danger-dark: #721c24; 6 | --border-radius: 0.375rem; 7 | --layout-gutter: 0.75rem; 8 | --font-weight-bold: 600; 9 | } 10 | 11 | .flash-alert { 12 | padding: .75rem 1.25rem; 13 | margin-bottom: 1rem; 14 | border: 1px solid transparent; 15 | border-radius: var(--border-radius); 16 | 17 | color: var(--fg-color); 18 | background-color: var(--bg-color); 19 | border-color: var(--fg-color); 20 | 21 | margin-top: var(--layout-gutter); 22 | margin-bottom: var(--layout-gutter); 23 | padding: var(--layout-gutter); 24 | } 25 | 26 | .flash-alert a { 27 | font-weight: var(--font-weight-bold); 28 | } 29 | 30 | .flash-alert > .close { 31 | cursor: pointer; 32 | padding: var(--layout-gutter); 33 | padding-right: calc(var(--layout-gutter) * 1.5); 34 | margin-top: calc(var(--layout-gutter) * -1); 35 | margin-bottom: calc(var(--layout-gutter) * -1); 36 | margin-right: calc(var(--layout-gutter) * -1); 37 | float: right; 38 | background: transparent; 39 | border: 0; 40 | color: inherit; 41 | } 42 | 43 | .flash-alert > .close:active, 44 | .flash-alert > .close:not(:disabled):not(.disabled):active, 45 | .flash-alert > .close:focus, 46 | .flash-alert > .close:hover { 47 | background: transparent; 48 | border: 0; 49 | color: inherit; 50 | } 51 | 52 | .flash-alert.-success { 53 | --bg-color: var(--color-success-light); 54 | --fg-color: var(--color-success-dark); 55 | } 56 | 57 | .flash-alert.-danger { 58 | --bg-color: var(--color-danger-light); 59 | --fg-color: var(--color-danger-dark); 60 | } -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | require "mocha/minitest" 5 | 6 | # Load all support files 7 | Dir[Rails.root.join("test/support/**/*.rb")].each { |f| require f } 8 | 9 | module ActiveSupport 10 | class TestCase 11 | # Run tests in parallel with specified workers 12 | parallelize(workers: :number_of_processors) 13 | 14 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 15 | fixtures :all 16 | 17 | # Add more helper methods to be used by all tests here... 18 | 19 | def assert_valid(model) 20 | assert model.valid?, model.errors.full_messages 21 | end 22 | 23 | def assert_presence(value, *args) 24 | assert_predicate value, :present?, *args 25 | end 26 | 27 | def login(user) 28 | user = users(user) unless user.is_a? User 29 | post session_url, params: { email_address: user.email_address, password: "password" } 30 | follow_redirect! 31 | assert_equal user, current_user 32 | end 33 | 34 | def logout 35 | delete session_url 36 | assert current_session.nil? 37 | end 38 | 39 | def current_session 40 | return unless cookie_jar[:session_id].present? 41 | Session.find_by(id: cookie_jar.signed[:session_id]) 42 | end 43 | 44 | def current_user 45 | current_session&.user 46 | end 47 | 48 | def cookie_jar 49 | ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash) 50 | end 51 | 52 | def array_to_param_hash(array) 53 | array.map.with_index { |value, index| [ index, value ] }.to_h 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | primary: 14 | <<: *default 15 | database: storage/development.sqlite3 16 | cache: 17 | <<: *default 18 | database: storage/development_cache.sqlite3 19 | migrations_paths: db/cache_migrate 20 | queue: 21 | <<: *default 22 | database: storage/development_queue.sqlite3 23 | migrations_paths: db/queue_migrate 24 | cable: 25 | <<: *default 26 | database: storage/development_cable.sqlite3 27 | migrations_paths: db/cable_migrate 28 | 29 | # Warning: The database defined as "test" will be erased and 30 | # re-generated from your development database when you run "rake". 31 | # Do not set this db to the same as development or production. 32 | test: 33 | <<: *default 34 | database: storage/test.sqlite3 35 | 36 | 37 | # Store production database in the storage/ directory, which by default 38 | # is mounted as a persistent Docker volume in config/deploy.yml. 39 | production: 40 | primary: 41 | <<: *default 42 | database: storage/production.sqlite3 43 | cache: 44 | <<: *default 45 | database: storage/production_cache.sqlite3 46 | migrations_paths: db/cache_migrate 47 | queue: 48 | <<: *default 49 | database: storage/production_queue.sqlite3 50 | migrations_paths: db/queue_migrate 51 | cable: 52 | <<: *default 53 | database: storage/production_cable.sqlite3 54 | migrations_paths: db/cable_migrate 55 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resource :session 3 | resources :passwords, param: :token 4 | resource :user_settings, only: %i[show edit update] 5 | resource :account_settings, only: %i[edit update] 6 | resources :agents do 7 | member do 8 | get "oauth/login_start", to: "claude_oauth#login_start", as: :oauth_login_start 9 | post "oauth/login_finish", to: "claude_oauth#login_finish", as: :oauth_login_finish 10 | post "oauth/refresh", to: "claude_oauth#refresh", as: :oauth_refresh 11 | end 12 | end 13 | resources :projects do 14 | resources :project_branches, only: %i[index] 15 | resources :tasks, shallow: true do 16 | resource :repository_download, only: [ :show ] 17 | member do 18 | get :branches 19 | patch :update_auto_push 20 | end 21 | resource :container, only: %i[create destroy] 22 | resources :runs, only: %i[create] 23 | end 24 | end 25 | 26 | resources :tasks, only: %i[create] 27 | 28 | mount MissionControl::Jobs::Engine, at: "/jobs" 29 | 30 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 31 | 32 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. 33 | # Can be used by load balancers and uptime monitors to verify that the app is live. 34 | get "up" => "rails/health#show", as: :rails_health_check 35 | 36 | # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) 37 | # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest 38 | # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker 39 | 40 | root "dashboard#index" 41 | end 42 | --------------------------------------------------------------------------------