plan_driven 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -1
- data/README.md +437 -37
- data/app/controllers/plan_driven/wizard/application_controller.rb +59 -0
- data/app/controllers/plan_driven/wizard/configuration_controller.rb +62 -0
- data/app/controllers/plan_driven/wizard/jobs_controller.rb +16 -0
- data/app/controllers/plan_driven/wizard/plans_controller.rb +113 -0
- data/app/views/layouts/plan_driven/wizard/application.html.erb +151 -0
- data/app/views/plan_driven/wizard/configuration/show.html.erb +104 -0
- data/app/views/plan_driven/wizard/plans/_agents.html.erb +44 -0
- data/app/views/plan_driven/wizard/plans/_approve.html.erb +40 -0
- data/app/views/plan_driven/wizard/plans/_finish.html.erb +31 -0
- data/app/views/plan_driven/wizard/plans/_plan.html.erb +52 -0
- data/app/views/plan_driven/wizard/plans/_tickets.html.erb +42 -0
- data/app/views/plan_driven/wizard/plans/index.html.erb +31 -0
- data/app/views/plan_driven/wizard/plans/new.html.erb +21 -0
- data/app/views/plan_driven/wizard/plans/show.html.erb +37 -0
- data/app/views/plan_driven/wizard/plans/statistics.html.erb +94 -0
- data/config/routes.rb +17 -0
- data/exe/plan-driven +1 -0
- data/lib/generators/plan_driven/install_generator.rb +7 -0
- data/lib/generators/plan_driven/templates/plan_driven.rb +10 -1
- data/lib/plan_driven/charts.rb +252 -0
- data/lib/plan_driven/cli/config_commands.rb +60 -0
- data/lib/plan_driven/cli/plan_commands.rb +6 -2
- data/lib/plan_driven/cli/setup_commands.rb +10 -0
- data/lib/plan_driven/cli/ticket_commands.rb +64 -3
- data/lib/plan_driven/cli/ui.rb +41 -2
- data/lib/plan_driven/cli.rb +21 -4
- data/lib/plan_driven/configuration.rb +31 -3
- data/lib/plan_driven/connections.rb +69 -0
- data/lib/plan_driven/cursor_agents.rb +10 -2
- data/lib/plan_driven/delivery.rb +23 -5
- data/lib/plan_driven/evidence.rb +11 -4
- data/lib/plan_driven/github.rb +4 -0
- data/lib/plan_driven/guards/migration_guard.rb +86 -9
- data/lib/plan_driven/guards/ticket_guard.rb +1 -2
- data/lib/plan_driven/interview.rb +161 -0
- data/lib/plan_driven/local_agents.rb +254 -0
- data/lib/plan_driven/renderer/html.rb +34 -5
- data/lib/plan_driven/renderer/markdown.rb +67 -3
- data/lib/plan_driven/renderer/style.css +21 -0
- data/lib/plan_driven/renderer.rb +11 -4
- data/lib/plan_driven/statistics.rb +195 -0
- data/lib/plan_driven/template.rb +7 -2
- data/lib/plan_driven/usage.rb +114 -0
- data/lib/plan_driven/version.rb +1 -1
- data/lib/plan_driven/wizard/engine.rb +19 -0
- data/lib/plan_driven/wizard.rb +245 -0
- data/lib/plan_driven.rb +8 -1
- metadata +32 -6
|
@@ -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 stats 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,15 @@ 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
|
-
require_relative "plan_driven/renderer"
|
|
31
34
|
require_relative "plan_driven/evidence"
|
|
35
|
+
require_relative "plan_driven/statistics"
|
|
36
|
+
require_relative "plan_driven/charts"
|
|
37
|
+
require_relative "plan_driven/renderer"
|
|
32
38
|
require_relative "plan_driven/delivery"
|
|
39
|
+
require_relative "plan_driven/wizard"
|
|
33
40
|
require_relative "plan_driven/railtie" if defined?(Rails::Railtie)
|
|
34
41
|
|
|
35
42
|
module PlanDriven
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plan_driven
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Blažević
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -50,9 +50,10 @@ dependencies:
|
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
51
|
version: '9'
|
|
52
52
|
description: |
|
|
53
|
-
plan_driven runs a Rails team's delivery process
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
plan_driven runs a Rails team's delivery process inside your Rails app, from a browser wizard
|
|
54
|
+
or the command line: every button in the wizard runs the same plan-driven command. A short
|
|
55
|
+
interview turns an idea into an implementation plan grounded in your real schema; the plan
|
|
56
|
+
is checked by guards written in Ruby, rendered to PDF and approved. Approved plans become tickets, each
|
|
56
57
|
ticket is handed to a Cursor cloud agent that opens a pull request, and only pull requests
|
|
57
58
|
you approve are merged. Acceptance criteria map to Cucumber scenarios, and every phase leaves
|
|
58
59
|
documentation behind: the plan, the tickets, the pull requests and a delivery report.
|
|
@@ -66,18 +67,37 @@ files:
|
|
|
66
67
|
- CHANGELOG.md
|
|
67
68
|
- LICENSE.txt
|
|
68
69
|
- README.md
|
|
70
|
+
- app/controllers/plan_driven/wizard/application_controller.rb
|
|
71
|
+
- app/controllers/plan_driven/wizard/configuration_controller.rb
|
|
72
|
+
- app/controllers/plan_driven/wizard/jobs_controller.rb
|
|
73
|
+
- app/controllers/plan_driven/wizard/plans_controller.rb
|
|
74
|
+
- app/views/layouts/plan_driven/wizard/application.html.erb
|
|
75
|
+
- app/views/plan_driven/wizard/configuration/show.html.erb
|
|
76
|
+
- app/views/plan_driven/wizard/plans/_agents.html.erb
|
|
77
|
+
- app/views/plan_driven/wizard/plans/_approve.html.erb
|
|
78
|
+
- app/views/plan_driven/wizard/plans/_finish.html.erb
|
|
79
|
+
- app/views/plan_driven/wizard/plans/_plan.html.erb
|
|
80
|
+
- app/views/plan_driven/wizard/plans/_tickets.html.erb
|
|
81
|
+
- app/views/plan_driven/wizard/plans/index.html.erb
|
|
82
|
+
- app/views/plan_driven/wizard/plans/new.html.erb
|
|
83
|
+
- app/views/plan_driven/wizard/plans/show.html.erb
|
|
84
|
+
- app/views/plan_driven/wizard/plans/statistics.html.erb
|
|
85
|
+
- config/routes.rb
|
|
69
86
|
- exe/plan-driven
|
|
70
87
|
- lib/generators/plan_driven/install_generator.rb
|
|
71
88
|
- lib/generators/plan_driven/templates/create_plan_driven_tables.rb.tt
|
|
72
89
|
- lib/generators/plan_driven/templates/plan_driven.rb
|
|
73
90
|
- lib/plan_driven.rb
|
|
74
91
|
- lib/plan_driven/agent_prompt.rb
|
|
92
|
+
- lib/plan_driven/charts.rb
|
|
75
93
|
- lib/plan_driven/cli.rb
|
|
94
|
+
- lib/plan_driven/cli/config_commands.rb
|
|
76
95
|
- lib/plan_driven/cli/plan_commands.rb
|
|
77
96
|
- lib/plan_driven/cli/setup_commands.rb
|
|
78
97
|
- lib/plan_driven/cli/ticket_commands.rb
|
|
79
98
|
- lib/plan_driven/cli/ui.rb
|
|
80
99
|
- lib/plan_driven/configuration.rb
|
|
100
|
+
- lib/plan_driven/connections.rb
|
|
81
101
|
- lib/plan_driven/credentials.rb
|
|
82
102
|
- lib/plan_driven/cursor_agents.rb
|
|
83
103
|
- lib/plan_driven/cursor_llm.mjs
|
|
@@ -95,8 +115,10 @@ files:
|
|
|
95
115
|
- lib/plan_driven/guards/ticket_guard.rb
|
|
96
116
|
- lib/plan_driven/guards/ticket_normalizer.rb
|
|
97
117
|
- lib/plan_driven/http.rb
|
|
118
|
+
- lib/plan_driven/interview.rb
|
|
98
119
|
- lib/plan_driven/json_reply.rb
|
|
99
120
|
- lib/plan_driven/llm.rb
|
|
121
|
+
- lib/plan_driven/local_agents.rb
|
|
100
122
|
- lib/plan_driven/models.rb
|
|
101
123
|
- lib/plan_driven/models/approval.rb
|
|
102
124
|
- lib/plan_driven/models/event.rb
|
|
@@ -112,9 +134,13 @@ files:
|
|
|
112
134
|
- lib/plan_driven/renderer/style.css
|
|
113
135
|
- lib/plan_driven/repository.rb
|
|
114
136
|
- lib/plan_driven/schema_context.rb
|
|
137
|
+
- lib/plan_driven/statistics.rb
|
|
115
138
|
- lib/plan_driven/template.rb
|
|
116
139
|
- lib/plan_driven/ticket_generator.rb
|
|
140
|
+
- lib/plan_driven/usage.rb
|
|
117
141
|
- lib/plan_driven/version.rb
|
|
142
|
+
- lib/plan_driven/wizard.rb
|
|
143
|
+
- lib/plan_driven/wizard/engine.rb
|
|
118
144
|
- lib/plan_driven/workflow.rb
|
|
119
145
|
homepage: https://github.com/blaz1988/plan-driven
|
|
120
146
|
licenses:
|
|
@@ -142,5 +168,5 @@ requirements: []
|
|
|
142
168
|
rubygems_version: 3.6.4
|
|
143
169
|
specification_version: 4
|
|
144
170
|
summary: From implementation plan to merged, tested pull requests, driven from the
|
|
145
|
-
terminal.
|
|
171
|
+
browser or the terminal.
|
|
146
172
|
test_files: []
|