brainiac-featureparity 0.1.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.
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brainiac
4
+ module Plugins
5
+ module Featureparity
6
+ # Prompt templates for FeatureParity agent dispatches.
7
+ module Prompts
8
+ # Channel-specific prompt for FeatureParity interactions.
9
+ # Injected into the agent prompt when dispatching for FP events.
10
+ CHANNEL = <<~PROMPT
11
+ ## FeatureParity Channel Rules
12
+
13
+ You're responding to a FeatureParity webhook event. FeatureParity tracks requirements
14
+ and their test coverage across surfaces.
15
+
16
+ ### FP Environment
17
+
18
+ This work is coming from a specific FP environment:
19
+ - **API URL:** {{FP_API_URL}}
20
+ - **Environment:** {{FP_ENVIRONMENT}}
21
+ - **Ephemeral:** {{FP_EPHEMERAL}}
22
+
23
+ **CRITICAL:** You MUST use `--api-url {{FP_API_URL}}` with ALL `fp` CLI commands.
24
+ This ensures you're reading from and writing to the correct FP instance (prod vs dev vs ephemeral).
25
+
26
+ ```bash
27
+ # CORRECT - always include --api-url
28
+ fp show <slug> --project <project> --api-url {{FP_API_URL}}
29
+ fp list --project <project> --api-url {{FP_API_URL}}
30
+ fp propose-change <slug> --project <project> --api-url {{FP_API_URL}} ...
31
+
32
+ # WRONG - will hit production FP even if you're working on dev
33
+ fp show <slug> --project <project>
34
+ ```
35
+
36
+ ### Requirement Work Workflow
37
+
38
+ When implementing a requirement from FP:
39
+ 1. Read the requirement details to understand the spec
40
+ 2. Implement the feature/fix in your worktree
41
+ 3. Write tests that satisfy the acceptance criteria
42
+ 4. Bind your tests to the requirement using `fp:<slug>` comments
43
+ 5. Commit and push your changes
44
+ 6. Open a PR
45
+
46
+ ### Requirement Rejection Workflow
47
+
48
+ A human has rejected a proposed change to a requirement. Your job:
49
+ 1. Read the rejection reason to understand what's wrong
50
+ 2. Review the requirement's current state in FP
51
+ 3. Make the necessary code/test changes to address the feedback
52
+ 4. Update the requirement in FP if needed using `fp` CLI commands (with --api-url!)
53
+ 5. Push changes and update the PR
54
+
55
+ ### FP CLI Commands
56
+
57
+ Remember: ALWAYS include `--api-url {{FP_API_URL}}`
58
+
59
+ ```bash
60
+ fp show <slug> --project <project> --api-url {{FP_API_URL}}
61
+ fp list --project <project> --api-url {{FP_API_URL}}
62
+ fp propose-change <slug> --project <project> --api-url {{FP_API_URL}} \\
63
+ --title "..." --why "..." --acceptance "..."
64
+ ```
65
+
66
+ ### Test Binding
67
+
68
+ To link a test to a requirement, add a comment with `fp:<slug>`:
69
+ ```ruby
70
+ # fp:user-login-validation
71
+ test "validates email format" do
72
+ # ...
73
+ end
74
+ ```
75
+
76
+ ### Important Notes
77
+
78
+ - When implementing: write tests that directly exercise the acceptance criteria
79
+ - When reworking: address the specific rejection reason — don't make unrelated changes
80
+ - Keep your response focused: what you implemented/changed, what's next
81
+ - **Never forget `--api-url`** — using the wrong FP instance corrupts data
82
+ PROMPT
83
+
84
+ # The full prompt template for initial work dispatches (FP-as-source).
85
+ WORK_DISPATCH = <<~PROMPT
86
+ ## FeatureParity: Implement Requirement
87
+
88
+ You've been assigned to implement a requirement directly from FeatureParity.
89
+
90
+ ### Requirement Details
91
+ - **Slug:** `{{REQUIREMENT_SLUG}}`
92
+ - **Title:** {{REQUIREMENT_TITLE}}
93
+ - **Project:** {{PROJECT_KEY}}
94
+
95
+ ### FP Instance
96
+ - **API URL:** {{FP_API_URL}}
97
+ - **Environment:** {{FP_ENVIRONMENT}}
98
+
99
+ **CRITICAL:** All `fp` CLI commands MUST include `--api-url {{FP_API_URL}}`
100
+
101
+ ### Description
102
+ {{REQUIREMENT_DESCRIPTION}}
103
+
104
+ ### Why This Matters
105
+ {{REQUIREMENT_WHY}}
106
+
107
+ ### Acceptance Criteria
108
+ {{ACCEPTANCE_CRITERIA}}
109
+
110
+ ### Your Task
111
+
112
+ 1. Read and understand the requirement fully
113
+ 2. Implement the feature in your worktree
114
+ 3. Write tests that exercise the acceptance criteria
115
+ 4. Bind your tests to this requirement using `fp:{{REQUIREMENT_SLUG}}` comments
116
+ 5. Commit your changes with a clear message referencing the requirement
117
+ 6. Push and open a PR
118
+
119
+ Work in the worktree at: {{WORKTREE_PATH}}
120
+
121
+ When you commit, include the requirement slug in the message:
122
+ ```
123
+ Implement {{REQUIREMENT_SLUG}}: {{REQUIREMENT_TITLE}}
124
+ ```
125
+
126
+ To view or update the requirement in FP:
127
+ ```bash
128
+ fp show {{REQUIREMENT_SLUG}} --project {{PROJECT_KEY}} --api-url {{FP_API_URL}}
129
+ ```
130
+ PROMPT
131
+
132
+ # The full prompt template for requirement rejection dispatches.
133
+ REJECTION_DISPATCH = <<~PROMPT
134
+ ## FeatureParity: Requirement Rejection
135
+
136
+ A requirement you proposed has been rejected and needs rework.
137
+
138
+ ### Rejection Details
139
+ - **Requirement:** `{{REQUIREMENT_SLUG}}` — {{REQUIREMENT_TITLE}}
140
+ - **Rejected by:** {{ACTOR}}
141
+ - **Rejection note:** {{REJECTION_NOTE}}
142
+
143
+ ### FP Instance
144
+ - **API URL:** {{FP_API_URL}}
145
+ - **Environment:** {{FP_ENVIRONMENT}}
146
+
147
+ **CRITICAL:** All `fp` CLI commands MUST include `--api-url {{FP_API_URL}}`
148
+
149
+ ### Source Context
150
+ This requirement was proposed from: {{SOURCE_TYPE}} ({{SOURCE_URL}})
151
+ {{SOURCE_CONTEXT}}
152
+
153
+ ### What Changed
154
+ {{CHANGE_KIND}}: {{CHANGE_DETAILS}}
155
+
156
+ ### Your Task
157
+
158
+ 1. Review the rejection note to understand what needs to change
159
+ 2. Make the necessary fixes in your worktree
160
+ 3. Update the requirement in FP if the wording/scope needs adjustment
161
+ 4. Commit and push your changes
162
+ 5. Summarize what you fixed and why
163
+
164
+ Work in the existing worktree at: {{WORKTREE_PATH}}
165
+
166
+ To view or update the requirement in FP:
167
+ ```bash
168
+ fp show {{REQUIREMENT_SLUG}} --api-url {{FP_API_URL}}
169
+ fp propose-change {{REQUIREMENT_SLUG}} --api-url {{FP_API_URL}} ...
170
+ ```
171
+ PROMPT
172
+
173
+ # Writing the tests a newly active requirement is missing (coverage.gap_detected).
174
+ GAP_DISPATCH = <<~PROMPT
175
+ ## FeatureParity: Write the Missing Tests
176
+
177
+ A requirement is now active on surfaces that have no tests yet.
178
+
179
+ ### Requirement
180
+ - **Slug:** `{{REQUIREMENT_SLUG}}`
181
+ - **Title:** {{REQUIREMENT_TITLE}}
182
+ - **Project:** {{PROJECT_KEY}}
183
+ - **Missing on:** {{MISSING_SURFACES}}
184
+
185
+ ### Acceptance Criteria
186
+ {{ACCEPTANCE_CRITERIA}}
187
+
188
+ ### FP Instance
189
+ - **API URL:** {{FP_API_URL}}
190
+ - **Environment:** {{FP_ENVIRONMENT}}
191
+
192
+ **CRITICAL:** All `fp` CLI commands MUST include `--api-url {{FP_API_URL}}`
193
+
194
+ ### Your Task
195
+
196
+ 1. Read the requirement: `fp show {{REQUIREMENT_SLUG}} --project {{PROJECT_KEY}} --api-url {{FP_API_URL}}`
197
+ 2. For each missing surface, write a test that exercises the acceptance criteria and mark it
198
+ `fp:{{REQUIREMENT_SLUG}}` (pin the surface, e.g. `fp:{{REQUIREMENT_SLUG}}@web`, when one test file serves several).
199
+ 3. If the product doesn't do what the acceptance says, don't bend the test to fit: say so, and
200
+ leave a question on the requirement (`fp question {{REQUIREMENT_SLUG}} "..."`) for a human.
201
+ 4. Run the tests, commit, push and open a PR.
202
+
203
+ Work in the worktree at: {{WORKTREE_PATH}}
204
+ PROMPT
205
+
206
+ # Answering a comment that tagged this agent (requirement.agent_mentioned).
207
+ MENTION_DISPATCH = <<~PROMPT
208
+ ## FeatureParity: You Were Tagged
209
+
210
+ {{COMMENT_AUTHOR}} tagged you ({{AGENT_NAME}}) in a comment on a requirement.
211
+
212
+ ### Requirement
213
+ - **Slug:** `{{REQUIREMENT_SLUG}}`
214
+ - **Title:** {{REQUIREMENT_TITLE}}
215
+ - **Project:** {{PROJECT_KEY}}
216
+
217
+ ### The Comment
218
+ {{COMMENT_BODY}}
219
+
220
+ ### FP Instance
221
+ - **API URL:** {{FP_API_URL}}
222
+ - **Environment:** {{FP_ENVIRONMENT}}
223
+
224
+ **CRITICAL:** All `fp` CLI commands MUST include `--api-url {{FP_API_URL}}`
225
+
226
+ ### Your Task
227
+
228
+ 1. Read the requirement and its discussion:
229
+ `fp show {{REQUIREMENT_SLUG}} --project {{PROJECT_KEY}} --api-url {{FP_API_URL}}`
230
+ `fp comments {{REQUIREMENT_SLUG}} --project {{PROJECT_KEY}} --api-url {{FP_API_URL}}`
231
+ 2. Do what was asked. If it is to re-check suspect tests, read the tests marked
232
+ `fp:{{REQUIREMENT_SLUG}}` against the current acceptance, fix them if they no longer prove it,
233
+ and re-report the evidence.
234
+ 3. Reply on the requirement so they know what you did:
235
+ `fp comments {{REQUIREMENT_SLUG}} --project {{PROJECT_KEY}} --api-url {{FP_API_URL}} --body "..."`
236
+
237
+ Work in: {{WORKTREE_PATH}}
238
+ PROMPT
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Brainiac
7
+ module Plugins
8
+ module Featureparity
9
+ # Verifies a FeatureParity delivery (Standard Webhooks, standardwebhooks.com).
10
+ #
11
+ # FeatureParity signs every delivery with the secret of whatever it was sent to: a
12
+ # project webhook, or an agent key's wake-up address. Each is a "whsec_<base64>"
13
+ # string shown once in FeatureParity. The signature is
14
+ # base64(HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>")) where the
15
+ # key is the base64-decoded part after "whsec_", sent as "v1,<signature>".
16
+ module Signature
17
+ # Reject deliveries stamped more than this far from now, so a captured request
18
+ # can't be replayed later.
19
+ TOLERANCE = 300
20
+
21
+ module_function
22
+
23
+ # True when `signature` (the webhook-signature header, which may hold several
24
+ # space-separated "v1,..." values) matches the body under any of `secrets`.
25
+ def valid?(secrets:, id:, timestamp:, signature:, body:, now: Time.now.to_i)
26
+ return false if id.to_s.empty? || !fresh?(timestamp, now)
27
+
28
+ offered = offered_signatures(signature)
29
+ Array(secrets).any? do |secret|
30
+ expected = sign(secret, id, timestamp, body)
31
+ offered.any? { |sig| secure_compare(sig, expected) }
32
+ end
33
+ end
34
+
35
+ def fresh?(timestamp, now)
36
+ timestamp.to_s.match?(/\A\d+\z/) && (now - timestamp.to_i).abs <= TOLERANCE
37
+ end
38
+
39
+ # The v1 signatures in a webhook-signature header ("v1,<sig> v1,<sig>").
40
+ def offered_signatures(header)
41
+ header.to_s.split.filter_map { |part| part.split(",", 2).last if part.start_with?("v1,") }
42
+ end
43
+
44
+ def sign(secret, id, timestamp, body)
45
+ key = Base64.decode64(secret.to_s.delete_prefix("whsec_"))
46
+ Base64.strict_encode64(OpenSSL::HMAC.digest("SHA256", key, "#{id}.#{timestamp}.#{body}"))
47
+ end
48
+
49
+ def secure_compare(given, expected)
50
+ return false unless given.bytesize == expected.bytesize
51
+
52
+ OpenSSL.fixed_length_secure_compare(given, expected)
53
+ end
54
+ end
55
+
56
+ # Remembers what was just handled, so a retried delivery (same webhook-id) or the same
57
+ # event arriving twice (a project webhook and an agent's wake-up address both
58
+ # subscribed) dispatches one agent, not two. In memory: a restart forgets, which at
59
+ # worst means one repeat.
60
+ module Dedupe
61
+ TTL = 3600
62
+ MAX = 2000
63
+ @seen = {}
64
+ @lock = Mutex.new
65
+
66
+ module_function
67
+
68
+ # Records `key` and returns true if it was already seen within TTL.
69
+ def seen?(key, now: Time.now.to_f)
70
+ return false if key.to_s.empty?
71
+
72
+ @lock.synchronize do
73
+ @seen.delete_if { |_, at| now - at > TTL } if @seen.size >= MAX
74
+ if (at = @seen[key]) && now - at <= TTL
75
+ true
76
+ else
77
+ @seen[key] = now
78
+ false
79
+ end
80
+ end
81
+ end
82
+
83
+ def reset!
84
+ @lock.synchronize { @seen.clear }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brainiac
4
+ module Plugins
5
+ module Featureparity
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "featureparity/version"
4
+ require_relative "featureparity/metadata"
5
+ require_relative "featureparity/config"
6
+ require_relative "featureparity/prompts"
7
+ require_relative "featureparity/signature"
8
+ require_relative "featureparity/handlers"
9
+ require_relative "featureparity/cli"
10
+
11
+ module Brainiac
12
+ module Plugins
13
+ module Featureparity
14
+ # Supported webhook events from FeatureParity
15
+ SUPPORTED_EVENTS = %w[
16
+ requirement.change_rejected
17
+ requirement.change_approved
18
+ requirement.change_proposed
19
+ requirement.removal_proposed
20
+ requirement.activated
21
+ requirement.archived
22
+ requirement.updated
23
+ requirement.work_started
24
+ requirement.agent_mentioned
25
+ evidence.passing
26
+ evidence.failing
27
+ evidence.suspect
28
+ coverage.gap_detected
29
+ ].freeze
30
+
31
+ class << self
32
+ # Called by Brainiac plugin system during server startup.
33
+ #
34
+ # @param app [Sinatra::Application] The running Brainiac server
35
+ def register(app)
36
+ Config.load!
37
+
38
+ # Register channel prompt
39
+ Brainiac.register_channel_prompt(:featureparity, Prompts::CHANNEL)
40
+
41
+ setup_routes(app)
42
+
43
+ LOG.info "[FeatureParity] Plugin registered (webhook: /featureparity)"
44
+ LOG.info "[FeatureParity] Accepting webhooks from environments: #{Config.accept_environments.join(', ')}"
45
+ end
46
+
47
+ private
48
+
49
+ def setup_routes(app)
50
+ setup_webhook_route(app)
51
+ setup_api_routes(app)
52
+ end
53
+
54
+ def setup_webhook_route(app)
55
+ app.post "/featureparity" do
56
+ content_type :json
57
+ request.body.rewind
58
+ # self here is the Sinatra request, so reach the plugin by name.
59
+ status_code, body = Brainiac::Plugins::Featureparity.receive(request.body.read, request.env)
60
+ halt status_code, body
61
+ end
62
+ end
63
+
64
+ def setup_api_routes(app)
65
+ app.get "/api/featureparity" do
66
+ content_type :json
67
+ {
68
+ enabled: true,
69
+ api_base_url: Brainiac::Plugins::Featureparity::Config.api_base_url,
70
+ environment: Brainiac::Plugins::Featureparity::Config.environment,
71
+ accept_environments: Brainiac::Plugins::Featureparity::Config.accept_environments,
72
+ supported_events: SUPPORTED_EVENTS
73
+ }.to_json
74
+ end
75
+ end
76
+
77
+ public
78
+
79
+ # Handle one delivery: verify it, drop a repeat, check where it came from, and hand
80
+ # it to its event's handler. Returns [status, json body].
81
+ def receive(raw_body, env)
82
+ LOG.debug "[FeatureParity] Webhook received: #{raw_body.length} bytes" if LOG.debug?
83
+ refusal = verification_refusal(raw_body, env)
84
+ return refusal if refusal
85
+
86
+ payload = JSON.parse(raw_body)
87
+ screened = screen(payload)
88
+ return screened if screened
89
+
90
+ reload_projects!
91
+ reload_agent_registry!
92
+ dispatch_event(payload["event"], payload)
93
+ rescue JSON::ParserError => e
94
+ LOG.error "[FeatureParity] Invalid JSON: #{e.message}"
95
+ [400, { error: "Invalid JSON" }.to_json]
96
+ rescue StandardError => e
97
+ LOG.error "[FeatureParity] Unhandled error: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}"
98
+ [500, { error: e.message }.to_json]
99
+ end
100
+
101
+ # Signed deliveries (Standard Webhooks): with secrets configured, anything that doesn't
102
+ # verify is refused before it is parsed. FeatureParity retries a delivery under the
103
+ # same webhook-id, so a repeat is acknowledged and dropped.
104
+ def verification_refusal(raw_body, env)
105
+ delivery_id = env["HTTP_WEBHOOK_ID"]
106
+ secrets = Config.signing_secrets
107
+ if secrets.empty?
108
+ LOG.warn "[FeatureParity] Accepting an unverified delivery: set signing_secrets in featureparity.json"
109
+ elsif !Signature.valid?(secrets: secrets, id: delivery_id, timestamp: env["HTTP_WEBHOOK_TIMESTAMP"],
110
+ signature: env["HTTP_WEBHOOK_SIGNATURE"], body: raw_body)
111
+ LOG.warn "[FeatureParity] Refused a delivery whose signature doesn't verify (id: #{delivery_id || 'none'})"
112
+ return [401, { error: "invalid signature" }.to_json]
113
+ end
114
+ return [200, { status: "duplicate" }.to_json] if delivery_id && Dedupe.seen?("delivery:#{delivery_id}")
115
+
116
+ nil
117
+ end
118
+
119
+ # An event from an environment this instance doesn't serve, or one it doesn't handle.
120
+ def screen(payload)
121
+ event = payload["event"]
122
+ webhook_env = (payload["metadata"] || {})["environment"]
123
+ LOG.info "[FeatureParity] Received event: #{event} from environment: #{webhook_env || 'unspecified'}"
124
+
125
+ unless Config.should_accept_environment?(webhook_env)
126
+ LOG.info "[FeatureParity] Ignoring webhook from environment '#{webhook_env}' " \
127
+ "(accepting: #{Config.accept_environments.join(', ')})"
128
+ return [200, { status: "ignored", reason: "environment mismatch", webhook_environment: webhook_env,
129
+ accepted_environments: Config.accept_environments }.to_json]
130
+ end
131
+ return nil if SUPPORTED_EVENTS.include?(event)
132
+
133
+ LOG.info "[FeatureParity] Ignoring unsupported event: #{event}"
134
+ [200, { status: "ignored", reason: "unsupported event" }.to_json]
135
+ end
136
+
137
+ # The events that start an agent; every other supported event is acknowledged.
138
+ HANDLERS = {
139
+ "requirement.change_rejected" => :handle_change_rejected,
140
+ "requirement.work_started" => :handle_work_started,
141
+ "requirement.agent_mentioned" => :handle_agent_mentioned,
142
+ "coverage.gap_detected" => :handle_gap_detected
143
+ }.freeze
144
+
145
+ # Route an accepted event to its handler. Returns [status, json body].
146
+ def dispatch_event(event, payload)
147
+ handler = HANDLERS[event]
148
+ return Handlers.public_send(handler, payload) if handler
149
+
150
+ LOG.info "[FeatureParity] #{event}: #{payload.dig('data', 'slug')}"
151
+ [200, { status: "acknowledged", event: event }.to_json]
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Brainiac Featureparity Plugin — entry point loaded by RubyGems.
4
+ require_relative "brainiac/plugins/featureparity"
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brainiac-featureparity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Davis
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: brainiac
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.8
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.8
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.25'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.25'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.75'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.75'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop-performance
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.25'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.25'
82
+ description: FeatureParity integration for Brainiac — starts the agent picked for
83
+ a requirement, answers comments that tag it, writes missing tests, and reworks rejected
84
+ proposals. Verifies FeatureParity's signed deliveries.
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - README.md
90
+ - lib/brainiac/plugins/featureparity.rb
91
+ - lib/brainiac/plugins/featureparity/cli.rb
92
+ - lib/brainiac/plugins/featureparity/config.rb
93
+ - lib/brainiac/plugins/featureparity/handlers.rb
94
+ - lib/brainiac/plugins/featureparity/metadata.rb
95
+ - lib/brainiac/plugins/featureparity/prompts.rb
96
+ - lib/brainiac/plugins/featureparity/signature.rb
97
+ - lib/brainiac/plugins/featureparity/version.rb
98
+ - lib/brainiac_featureparity.rb
99
+ homepage: https://github.com/stowzilla/brainiac-featureparity
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ rubygems_mfa_required: 'true'
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '3.4'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.6.9
119
+ specification_version: 4
120
+ summary: FeatureParity plugin for Brainiac
121
+ test_files: []