v0.4.0 — architecture lockdown, money-path coverage, instrumentation
The architecture-and-hardening release. Chaos-gated at 225 kill/deploy cycles — zero duplicate side effects, byte-identical replay, on SQLite and Postgres.
Fixed
The email approval channel had never worked. The approval email template and the confirmation page both called approval_url/approval_path, but the route is declared inside namespace :channels, so the real helpers are channels_approval_url/_path. Rendering raised — meaning ChannelMailer#approval blew up and the "your agent needs approval" email was never delivered, and the confirmation page 500'd. If you relied on email approvals, you were silently never told that a money-moving call was parked. Found by writing the first specs for these surfaces.
Changed (breaking, pre-1.0)
- The inference seam is
Adapters::, notEngines::— andconfig.adapter, notconfig.engine. "Engine" meant two unrelated things in one namespace: the Rails engine atSilas::Engine, and the pluggable inference backend. Every comparable seam disambiguates — ActiveJob hasQueueAdapters::, ActiveStorageService::, RubyLLMProvider. Changed now because 1.0 freezes the public API and host apps subclass this seam. Nothing breaks today:Silas::Engines::Base,config.engine, andSilas.resolved_enginestill resolve, warn through the new deprecator, and are removed in 2.0. - Notification names follow the Rails convention
<event>.silas(likesql.active_record). The two existing events were backwards:silas.step→step.silas,silas.delta→delta.silas.subscribe(/\.silas\z/)now catches everything.
Added
- Instrumentation for the durable loop. It was silent before — a turn could start, park for a human, be rescued after a
kill -9, breach a budget and finish without a single line. Ten documented events now.tool.silastimes the tool's own execution and reports how it settled;resume.silascarriesparked_for— how long the human actually took.Silas::LogSubscriberrenders them at operator-filterable levels and stays quiet when the rescuer did nothing. Silas.deprecator, registered inapp.deprecators[:silas], so hosts control Silas deprecations exactly as they control Rails'.- 36 specs on the four money-path surfaces that had zero coverage — Slack webhook signature verification end to end (unsigned, wrong-secret and replayed requests refused), the signed email approval links (tampered/expired/wrong-purpose tokens refused; GET never mutates, so a link preview cannot approve a refund), inbound-mail threading, and the approval mailer.
- Quality tooling in CI:
rubocop-rails-omakase(zero offenses), SimpleCov with a 90% floor that fails the build (actual 92.5%), Brakeman and bundler-audit, and arake zeitwerk:checkjob. - Ten dependency contract specs pinning the Solid Queue and RubyLLM internals Silas couples to, where a rename would break recovery silently — plus an allowed-to-fail canary against ruby_llm edge, which on its first run already flagged that RubyLLM 2.0 drops
Tool::Haltand changes the model-registry price accessors. docs/conventions.mdrecording the deliberate deviations from Rails defaults and the audited posture: nothing mass-assigned, noraw/html_safeanywhere,Time.currentthroughout.
No migration. 336 specs on SQLite and Postgres.