├── 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 |
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 |
6 | <% @agents.each do |agent| %>
7 |
8 | <%= link_to agent.name, agent_path(agent) %>
9 |
10 | <% end %>
11 |
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 |
6 | <% @projects.each do |project| %>
7 |
8 | <%= link_to project.name, project_path(project) %>
9 |
10 | <% end %>
11 |
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 |
--------------------------------------------------------------------------------
/app/views/step/thinkings/_thinking.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
2 | <% @tasks.each do |task| %>
3 |
4 | <%= link_to "##{task.id}: #{task.description.presence || 'No description'}", task_path(task) %>
5 | <% unless @project %>
6 | - <%= task.project.name %>
7 | <% end %>
8 | (<%= task.created_at.strftime("%m/%d %H:%M") %>)
9 |
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/app/views/step/ls_tools/_ls_tool.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
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 |
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 |
6 | <% else %>
7 |
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 |
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 | <%= task.description.presence || "Task ##{task.id}" %> - Summoncircle
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
12 | <% else %>
13 |
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 |
--------------------------------------------------------------------------------
/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 |
3 |
4 | <%= link_to "SummonCircle", root_path, class: (current_page?(root_path) ? "current" : nil) %>
5 | <%= link_to "Projects", projects_path, class: (current_page?(projects_path) ? "current" : nil) %>
6 | <%= link_to "Agents", agents_path, class: (current_page?(agents_path) ? "current" : nil) %>
7 | <% if authenticated? %>
8 | <%= link_to "Settings", user_settings_path, class: (current_page?(user_settings_path) || current_page?(edit_user_settings_path) ? "current" : nil) %>
9 | <%= link_to "Sign out", session_path, data: { turbo_method: :delete } %>
10 | <% end %>
11 |
12 |
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 |
--------------------------------------------------------------------------------
/.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 |
6 | <%= link_to "View log", task_path(run.task, selected_run_id: run.id), class: "chat-log-link" %>
7 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 | Copy the URL below and paste it into your browser
9 | Log in to Anthropic and authorize the application
10 | After authorization, you'll be redirected to a page with an authorization code
11 | Copy the authorization code and paste it in the form below
12 |
13 |
14 |
15 | OAuth Login URL:
16 |
17 | Copy URL
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 |
8 | Working Changes
9 | Changes from Base Branch
10 |
11 | <% end %>
12 |
13 |
14 | Side by Side
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | Remove
17 |
18 | <% end %>
19 |
20 |
21 |
Add <%= singular_name %>
22 |
23 |
24 |
25 |
29 | Remove
30 |
31 |
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 |
--------------------------------------------------------------------------------