plan_driven 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -1
  3. data/README.md +339 -31
  4. data/app/controllers/plan_driven/wizard/application_controller.rb +59 -0
  5. data/app/controllers/plan_driven/wizard/configuration_controller.rb +62 -0
  6. data/app/controllers/plan_driven/wizard/jobs_controller.rb +16 -0
  7. data/app/controllers/plan_driven/wizard/plans_controller.rb +102 -0
  8. data/app/views/layouts/plan_driven/wizard/application.html.erb +139 -0
  9. data/app/views/plan_driven/wizard/configuration/show.html.erb +104 -0
  10. data/app/views/plan_driven/wizard/plans/_agents.html.erb +44 -0
  11. data/app/views/plan_driven/wizard/plans/_approve.html.erb +40 -0
  12. data/app/views/plan_driven/wizard/plans/_finish.html.erb +24 -0
  13. data/app/views/plan_driven/wizard/plans/_plan.html.erb +52 -0
  14. data/app/views/plan_driven/wizard/plans/_tickets.html.erb +42 -0
  15. data/app/views/plan_driven/wizard/plans/index.html.erb +31 -0
  16. data/app/views/plan_driven/wizard/plans/new.html.erb +21 -0
  17. data/app/views/plan_driven/wizard/plans/show.html.erb +36 -0
  18. data/config/routes.rb +16 -0
  19. data/exe/plan-driven +1 -0
  20. data/lib/generators/plan_driven/install_generator.rb +7 -0
  21. data/lib/generators/plan_driven/templates/plan_driven.rb +10 -1
  22. data/lib/plan_driven/cli/config_commands.rb +60 -0
  23. data/lib/plan_driven/cli/plan_commands.rb +6 -2
  24. data/lib/plan_driven/cli/setup_commands.rb +10 -0
  25. data/lib/plan_driven/cli/ticket_commands.rb +23 -3
  26. data/lib/plan_driven/cli/ui.rb +41 -2
  27. data/lib/plan_driven/cli.rb +20 -4
  28. data/lib/plan_driven/configuration.rb +31 -3
  29. data/lib/plan_driven/connections.rb +69 -0
  30. data/lib/plan_driven/cursor_agents.rb +10 -2
  31. data/lib/plan_driven/delivery.rb +23 -5
  32. data/lib/plan_driven/evidence.rb +5 -1
  33. data/lib/plan_driven/github.rb +4 -0
  34. data/lib/plan_driven/guards/migration_guard.rb +86 -9
  35. data/lib/plan_driven/guards/ticket_guard.rb +1 -2
  36. data/lib/plan_driven/interview.rb +161 -0
  37. data/lib/plan_driven/local_agents.rb +254 -0
  38. data/lib/plan_driven/renderer/markdown.rb +26 -1
  39. data/lib/plan_driven/template.rb +7 -2
  40. data/lib/plan_driven/usage.rb +114 -0
  41. data/lib/plan_driven/version.rb +1 -1
  42. data/lib/plan_driven/wizard/engine.rb +19 -0
  43. data/lib/plan_driven/wizard.rb +245 -0
  44. data/lib/plan_driven.rb +5 -0
  45. metadata +23 -1
@@ -23,6 +23,7 @@ module PlanDriven
23
23
  parts << "## Tickets and pull requests\n\n#{delivery_table(plan)}"
24
24
  parts << "## Acceptance criteria and proof\n\n#{Evidence.matrix_markdown(plan)}"
25
25
  parts << "## Checks run on each pull request\n\n#{guard_findings(plan)}"
26
+ parts << "## Tokens and cost\n\n#{usage_table(plan)}"
26
27
  parts << "## Approvals\n\n#{approval_history(plan)}"
27
28
  parts << "## Timeline\n\n#{timeline(plan)}"
28
29
  parts << "## The approved plan\n\nThe plan this delivery implements is in `plan.md`, revision #{plan.revision}."
@@ -116,8 +117,32 @@ module PlanDriven
116
117
  table(%w[When Subject Role Decision By Note], rows)
117
118
  end
118
119
 
120
+ def usage_table(plan)
121
+ rows = Usage.rows(plan)
122
+ return "No token usage recorded." if rows.empty?
123
+
124
+ "#{table(%w[Step Ticket Model Input Output Cache Time Cost], rows.map { |row| usage_row(row) })}\n\n" \
125
+ "#{usage_total(Usage.totals(rows))}"
126
+ end
127
+
128
+ def usage_row(row)
129
+ tokens = row.tokens.transform_values { |count| Usage.format_tokens(count) }
130
+ cache = Usage.format_tokens(row.tokens["cache_write_tokens"] + row.tokens["cache_read_tokens"])
131
+ time = row.duration_ms ? "#{(row.duration_ms / 60_000.0).round(1)} min" : "-"
132
+ [row.step.to_s, row.ticket || "-", row.model.to_s, tokens["input_tokens"], tokens["output_tokens"], cache, time,
133
+ Usage.format_cost(row.cost)]
134
+ end
135
+
136
+ def usage_total(totals)
137
+ tokens = Usage.format_tokens(totals[:total_tokens])
138
+ return "**Total: #{tokens} tokens, #{Usage.format_cost(totals[:cost])}**" if totals[:cost]
139
+
140
+ "**Total: #{tokens} tokens.** No price is set for #{totals[:unpriced].join(", ")}; " \
141
+ "add it to `config.token_prices` to see dollars."
142
+ end
143
+
119
144
  def timeline(plan)
120
- plan.events.map do |event|
145
+ plan.events.where.not(name: %w[llm.usage agent.usage]).map do |event|
121
146
  target = event.ticket ? " #{event.ticket.key}" : ""
122
147
  "- #{event.created_at.strftime("%-d %b %Y %H:%M")} · #{event.name}#{target} · #{event.actor}"
123
148
  end.join("\n")
@@ -15,6 +15,11 @@ module PlanDriven
15
15
  def drafted?
16
16
  source == :draft
17
17
  end
18
+
19
+ # The question as the interview shows it, in the terminal and in the wizard.
20
+ def prompt
21
+ required ? question.to_s : "#{question} (optional)"
22
+ end
18
23
  end
19
24
 
20
25
  GROUPS = ["Overview", "Background", "Architectural changes", "Work overview", "Risks", "Testing"].freeze
@@ -68,7 +73,7 @@ module PlanDriven
68
73
  { key: "when", title: "When", group: "Overview", source: :ask, required: false, min_words: 0,
69
74
  question: "When is it needed? (date, milestone, or leave empty)", guidance: "Target date or milestone." },
70
75
  { key: "background", title: "Background", group: "Background", source: :ask, required: false, min_words: 0,
71
- question: "Links, PRDs, existing tickets, earlier decisions (optional)",
76
+ question: "Links, PRDs, existing tickets, earlier decisions",
72
77
  guidance: "Links, product documents, related tickets and earlier decisions." },
73
78
  { key: "existing_data_structure", title: "Existing Data Structure", group: "Background", source: :draft,
74
79
  required: true, min_words: 30,
@@ -91,7 +96,7 @@ module PlanDriven
91
96
  source: :draft, required: true, min_words: 5,
92
97
  guidance: "Queues, external services, feature flags and rollout order. Say so when there are none." },
93
98
  { key: "out_of_scope", title: "Out of Scope", group: "Work overview", source: :ask, required: false,
94
- min_words: 0, question: "What is explicitly out of scope? (optional)",
99
+ min_words: 0, question: "What is explicitly out of scope?",
95
100
  guidance: "What this plan deliberately doesn't do." },
96
101
  { key: "risks", title: "Risks", group: "Risks", source: :draft, required: true, min_words: 15,
97
102
  guidance: "The main risks as a list, and how each is mitigated." },
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanDriven
4
+ # What a plan cost to deliver: tokens for every model call and agent run, recorded as events
5
+ # (`llm.usage`, `agent.usage`), and dollars when config.token_prices has a price for the model.
6
+ # Prices change and differ per account, so the gem ships none: put in what your provider charges.
7
+ module Usage
8
+ FIELDS = %w[input_tokens output_tokens cache_write_tokens cache_read_tokens].freeze
9
+
10
+ Row = Struct.new(:step, :ticket, :model, :calls, :tokens, :duration_ms, :cost, keyword_init: true) do
11
+ def total_tokens
12
+ tokens.values.sum
13
+ end
14
+ end
15
+
16
+ module_function
17
+
18
+ # Dollars for these tokens, or nil when the model has no price. Prices are per million tokens:
19
+ # config.token_prices = { "your-model-id" => { input: 3.0, output: 15.0, cache_write: 3.75, cache_read: 0.3 } }
20
+ def cost(model, tokens, config: PlanDriven.configuration)
21
+ price = config.token_prices.to_h.transform_keys(&:to_s)[model.to_s] or return
22
+ price = price.transform_keys(&:to_s)
23
+ FIELDS.sum { |field| tokens[field].to_i * price.fetch(field.delete_suffix("_tokens"), 0).to_f } / 1_000_000.0
24
+ end
25
+
26
+ # What the metered model spent in one step, as an `llm.usage` event.
27
+ def record_llm(plan, llm, step, actor:)
28
+ spent = llm.take
29
+ return if spent["calls"].zero?
30
+
31
+ plan.log!("llm.usage", actor: actor, step: step, model: llm.model, **spent.symbolize_keys)
32
+ end
33
+
34
+ # One finished agent run. Usage is bookkeeping: a provider that can't report it mustn't stop
35
+ # the ticket moving.
36
+ def record_agent(ticket, run, agents, actor:, config: PlanDriven.configuration)
37
+ tokens = agents.respond_to?(:usage) ? agents.usage(ticket.agent_id, run.id) : {}
38
+ step = ticket.plan.events.exists?(name: "agent.usage", ticket: ticket) ? "agent follow-up" : "agent run"
39
+ ticket.plan.log!("agent.usage", actor: actor, ticket: ticket, step: step, run: run.id,
40
+ model: config.agent_model || "#{config.agent_provider} default",
41
+ calls: 1, duration_ms: run.duration_ms, **tokens.to_h.symbolize_keys)
42
+ rescue Error => e
43
+ ticket.plan.log!("agent.usage", actor: actor, ticket: ticket, run: run.id, error: e.message[0, 200])
44
+ end
45
+
46
+ def tokens_from(payload)
47
+ FIELDS.to_h { |field| [field, payload.to_h[field].to_i] }
48
+ end
49
+
50
+ def rows(plan, config: PlanDriven.configuration)
51
+ plan.events.where(name: %w[llm.usage agent.usage]).order(:created_at).map do |event|
52
+ payload = event.payload.to_h
53
+ tokens = tokens_from(payload)
54
+ Row.new(step: payload["step"], ticket: event.ticket&.key, model: payload["model"], calls: payload["calls"].to_i,
55
+ tokens: tokens, duration_ms: payload["duration_ms"],
56
+ cost: cost(payload["model"], tokens, config: config))
57
+ end
58
+ end
59
+
60
+ def totals(rows)
61
+ tokens = FIELDS.to_h { |field| [field, rows.sum { |row| row.tokens[field] }] }
62
+ costs = rows.map(&:cost)
63
+ { tokens: tokens, total_tokens: tokens.values.sum, cost: costs.any?(&:nil?) ? nil : costs.sum,
64
+ priced_cost: costs.compact.sum, unpriced: rows.select { |row| row.cost.nil? }.map(&:model).uniq }
65
+ end
66
+
67
+ def format_tokens(count)
68
+ count >= 1_000_000 ? format("%.2fM", count / 1_000_000.0) : count.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
69
+ end
70
+
71
+ def format_cost(cost)
72
+ cost ? format("$%.2f", cost) : "-"
73
+ end
74
+ end
75
+
76
+ # Wraps any model client and counts what each step spends, so Delivery can record it.
77
+ class MeteredLLM
78
+ def initialize(llm)
79
+ @llm = llm
80
+ reset
81
+ end
82
+
83
+ def chat(**options)
84
+ reply = @llm.chat(**options)
85
+ @calls += 1
86
+ @input += reply.input_tokens.to_i
87
+ @output += reply.output_tokens.to_i
88
+ reply
89
+ end
90
+
91
+ def label
92
+ @llm.label
93
+ end
94
+
95
+ def model
96
+ label.split("/", 2).last
97
+ end
98
+
99
+ # What was spent since the last take, as an event payload.
100
+ def take
101
+ spent = { "calls" => @calls, "input_tokens" => @input, "output_tokens" => @output }
102
+ reset
103
+ spent
104
+ end
105
+
106
+ private
107
+
108
+ def reset
109
+ @calls = 0
110
+ @input = 0
111
+ @output = 0
112
+ end
113
+ end
114
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanDriven
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanDriven
4
+ module Wizard
5
+ # Mounted by the install generator, in development only:
6
+ #
7
+ # mount PlanDriven::Wizard::Engine, at: "/plan_driven" if Rails.env.development?
8
+ class Engine < ::Rails::Engine
9
+ isolate_namespace PlanDriven::Wizard
10
+ engine_name "plan_driven_wizard"
11
+ config.root = File.expand_path("../../..", __dir__)
12
+
13
+ # Keys pasted on the Configuration page go to `plan-driven connect` on stdin, never to the log.
14
+ initializer "plan_driven_wizard.filter_parameters" do |app|
15
+ app.config.filter_parameters += [:api_key]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "open3"
6
+ require "securerandom"
7
+ require "shellwords"
8
+
9
+ module PlanDriven
10
+ # The browser wizard at /plan_driven. Every button runs a real `plan-driven` command in the
11
+ # background and streams its output to the page, so the wizard can do nothing the CLI can't,
12
+ # and the terminal panel shows exactly which command ran.
13
+ module Wizard
14
+ # The commands the wizard may run, built from form fields. Nothing else reaches a shell:
15
+ # arguments are passed as an array, never interpolated.
16
+ module Commands
17
+ PLAN_ONLY = %w[check submit pdf status evidence report usage log].freeze
18
+
19
+ BUILDERS = {
20
+ "doctor" => ->(_p) { [] },
21
+ "new" => ->(p) { [required(p, "title")] },
22
+ "edit" => ->(p) { [plan(p), section(p), "--from", required(p, "file")] },
23
+ "redraft" => ->(p) { [plan(p), section(p), required(p, "instruction")] },
24
+ "approve" => ->(p) { [plan(p), *role(p), *note(p)] },
25
+ "reject" => ->(p) { [plan(p), *role(p), "--note", required(p, "note")] },
26
+ "tickets" => ->(p) { [plan(p), *p["instruction"].to_s.strip.presence] },
27
+ "approve-tickets" => ->(p) { [plan(p), *role(p)] },
28
+ "develop" => ->(p) { [plan(p), *Array(p["tickets"]).map { |key| ticket_key(key) }] },
29
+ "review" => ->(p) { [ticket(p)] },
30
+ "merge" => ->(p) { [ticket(p)] },
31
+ "approve-pr" => ->(p) { [ticket(p), *note(p)] },
32
+ "feedback" => ->(p) { [ticket(p), required(p, "feedback")] },
33
+ "questions" => ->(_p) { [] },
34
+ "question" => ->(p) { [question_key(p), *question_flags(p)] },
35
+ # The key itself goes on stdin, so it's never in the command line or the panel.
36
+ "connect" => ->(p) { [Connections.find(required(p, "service")).name] }
37
+ }.merge(PLAN_ONLY.to_h { |name| [name, ->(p) { [plan(p)] }] }).freeze
38
+
39
+ module_function
40
+
41
+ def argv(action, params)
42
+ builder = BUILDERS[action.to_s] or raise ArgumentError, "the wizard can't run `#{action}`"
43
+ [action.to_s, *builder.call(params.to_h.transform_keys(&:to_s)), "--yes"]
44
+ end
45
+
46
+ # What `new` reads on stdin. The interview takes each answer up to an empty line, so blank
47
+ # lines inside an answer are folded away, and an empty answer is the empty line alone.
48
+ def interview_input(template, answers)
49
+ template.asked.map do |section|
50
+ answer = answers[section.key].to_s.strip.gsub(/\r\n?/, "\n").gsub(/\n\s*\n+/, "\n")
51
+ answer.empty? ? "\n" : "#{answer}\n\n"
52
+ end.join
53
+ end
54
+
55
+ def required(params, name)
56
+ params[name].to_s.strip.presence or raise ArgumentError, "#{name.tr("_", " ")} is required"
57
+ end
58
+
59
+ def plan(params)
60
+ key = required(params, "plan").upcase
61
+ raise ArgumentError, "not a plan key" unless key.match?(/\A[A-Z]+-\d+\z/)
62
+
63
+ key
64
+ end
65
+
66
+ def ticket(params)
67
+ "#{plan(params)}/#{ticket_key(required(params, "ticket"))}"
68
+ end
69
+
70
+ def ticket_key(key)
71
+ key.to_s.upcase.tap { |value| raise ArgumentError, "not a ticket key" unless value.match?(/\AT\d+\z/) }
72
+ end
73
+
74
+ def section(params)
75
+ key = required(params, "section")
76
+ PlanDriven.configuration.template[key] or raise ArgumentError, "unknown section #{key}"
77
+ key
78
+ end
79
+
80
+ def role(params)
81
+ params["role"].to_s.strip.empty? ? [] : ["--as", params["role"].strip]
82
+ end
83
+
84
+ def note(params)
85
+ params["note"].to_s.strip.empty? ? [] : ["--note", params["note"].strip]
86
+ end
87
+
88
+ def question_key(params)
89
+ required(params, "key").tap do |key|
90
+ raise ArgumentError, "not a question key" unless key.match?(Interview::KEY)
91
+ end
92
+ end
93
+
94
+ def question_flags(params)
95
+ return ["--remove"] if params["remove"].to_s == "1"
96
+
97
+ flags = []
98
+ flags += ["--title", params["title"].strip] if params["title"].to_s.strip != ""
99
+ flags += ["--ask", params["question"].strip] if params["question"].to_s.strip != ""
100
+ flags += ["--group", params["group"]] if params["group"].to_s != ""
101
+ flags << "--required" if params["required"].to_s == "1"
102
+ flags << "--optional" if params["required"].to_s == "0"
103
+ flags
104
+ end
105
+ end
106
+
107
+ # One command run: its output in a log file, its state in a JSON file next to it, so any
108
+ # request (and any Puma thread) can read how it's going.
109
+ class Job
110
+ # About as many characters as fit on a line of the terminal panel, for tables.
111
+ PANEL_COLUMNS = 76
112
+ SERVER_ENV = %w[RAILS_ENV RACK_ENV PORT PIDFILE].freeze
113
+
114
+ attr_reader :id
115
+
116
+ def self.dir(root = PlanDriven.configuration.root_path)
117
+ root.join("tmp/plan_driven/wizard").tap { |path| FileUtils.mkdir_p(path) }
118
+ end
119
+
120
+ def self.find(id, root: PlanDriven.configuration.root_path)
121
+ raise ArgumentError, "not a job" unless id.to_s.match?(/\A[a-f0-9]{16}\z/)
122
+
123
+ new(id, root: root).tap { |job| raise ArgumentError, "no job #{id}" unless job.state_file.exist? }
124
+ end
125
+
126
+ # Starts `argv` in a thread and returns at once.
127
+ def self.start(argv, stdin: nil, actor: nil, root: PlanDriven.configuration.root_path)
128
+ job = new(SecureRandom.hex(8), root: root)
129
+ job.begin!(argv, stdin: stdin, actor: actor)
130
+ job
131
+ end
132
+
133
+ def initialize(id, root:)
134
+ @id = id
135
+ @root = Pathname(root)
136
+ end
137
+
138
+ def begin!(argv, stdin:, actor:)
139
+ command = executable + argv
140
+ write_state("command" => display(command, actor), "status" => "running", "started_at" => Time.now.to_i)
141
+ File.write(log_file, "")
142
+ Thread.new { execute(command, stdin, actor) }
143
+ end
144
+
145
+ def state
146
+ JSON.parse(state_file.read)
147
+ end
148
+
149
+ def output
150
+ log_file.exist? ? log_file.read.force_encoding(Encoding::UTF_8).scrub : ""
151
+ end
152
+
153
+ def to_h
154
+ state.merge("id" => id, "output" => output)
155
+ end
156
+
157
+ def state_file
158
+ self.class.dir(@root).join("#{id}.json")
159
+ end
160
+
161
+ def log_file
162
+ self.class.dir(@root).join("#{id}.log")
163
+ end
164
+
165
+ private
166
+
167
+ def execute(command, stdin, actor)
168
+ env = { "LANG" => "en_US.UTF-8", "LC_ALL" => "en_US.UTF-8", "NO_COLOR" => "1", "COLUMNS" => PANEL_COLUMNS.to_s }
169
+ # The server's own environment stays with the server: a command runs as it would when typed.
170
+ SERVER_ENV.each { |name| env[name] = nil }
171
+ env["PLAN_DRIVEN_ACTOR"] = actor if actor.present?
172
+ status = Open3.popen2e(env, *command, chdir: @root.to_s) do |input, output, thread|
173
+ input.write(stdin.to_s)
174
+ input.close
175
+ File.open(log_file, "a") do |log|
176
+ output.each_char do |char|
177
+ log.write(char)
178
+ log.flush if char == "\n"
179
+ end
180
+ end
181
+ thread.value
182
+ end
183
+ finish(status.exitstatus)
184
+ rescue StandardError => e
185
+ File.write(log_file, "\n✗ #{e.message}\n", mode: "a")
186
+ finish(1)
187
+ end
188
+
189
+ def finish(code)
190
+ write_state(state.merge("status" => code.to_i.zero? ? "succeeded" : "failed", "exit" => code,
191
+ "finished_at" => Time.now.to_i))
192
+ end
193
+
194
+ def write_state(data)
195
+ File.write(state_file, JSON.generate(data))
196
+ end
197
+
198
+ def executable
199
+ binstub = @root.join("bin/plan-driven")
200
+ binstub.exist? ? ["bin/plan-driven"] : %w[bundle exec plan-driven]
201
+ end
202
+
203
+ def display(command, actor)
204
+ prefix = actor.present? ? "PLAN_DRIVEN_ACTOR=#{Shellwords.escape(actor)} " : ""
205
+ shown = command.map { |arg| arg.start_with?(@root.to_s) ? arg.delete_prefix("#{@root}/") : arg }
206
+ prefix + Shellwords.join(shown)
207
+ end
208
+ end
209
+
210
+ # The wizard's pages, in order, and which ones a plan in a given phase has reached.
211
+ module Steps
212
+ ORDER = [
213
+ ["plan", "Plan", "Read it, edit or redraft any section, then submit"],
214
+ ["approve", "Approve", "Every role signs off on this revision"],
215
+ ["tickets", "Tickets", "Draft the tickets, steer them, approve them"],
216
+ ["agents", "Agents & PRs", "One agent and one pull request per ticket"],
217
+ ["finish", "Proof & report", "Evidence, the delivery report, tokens and cost"]
218
+ ].freeze
219
+
220
+ REACHED = {
221
+ "draft" => "plan", "in_review" => "approve", "approved" => "tickets", "ticketed" => "tickets",
222
+ "tickets_approved" => "agents", "in_development" => "agents", "delivered" => "finish"
223
+ }.freeze
224
+
225
+ module_function
226
+
227
+ def keys
228
+ ORDER.map(&:first)
229
+ end
230
+
231
+ def current(plan)
232
+ REACHED.fetch(plan.status, "plan")
233
+ end
234
+
235
+ def reached?(plan, step)
236
+ keys.index(step).to_i <= keys.index(current(plan)).to_i
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ if defined?(Rails::Railtie)
243
+ require "rails/engine"
244
+ require_relative "wizard/engine"
245
+ end
data/lib/plan_driven.rb CHANGED
@@ -12,6 +12,8 @@ require_relative "plan_driven/errors"
12
12
  require_relative "plan_driven/credentials"
13
13
  require_relative "plan_driven/configuration"
14
14
  require_relative "plan_driven/template"
15
+ require_relative "plan_driven/interview"
16
+ require_relative "plan_driven/connections"
15
17
  require_relative "plan_driven/schema_context"
16
18
  require_relative "plan_driven/workflow"
17
19
  require_relative "plan_driven/models"
@@ -26,10 +28,13 @@ require_relative "plan_driven/agent_prompt"
26
28
  require_relative "plan_driven/http"
27
29
  require_relative "plan_driven/github"
28
30
  require_relative "plan_driven/cursor_agents"
31
+ require_relative "plan_driven/local_agents"
32
+ require_relative "plan_driven/usage"
29
33
  require_relative "plan_driven/repository"
30
34
  require_relative "plan_driven/renderer"
31
35
  require_relative "plan_driven/evidence"
32
36
  require_relative "plan_driven/delivery"
37
+ require_relative "plan_driven/wizard"
33
38
  require_relative "plan_driven/railtie" if defined?(Rails::Railtie)
34
39
 
35
40
  module PlanDriven
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plan_driven
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Blažević
@@ -66,6 +66,21 @@ files:
66
66
  - CHANGELOG.md
67
67
  - LICENSE.txt
68
68
  - README.md
69
+ - app/controllers/plan_driven/wizard/application_controller.rb
70
+ - app/controllers/plan_driven/wizard/configuration_controller.rb
71
+ - app/controllers/plan_driven/wizard/jobs_controller.rb
72
+ - app/controllers/plan_driven/wizard/plans_controller.rb
73
+ - app/views/layouts/plan_driven/wizard/application.html.erb
74
+ - app/views/plan_driven/wizard/configuration/show.html.erb
75
+ - app/views/plan_driven/wizard/plans/_agents.html.erb
76
+ - app/views/plan_driven/wizard/plans/_approve.html.erb
77
+ - app/views/plan_driven/wizard/plans/_finish.html.erb
78
+ - app/views/plan_driven/wizard/plans/_plan.html.erb
79
+ - app/views/plan_driven/wizard/plans/_tickets.html.erb
80
+ - app/views/plan_driven/wizard/plans/index.html.erb
81
+ - app/views/plan_driven/wizard/plans/new.html.erb
82
+ - app/views/plan_driven/wizard/plans/show.html.erb
83
+ - config/routes.rb
69
84
  - exe/plan-driven
70
85
  - lib/generators/plan_driven/install_generator.rb
71
86
  - lib/generators/plan_driven/templates/create_plan_driven_tables.rb.tt
@@ -73,11 +88,13 @@ files:
73
88
  - lib/plan_driven.rb
74
89
  - lib/plan_driven/agent_prompt.rb
75
90
  - lib/plan_driven/cli.rb
91
+ - lib/plan_driven/cli/config_commands.rb
76
92
  - lib/plan_driven/cli/plan_commands.rb
77
93
  - lib/plan_driven/cli/setup_commands.rb
78
94
  - lib/plan_driven/cli/ticket_commands.rb
79
95
  - lib/plan_driven/cli/ui.rb
80
96
  - lib/plan_driven/configuration.rb
97
+ - lib/plan_driven/connections.rb
81
98
  - lib/plan_driven/credentials.rb
82
99
  - lib/plan_driven/cursor_agents.rb
83
100
  - lib/plan_driven/cursor_llm.mjs
@@ -95,8 +112,10 @@ files:
95
112
  - lib/plan_driven/guards/ticket_guard.rb
96
113
  - lib/plan_driven/guards/ticket_normalizer.rb
97
114
  - lib/plan_driven/http.rb
115
+ - lib/plan_driven/interview.rb
98
116
  - lib/plan_driven/json_reply.rb
99
117
  - lib/plan_driven/llm.rb
118
+ - lib/plan_driven/local_agents.rb
100
119
  - lib/plan_driven/models.rb
101
120
  - lib/plan_driven/models/approval.rb
102
121
  - lib/plan_driven/models/event.rb
@@ -114,7 +133,10 @@ files:
114
133
  - lib/plan_driven/schema_context.rb
115
134
  - lib/plan_driven/template.rb
116
135
  - lib/plan_driven/ticket_generator.rb
136
+ - lib/plan_driven/usage.rb
117
137
  - lib/plan_driven/version.rb
138
+ - lib/plan_driven/wizard.rb
139
+ - lib/plan_driven/wizard/engine.rb
118
140
  - lib/plan_driven/workflow.rb
119
141
  homepage: https://github.com/blaz1988/plan-driven
120
142
  licenses: