Skip to content

About

Spec Kit for Ruby

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

spec_kit

A Ruby port of GitHub Spec Kit, redesigned as a library / Rails-engine that drives Spec-Driven Development directly through RubyLLM.

spec_kit lets your Rails application generate the same artifacts the original Spec Kit produces — constitution.md, spec.md, plan.md, tasks.md, analysis.md, checklists, and an implementation guide — by calling an LLM through your existing RubyLLM configuration. It is library-first: there is no CLI, no IDE-agent integration, and no shell scripts to wire up. You import the gem, hand it a chat object, and call workflows.

Why a Ruby port?

The original Spec Kit is a Python CLI bootstrap tool. It writes slash-command files and templates into your project so an IDE-based AI agent (Copilot, Cursor, Claude, …) can interpret them at runtime. spec_kit flips that model: it runs the workflows itself and just hands you back the markdown.

That makes it a much better fit when you want SDD as a feature of your own application — for example, a Rails admin console where product managers can spec a new feature, generate a plan, and hand a polished tasks file off to engineering.

Architectural decisions

Concern spec_kit's choice
Driving the LLM RubyLLM (dependency-injected — bring your own chat / model / API key)
Persistence File-based, mirrors specs/NNN-feature-name/ from the original. Pluggable for future AR backend
Rails integration Railtie + install generator + rake tasks (no controllers / routes shipped)
Templates / prompts Bundled verbatim from the original Spec Kit, overridable per-project
AI-agent integrations Dropped (RubyLLM is the integration)
Workflow engine Plain Ruby methods (SpecKit::Workflows::Specify#call) instead of a YAML interpreter
Extensions / presets Out of scope for v0.1; the simple Configuration#template_overrides hook covers most needs

Installation

In your Rails application's Gemfile (must already include ruby_llm):

gem "ruby_llm", "~> 1.14"
gem "spec_kit", path: "../spec-kit-ruby/spec_kit_ruby"   # or git: ..., or rubygems once published

Complete implementation example (Rails + RubyLLM)

This section wires RubyLLM, spec_kit, a service object that runs the full SDD pipeline, and a minimal API controller. Adjust names, auth, and error handling to match your app.

1. Install the generator layout

bundle install
bundle exec rails g spec_kit:install

You should have config/initializers/spec_kit.rb, memory/, and specs/.

2. RubyLLM initializer

git clone <repo> && cd spec-kit-ruby/spec_kit
cd spec_kit_ruby
bin/setup
    spec.md
    checklists/
      requirements.md
    plan.md
    checklists/
      security.md
      accessibility.md
      requirements.md          # from Specify
      security.md              # from Checklist topic "security"
      accessibility.md       # from Checklist topic "accessibility"
.specify/
Here’s what changed in the README:

Added: “Complete implementation example (Rails + RubyLLM)”
A single walkthrough that includes:

Install — rails g spec_kit:install
config/initializers/ruby_llm.rb — minimal RubyLLM.configure + link to RubyLLM docs
config/initializers/spec_kit.rb — default_chat as a lambda (fresh chat per call), use_git / branch_numbering
app/services/sdd/pipeline.rb — Sdd::Pipeline#run_full! running Specify → Plan → Tasks → Analyze → Implement plus optional Checklist topics; optional snippet for Constitution
app/controllers/sdd/features_controller.rb — create action + rescuing Sdd::Pipeline::Error and SpecKit::Error
config/routes.rb — namespace :sdd + resources :features, only: [:create]
curl — JSON example with description, tech_stack, checklist_topics
On-disk layout — one tree listing under memory/, specs/…, .specify/ (single checklists/ subtree, no duplication)
Sdd::RunPipelineJob — optional ApplicationJob example
Other fixes
Gemfile path updated to spec_kit_ruby (matches this repo).
Development section now uses cd spec_kit_ruby.
The new section is inserted after Configuration and before Usage, so install → config → full example → rake/snippets.

If you want, we can add a second example that uses only write_to_disk: false and returns markdown in the JSON body (preview mode).

Complete implementation example (Rails + RubyLLM)

This section wires RubyLLM, spec_kit, a service object that runs the full SDD pipeline, and a minimal API controller. Adjust names, auth, and error handling to match your app.

1. Install the generator layout

bundle install
bundle exec rails g spec_kit:install

You should have config/initializers/spec_kit.rb, memory/, and specs/.

2. RubyLLM initializer

git clone <repo> && cd spec-kit-ruby/spec_kit
cd spec_kit_ruby
bin/setup
    spec.md
    checklists/
      requirements.md
    plan.md
    checklists/
      security.md
      accessibility.md
      requirements.md          # from Specify
      security.md              # from Checklist topic "security"
      accessibility.md       # from Checklist topic "accessibility"
.specify/
Here’s what changed in the README:

Added: “Complete implementation example (Rails + RubyLLM)”
A single walkthrough that includes:

Install — rails g spec_kit:install
config/initializers/ruby_llm.rb — minimal RubyLLM.configure + link to RubyLLM docs
config/initializers/spec_kit.rb — default_chat as a lambda (fresh chat per call), use_git / branch_numbering
app/services/sdd/pipeline.rb — Sdd::Pipeline#run_full! running Specify → Plan → Tasks → Analyze → Implement plus optional Checklist topics; optional snippet for Constitution
app/controllers/sdd/features_controller.rb — create action + rescuing Sdd::Pipeline::Error and SpecKit::Error
config/routes.rb — namespace :sdd + resources :features, only: [:create]
curl — JSON example with description, tech_stack, checklist_topics
On-disk layout — one tree listing under memory/, specs/…, .specify/ (single checklists/ subtree, no duplication)
Sdd::RunPipelineJob — optional ApplicationJob example
Other fixes
Gemfile path updated to spec_kit_ruby (matches this repo).
Development section now uses cd spec_kit_ruby.
The new section is inserted after Configuration and before Usage, so install → config → full example → rake/snippets.

If you want, we can add a second example that uses only write_to_disk: false and returns markdown in the JSON body (preview mode).

Then run:

bundle install
bundle exec rails g spec_kit:install

The generator creates:

  • config/initializers/spec_kit.rb — wire-up for your RubyLLM defaults
  • memory/.keep — where constitution.md will live
  • specs/.keep — where feature directories will live

Configuration

Open config/initializers/spec_kit.rb:

SpecKit.configure do |config|
  # spec_kit auto-detects Rails.root via the railtie. Override only if needed.
  # config.project_root = Rails.root.join("workspace")

  # `:sequential` (001-name) or `:timestamp` (YYYYMMDD-HHMMSS-name)
  config.branch_numbering = :sequential

  # Optional: try to create + check out a git branch named after each feature.
  config.use_git = true

  # IMPORTANT: the chat factory. Returning a fresh chat per invocation is
  # recommended because RubyLLM chats accumulate context across `#ask` calls.
  config.default_chat = -> { RubyLLM.chat(model: ENV.fetch("OPENAI_MODEL", "gpt-4o")) }
end

spec_kit does not configure RubyLLM itself. Set up RubyLLM.configure in its own initializer (with your API keys, custom adapters, observability, etc.) — spec_kit just calls into whatever you already have.

Complete implementation example (Rails + RubyLLM)

This section wires RubyLLM, spec_kit, a service object that runs the full SDD pipeline, and a minimal API controller. Adjust names, auth, and error handling to match your app.

1. Install the generator layout

bundle install
bundle exec rails g spec_kit:install

You should have config/initializers/spec_kit.rb, memory/, and specs/.

2. RubyLLM initializer

Create config/initializers/ruby_llm.rb. The exact options depend on your provider; see the RubyLLM configuration docs.

# frozen_string_literal: true

# Minimal example — set keys for your provider (OpenAI, Anthropic, etc.).
RubyLLM.configure do |config|
  config.openai_api_key = ENV.fetch("OPENAI_API_KEY")
  # config.anthropic_api_key = ENV.fetch("ANTHROPIC_API_KEY") # if you use Claude
end

3. spec_kit initializer

Edit config/initializers/spec_kit.rb (created by the generator) so every workflow gets a fresh chat. RubyLLM chats accumulate context across #ask calls, so do not reuse one chat across workflows unless you intend that.

# frozen_string_literal: true

SpecKit.configure do |config|
  config.branch_numbering = :sequential
  config.use_git          = false # set true if you want a git branch per feature
  config.default_chat = lambda do
    RubyLLM.chat(model: ENV.fetch("OPENAI_MODEL", "gpt-4o"))
  end
end

With this in place you can call SpecKit::Workflows.run(:specify, arguments: "...") without passing chat: every time. You can still pass chat: to override per request.

4. Service: run the full pipeline

Create app/services/sdd/pipeline.rb:

# frozen_string_literal: true

module Sdd
  # Runs Spec-Driven Development workflows in order and returns paths to the
  # artifacts under `specs/<feature-dir>/`. Uses `SpecKit.configuration.default_chat`
  # unless you inject `chat:` per call.
  class Pipeline
    Error = Class.new(StandardError)

    def initialize(project: SpecKit::Project.new, chat: nil)
      @project = project
      @chat    = chat
    end

    # @param description [String] natural-language feature idea (required)
    # @param tech_stack [String, nil] notes for Plan (framework, DB, etc.)
    # @param topics [Array<String>] extra checklist topics, e.g. %w[security accessibility]
    # @return [Hash] :feature (directory name) and :paths (symbol => string path)
    def run_full!(description:, tech_stack: nil, topics: [])
      raise Error, "description is required" if description.to_s.strip.empty?

      feature = SpecKit::Workflows.run(
        :specify,
        project: @project,
        chat: @chat,
        arguments: description
      ).feature

      SpecKit::Workflows.run(:plan,       project: @project, chat: @chat, feature: feature, tech_stack: tech_stack)
      SpecKit::Workflows.run(:tasks,      project: @project, chat: @chat, feature: feature)
      SpecKit::Workflows.run(:analyze,    project: @project, chat: @chat, feature: feature)
      SpecKit::Workflows.run(:implement,  project: @project, chat: @chat, feature: feature)

      topics.each do |topic|
        SpecKit::Workflows.run(:checklist, project: @project, chat: @chat, feature: feature, topic: topic)
      end

      {
        feature: feature.directory_name,
        paths: {
          spec: feature.spec_path.to_s,
          plan: feature.plan_path.to_s,
          tasks: feature.tasks_path.to_s,
          analysis: feature.analysis_path.to_s,
          implementation: feature.directory.join("implementation.md").to_s
        }
      }
    end
  end
end

Optional one-shot constitution (run once per repo or when principles change):

SpecKit::Workflows.run(
  :constitution,
  arguments: "Test-first, small PRs, observability on errors, prefer boring Rails patterns."
)

5. HTTP API controller

Create app/controllers/sdd/features_controller.rb:

# frozen_string_literal: true

module Sdd
  class FeaturesController < ApplicationController
    # Skip or replace with your authentication.
    skip_before_action :verify_authenticity_token, if: -> { request.format.json? }

    def create
      result = Sdd::Pipeline.new.run_full!(
        description: params.require(:description),
        tech_stack: params[:tech_stack],
        topics: Array(params[:checklist_topics]).presence || []
      )
      render json: result, status: :created
    rescue Sdd::Pipeline::Error => e
      render json: { error: e.message }, status: :unprocessable_entity
    rescue SpecKit::Error => e
      render json: { error: e.message }, status: :unprocessable_entity
    end
  end
end

Add routes in config/routes.rb:

namespace :sdd do
  resources :features, only: [:create]
end

6. Try it with curl

export OPENAI_API_KEY=...
export OPENAI_MODEL=gpt-4o

curl -sS -X POST http://localhost:3000/sdd/features \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Users can organize photos into albums and reorder them by drag-and-drop on one page.",
    "tech_stack": "Ruby on Rails 7.1, PostgreSQL, Hotwire, Active Storage",
    "checklist_topics": ["security", "accessibility"]
  }'

7. What appears on disk

After a successful run you will have something like:

memory/
  constitution.md              # if you ran the :constitution workflow
specs/
  001-photo-albums/            # exact name depends on description & numbering
    spec.md
    plan.md
    research.md
    data-model.md
    quickstart.md
    tasks.md
    analysis.md
    implementation.md
    checklists/
      requirements.md          # from Specify
      security.md              # from Checklist topic "security"
      accessibility.md       # from Checklist topic "accessibility"
.specify/
  feature.json                 # “current” feature pointer for rake tasks

8. Optional: background job for long runs

# frozen_string_literal: true

class Sdd::RunPipelineJob < ApplicationJob
  queue_as :default

  def perform(description, tech_stack = nil, checklist_topics = [])
    Sdd::Pipeline.new.run_full!(
      description: description,
      tech_stack: tech_stack,
      topics: Array(checklist_topics)
    )
    # Notify user (email, Slack, Action Cable) with paths or attach to a record.
  end
end

# Sdd::RunPipelineJob.perform_later("Build invoicing…", "Rails 7, Stripe", %w[security])

This example is intentionally focused on wiring. Add authorization, idempotency, storing feature IDs in Active Record, and streaming responses if users should watch progress.

Usage

From a rake task

# Generate / update the project constitution
bundle exec rake "spec_kit:constitution[Code quality, Test-first, Rails idioms]"

# Spec a new feature
bundle exec rake "spec_kit:specify[Build a photo album organizer with drag-and-drop reordering]"

# Plan + research/data-model/quickstart for the most-recent feature
bundle exec rake "spec_kit:plan[Ruby on Rails 7, PostgreSQL, Stimulus]"

# Break the plan into tasks
bundle exec rake spec_kit:tasks

# Cross-check spec/plan/tasks for gaps
bundle exec rake spec_kit:analyze

# Add a domain-specific checklist
bundle exec rake "spec_kit:checklist[security]"

# Generate the implementation guide
bundle exec rake spec_kit:implement

After spec_kit:specify, .specify/feature.json records the most-recently created feature so the subsequent plan/tasks/analyze/implement tasks operate on it without you naming it. Pass an explicit feature name as the second argument when you need to target an older one:

bundle exec rake "spec_kit:plan[Ruby + Rails,001-photo-albums]"

From Ruby (controllers, jobs, services)

The whole API surface is Ruby — call workflows wherever it makes sense in your app:

class FeaturesController < ApplicationController
  def create
    project = SpecKit::Project.new
    chat    = RubyLLM.chat(model: "gpt-4o")

    result = SpecKit::Workflows::Specify.new(project: project, chat: chat)
                                        .call(arguments: params[:description])

    render json: {
      feature:  result.feature.directory_name,
      spec:     result.path_for(:spec).to_s,
      content:  result.content_for(:spec)
    }
  end
end

Each workflow returns a SpecKit::Workflows::Result:

result.feature                       # SpecKit::Feature (or nil for Constitution)
result.path_for(:spec)               # Pathname to the written file
result.content_for(:spec)            # The raw markdown
result.metadata[:verdict]            # Workflow-specific extras (analyze, checklist, …)

Background-job example

class GenerateFeaturePlanJob < ApplicationJob
  queue_as :default

  def perform(feature_directory)
    feature = SpecKit::Project.new.feature(feature_directory)
    SpecKit::Workflows.run(:plan,
                           feature: feature,
                           tech_stack: "Ruby on Rails 7 + PostgreSQL",
                           chat: RubyLLM.chat(model: "gpt-4o-mini"))
  end
end

Read-only mode for previews

Set config.write_to_disk = false (or pass an LLM-only chat) to generate artifacts without touching the filesystem — handy for "preview before save" UX:

SpecKit.configuration.write_to_disk = false
result = SpecKit::Workflows::Specify.new(project: project, chat: chat)
                                    .call(arguments: "...")
preview = result.content_for(:spec)

The eight workflows

Workflow Inputs Outputs
SpecKit::Workflows::Constitution a directive string memory/constitution.md
SpecKit::Workflows::Specify a feature description specs/NNN-name/spec.md, …/checklists/requirements.md
SpecKit::Workflows::Clarify a feature structured questions; later: revised spec.md
SpecKit::Workflows::Plan a feature, optional tech-stack notes plan.md, research.md, data-model.md, quickstart.md
SpecKit::Workflows::Tasks a feature with a plan tasks.md
SpecKit::Workflows::Analyze a feature with spec+plan+tasks analysis.md plus a :verdict metadata key
SpecKit::Workflows::Checklist a feature, a topic checklists/<topic>.md
SpecKit::Workflows::Implement a feature with tasks implementation.md

Bundled templates and prompts

spec_kit ships verbatim copies of the original Spec Kit's template and prompt files under data/spec_kit/:

  • Templates (skeletons populated by the LLM): spec-template.md, plan-template.md, tasks-template.md, constitution-template.md, checklist-template.md.
  • Prompts (system instructions for each workflow): specify.md, plan.md, tasks.md, clarify.md, analyze.md, checklist.md, implement.md, constitution.md.

To override any of them per-project, point Configuration#template_overrides (or #prompt_overrides) at your local copy:

SpecKit.configure do |config|
  config.template_overrides = {
    spec: Rails.root.join("config/spec_kit/spec-template.md").to_s
  }
  config.prompt_overrides = {
    plan: Rails.root.join("config/spec_kit/plan.md").to_s
  }
end

Bringing your own chat object

SpecKit::Llm adapts to any RubyLLM-shaped object. The duck-typed contract is:

  1. #with_instructions(text) returning self, then #ask(prompt) returning a response (RubyLLM ≥ 1.x).
  2. #ask(prompt, system: text) for custom adapters.
  3. #call(prompt:, system:) for proc-shaped fakes (used internally by tests).

The response can be a String, anything with #content, or a Hash with :content. This means:

  • You can swap RubyLLM for a stubbed chat in tests (see spec/support/fake_chat.rb).
  • You can wrap RubyLLM in observability / retry / cost-tracking middleware and still pass the wrapped object straight to spec_kit.

Compatibility with the original Spec Kit

A specs/NNN-feature-name/ directory produced by spec_kit is byte-compatible with the layout the original Spec Kit produces — same filenames, same headings, same template structure. You can use both tools on the same repo without conflict.

The reverse is also true: if a teammate ran specify init first and produced an existing specs/ tree, spec_kit will pick it up: Project#features will list the existing directories, and Project#current_feature will respect a pre-existing .specify/feature.json.

Development

cd spec_kit_ruby
bin/setup
bundle exec rspec
bundle exec ruby -Ilib bin/console

License

MIT. Portions of the bundled templates and prompt content are © GitHub, Inc. and used under the terms of the original Spec Kit MIT license. See LICENSE.

About

Spec Kit for Ruby

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages