Class: Silas::Generators::InstallGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Defined in:
lib/generators/silas/install/install_generator.rb

Constant Summary collapse

RESCUER_ENTRY =
<<~YAML.freeze
  # Silas: retries jobs failed by dead-worker reaping/pruning, sweeps
  # expired approvals, and fails turns stranded by dead loop jobs. Part
  # of the durability contract — do not remove.
  silas_dead_job_rescuer:
    class: Silas::DeadJobRescuerJob
    queue: default
    schedule: every 30 seconds
YAML

Instance Method Summary collapse

Instance Method Details

#add_rescuer_recurring_taskObject

Idempotent and environment-aware: the entry is injected directly under EVERY deployable top-level env key (production, staging, …) — never a blind append into whatever block happens to end the file, and never a duplicate key on a re-run. A staging worker needs the rescuer exactly as much as production does.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/generators/silas/install/install_generator.rb', line 81

def add_rescuer_recurring_task
  recurring = "config/recurring.yml"
  path = File.expand_path(recurring, destination_root)

  unless File.exist?(path)
    create_file recurring, "production:\n#{RESCUER_ENTRY.indent(2)}"
    return
  end

  if File.read(path).include?("silas_dead_job_rescuer")
    say_status :skip, "#{recurring} already contains silas_dead_job_rescuer", :yellow
    return
  end

  injected = false
  gsub_file recurring, /^(?!development:|test:)([a-zA-Z_]+:)[ \t]*$/ do |header|
    injected = true
    "#{header}\n#{RESCUER_ENTRY.indent(2)}"
  end
  append_to_file recurring, "\nproduction:\n#{RESCUER_ENTRY.indent(2)}" unless injected
end

#create_agent_directoryObject



23
24
25
26
27
28
29
# File 'lib/generators/silas/install/install_generator.rb', line 23

def create_agent_directory
  template "instructions.md", "app/agent/instructions.md"
  template "agent.yml", "app/agent/agent.yml"
  template "example_tool.rb", "app/agent/tools/example_tool.rb"
  template "example_skill.md", "app/agent/skills/example.md"
  template "example_schedule.md", "app/agent/schedules/example.md"
end

#create_channelsObject

Channels are opt-in: scaffold the two reference bindings + mount the engine that serves their webhooks. Delete a channel file to disable it.



33
34
35
36
# File 'lib/generators/silas/install/install_generator.rb', line 33

def create_channels
  template "channel_slack.rb", "app/agent/channels/slack.rb"
  template "channel_email.rb", "app/agent/channels/email.rb"
end

#create_claude_skillObject

A Claude Code / coding-agent skill: the app/agent/ conventions, the effect-mode and approval decision rules, and the ledger rules an agent must never violate — so a coding agent driving this app builds Silas agents correctly without the human learning the framework first. Delete the file if you don't use coding agents.



43
44
45
# File 'lib/generators/silas/install/install_generator.rb', line 43

def create_claude_skill
  template "claude_skill.md", ".claude/skills/silas/SKILL.md"
end

#create_eval_gateObject

Evals as a deploy gate: an example scenario + a bin/ci wrapper. Rails 8.1 apps ship their own bin/ci (rubocop, brakeman, tests) — never clobber it; tell the user to add the eval gate to theirs instead.



54
55
56
57
58
59
60
61
62
63
64
# File 'lib/generators/silas/install/install_generator.rb', line 54

def create_eval_gate
  template "example_eval.rb", "test/agent_evals/example_eval.rb"

  if File.exist?(File.expand_path("bin/ci", destination_root))
    say "bin/ci already exists — left untouched. Add `bin/rails silas:eval` " \
        "to it to gate deploys on agent evals.", :yellow
  else
    copy_file "bin_ci", "bin/ci"
    chmod "bin/ci", 0o755
  end
end

#create_initializerObject



9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/generators/silas/install/install_generator.rb', line 9

def create_initializer
  template "initializer.rb", "config/initializers/silas.rb"
  # ruby_llm reads no provider keys from ENV on its own; without this the
  # first model call raises ConfigurationError. Brownfield apps often
  # already configure ruby_llm themselves — never touch an existing one
  # (not even a conflict prompt: an accidental Y clobbers production
  # provider config).
  if File.exist?(File.join(destination_root, "config/initializers/ruby_llm.rb"))
    say_status :skip, "config/initializers/ruby_llm.rb exists — left untouched", :yellow
  else
    template "ruby_llm.rb", "config/initializers/ruby_llm.rb"
  end
end

#install_migrationsObject



103
104
105
106
107
# File 'lib/generators/silas/install/install_generator.rb', line 103

def install_migrations
  rake "silas:install:migrations"
rescue StandardError
  say "Run `bin/rails silas:install:migrations db:migrate` to install Silas's tables.", :yellow
end

#mount_engineObject



47
48
49
# File 'lib/generators/silas/install/install_generator.rb', line 47

def mount_engine
  route 'mount Silas::Engine => "/silas"'
end

#show_next_stepsObject



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/generators/silas/install/install_generator.rb', line 109

def show_next_steps
  say <<~MSG, :green

    Silas installed. Next:
      1. bin/rails db:migrate
      2. export ANTHROPIC_API_KEY=sk-ant-...  (config/initializers/ruby_llm.rb reads it)
         then `bin/rails silas:doctor` to verify the whole setup
      3. Edit app/agent/instructions.md (your agent's persona)
      4. Write tools in app/agent/tools/ (keyword signature = schema)
      5. Talk to it: bin/rails silas:chat  (or Silas.agent.start(input: "hello"))
      6. Schedules: edit app/agent/schedules/*, then `bin/rails silas:schedules`
      7. Channels (optional): set credentials.silas.slack.{signing_secret,bot_token}
         for Slack; route inbound mail to Silas::AgentMailbox for email.
         Delete app/agent/channels/{slack,email}.rb to disable. Any other
         transport: `bin/rails g silas:channel whatsapp`.
      8. Inbox + web chat: /silas/inbox, deny-by-default — uncomment
         config.inbox_auth in config/initializers/silas.rb to make it visible.
      9. Restart your server if it was running (app/agent/ registers at boot).
  MSG
end

#show_queue_adapter_remedyObject

Rails defaults development to the in-process :async adapter, which the doctor step above FAILS — without this the documented happy path ends on a red X. Detected, printed, never written: a gsub against the host's database.yml no-ops silently the moment they've edited it, and a force-written cable.yml clobbers whatever Redis config they run.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/generators/silas/install/install_generator.rb', line 135

def show_queue_adapter_remedy
  return unless defined?(::ActiveJob::Base)
  return unless ::ActiveJob::Base.queue_adapter.class.name.to_s.include?("AsyncAdapter")

  say <<~MSG, :yellow

    Queue adapter: this app is on ActiveJob's in-process :async adapter, and
    `bin/rails silas:doctor` fails it — :async runs a re-enqueued continuation
    concurrently with the original, which double-executes steps and breaks
    exactly-once tool effects.

    #{Silas::Doctor::ASYNC_QUEUE_REMEDY}
  MSG
end