featureparity 0.0.14 → 0.0.16

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.
@@ -213,6 +213,9 @@ module Fp
213
213
  gate:
214
214
  name: Parity gate
215
215
  runs-on: ubuntu-latest
216
+ permissions:
217
+ contents: read
218
+ pull-requests: write
216
219
  steps:
217
220
  - uses: actions/checkout@v4
218
221
 
@@ -223,6 +226,21 @@ module Fp
223
226
  - name: Install fp
224
227
  run: gem install featureparity
225
228
 
229
+ - name: Update PR requirement changes
230
+ if: github.event_name == 'pull_request'
231
+ env:
232
+ FP_API_KEY: ${{ secrets.FP_API_KEY }}
233
+ GITHUB_TOKEN: ${{ github.token }}
234
+ run: |
235
+ if fp help | grep -q -- '--github-pr'; then
236
+ fp changes --github-pr --github-summary
237
+ else
238
+ echo '## FeatureParity requirement changes' >> "$GITHUB_STEP_SUMMARY"
239
+ echo '' >> "$GITHUB_STEP_SUMMARY"
240
+ echo 'The installed fp version cannot update the pull request description yet.' >> "$GITHUB_STEP_SUMMARY"
241
+ exit 1
242
+ fi
243
+
226
244
  # Exits non-zero when the project violates its enforcement level.
227
245
  # Add `--warn-only` while adopting if you need the annotations without
228
246
  # blocking merges yet.
@@ -290,22 +308,61 @@ module Fp
290
308
  If one is wrong or outdated, propose the change — never edit or ignore it.
291
309
 
292
310
  **Start every task by checking the requirements — before you write code, not
293
- after.** When you pick up a feature or bug, first run `fp list` / `fp show` to find
294
- the requirement it belongs to. If one exists, you'll bind your test to it; if none
295
- does, propose one (as a draft) so the work is on the record from the outset. Treating
296
- this as the opening move — not a chore at the end — is what keeps parity honest.
311
+ after.** When you pick up a feature or bug, first inventory the full request and
312
+ compare it with the active and draft requirements. Find what already covers the work,
313
+ what must change, and what is missing. Treating this as the opening move — not a chore
314
+ at the end — is what keeps the catalog trustworthy.
297
315
 
298
- ### 1. Find the requirement — do this first
316
+ ### 1. Find and model the requirements — do this first
299
317
 
300
318
  ```bash
301
319
  fp list # active requirements (project comes from .featureparity.yml)
320
+ fp list --status draft # proposed work — check before creating duplicates
302
321
  fp list --gaps # the ones with no evidence yet — good places to start
303
322
  fp show <slug> # details, acceptance criteria, and existing receipts
304
323
  fp history <slug> # why it says what it says, and whether a change is pending
324
+ fp changes --source-url URL # requirement activity prompted by a work item
305
325
  ```
306
326
 
307
- If no requirement covers what you're building, propose one. Agents always create
308
- **drafts**; a human activates them:
327
+ **The requirement catalog is the human-readable product specification, not a box to
328
+ tick with one umbrella record.** It records the team's current shared understanding;
329
+ it is not a finished decree handed from product to engineering. Refine requirements
330
+ through ongoing conversation with customers, domain experts, and builders. The human
331
+ product owner decides value and priority; agents should surface gaps, inconsistencies,
332
+ alternatives, and useful splits rather than silently making product decisions.
333
+
334
+ Before coding, enumerate the independently meaningful promises in the request:
335
+ user-visible behaviours, policies, limits, plans, deadlines, state transitions,
336
+ permissions, failure modes, and affected surfaces. Then model them:
337
+
338
+ - Give each independently testable or independently failable product promise its own
339
+ focused requirement. If one acceptance sentence joins behaviours that could pass or
340
+ fail separately, split it.
341
+ - Use a parent for the broad capability and children for distinct behaviours, policies,
342
+ or surface-specific promises. Do not create requirements for implementation details
343
+ or refactors that make no product promise.
344
+ - Put concrete product decisions — prices, quotas, dates, roles, fallbacks — in the
345
+ requirement acceptance criteria. Never leave them only as assumptions in code or a PR.
346
+ - Check adjacent requirements and their history. New requirements do not excuse an old
347
+ requirement that the work makes inaccurate; propose a change to keep the whole catalog true.
348
+ - Before finishing, reconcile the implementation against the catalog: every shipped
349
+ behaviour maps to a current requirement, every changed promise is recorded, and every
350
+ focused requirement has appropriate marked evidence or an explicit gap. A broad marker
351
+ must never appear to prove several acceptance clauses that its test does not exercise.
352
+
353
+ **Prevent contradictions before they enter the catalog.** Before proposing a new or changed
354
+ promise, compare its intended acceptance criteria with related active and draft requirements
355
+ and read their history. Ask whether both promises can be true at the same time for the same
356
+ user, state, and conditions. If not, do not add another requirement.
357
+
358
+ A failing test bound to an existing requirement, or an implementation that only works by
359
+ changing one, is a contradiction signal — not permission to weaken the test or write a
360
+ second requirement around it. Identify which promise matches product intent, then use
361
+ `fp propose-change` (or `--removal`) with the source and reason to put that decision before
362
+ a human. If the intent is unclear, stop and ask rather than making the catalog say both things.
363
+
364
+ If no requirement covers what you're building, propose one or a parent/child tree. Agents
365
+ always create **drafts**; a human activates them:
309
366
 
310
367
  ```bash
311
368
  fp propose --slug <slug> --name "..." --required <surface>,<surface> \\
@@ -313,18 +370,45 @@ module Fp
313
370
  --source-type fizzy --source-url <card-or-issue-url>
314
371
  ```
315
372
 
316
- **Writing the `--why`: brief, and it actually explains why.** Someone approving
317
- this requirement needs to grasp in one read why the product needs it — the user
318
- problem it solves or the outcome it protects. State that and stop. Longer is fine
319
- when it earns its length; wordy is not. Avoid two things: filler that says nothing
320
- specific, and describing *how* the feature works instead of *why* anyone wants it.
321
-
322
- - ✅ `--why "Warehouse staff scan the label to look up a container; without a QR code they key in the ID by hand and mis-type it."`
323
- - ❌ `--why "This is an important requirement that ensures a high-quality experience for our users across all surfaces."` (filler — says nothing specific)
324
- - ❌ `--why "The label renders a QR image encoding the container ID at print time."` (how it works, not why anyone needs it)
325
-
326
- Same rule for `--acceptance`: state the observable condition that proves it's done
327
- (`"QR scans to the container's detail page"`), not a restatement of the title.
373
+ **Write a requirement as a product decision someone can approve, not as a design
374
+ task.** It should read like a capable teammate wrote it: direct, specific, and free
375
+ of boilerplate such as “ensure a seamless experience.”
376
+
377
+ - **Title:** one clear product promise in present tense. Name the user-visible
378
+ capability, not the work (`“Dispatchers see current shipment status”`, not
379
+ `“Implement status polling”`).
380
+ - **Why:** one or two natural sentences naming who needs this, the problem or risk,
381
+ and the outcome worth protecting. Do not repeat the title, sell the feature, or
382
+ explain the mechanism.
383
+ - **Acceptance:** the observable behaviour that would let a reviewer decide whether
384
+ the promise is true. State the trigger or situation, the result, and material
385
+ boundaries such as permissions, failure states, and affected surfaces. Use examples
386
+ and marked tests to ground and double-check the rule. Do not mistake a few examples for the rule
387
+ or let tests stand for every case; neither replaces the general rule and continued
388
+ conversation.
389
+ - **Numbers and policy:** include a price, limit, timeout, date, role, or fallback only
390
+ when the source made it a real product decision. Never invent a plausible value to
391
+ fill a gap. If the decision matters and the source is silent, ask.
392
+ - **Implementation detail:** leave out components, data models, algorithms, endpoints,
393
+ and UI controls unless that interface or constraint is itself part of the promise.
394
+
395
+ Learn from the request, existing requirements and history, and available evidence about
396
+ what users actually do. Look for what they attempt, where they abandon a task, what they
397
+ use, and what experiments reveal. Treat the first proposed solution as input, not
398
+ unquestionable scope. Preserve important uncertainty as a question rather than
399
+ laundering a guess into acceptance criteria.
400
+
401
+ - ✅ **Title:** `Dispatchers see current shipment status`
402
+ **Why:** `Dispatchers need to spot delayed shipments before they miss a delivery window.`
403
+ **Acceptance:** `When a shipment's status changes, the dashboard shows the new status; if status is unavailable, it says so instead of presenting stale data as current.`
404
+ - ❌ **Title:** `Implement Redis-backed 30-second status polling`
405
+ **Why:** `This important feature ensures a seamless, robust experience.`
406
+ **Acceptance:** `Use a background worker and refresh every 30 seconds.`
407
+ This invents policy, prescribes a design, and never states the product outcome.
408
+
409
+ Before proposing, read the draft once as a manager deciding scope: is the value clear,
410
+ can the result be observed, are product decisions distinguished from guesses, and can
411
+ every sentence earn its place? If not, revise or ask before writing the record.
328
412
 
329
413
  ### 2. If the requirement is wrong, propose a change — don't silently edit it
330
414
 
@@ -347,8 +431,9 @@ module Fp
347
431
 
348
432
  - Never edit a requirement's wording by any other route. `fp propose-change` is the
349
433
  only one that records what it was and what prompted the change.
350
- - You cannot change `--slug` (it's the join to every `fp:<slug>` marker) or remove a
351
- required surface. Both come back as 403.
434
+ - You may correct `--name` (the title) or `--slug` through a proposal. Existing evidence
435
+ follows a proposed slug, but every `fp:<slug>` marker still needs the same correction.
436
+ You still cannot remove a required surface.
352
437
  - You cannot approve or reject — not even your own proposal.
353
438
  - One open proposal per requirement (409 otherwise). Run `fp history <slug>` first.
354
439
  - **A requirement awaiting review still counts toward the gate.** Proposing a change
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Fp
4
4
  module Commands
5
- # fp propose --project <slug> --slug X --name "..." --why "..." --required a,b,c --acceptance "..." --parent <slug> --category <cat>
5
+ # fp propose --project <slug> --slug X --name "..." --why "..." --required a,b,c --required-types unit,e2e --acceptance "..." --parent <slug> --category <cat>
6
6
  # Create a new requirement as draft
7
7
  class Propose < Base
8
8
  KNOWN_FLAGS = {
@@ -11,6 +11,7 @@ module Fp
11
11
  name: :string,
12
12
  why: :string,
13
13
  required: :string,
14
+ required_types: :string,
14
15
  acceptance: :string,
15
16
  parent: :string,
16
17
  category: :string,
@@ -27,7 +28,7 @@ module Fp
27
28
  opts, = parse_flags(args, KNOWN_FLAGS)
28
29
 
29
30
  require_project!(opts)
30
- require_flag(opts, :slug, '--slug is required (immutable identifier for the requirement)')
31
+ require_flag(opts, :slug, '--slug is required (initial key for the requirement)')
31
32
  require_flag(opts, :name, '--name is required (human-readable title)')
32
33
 
33
34
  # Validate category client-side for better UX (fail fast with helpful message)
@@ -41,6 +42,9 @@ module Fp
41
42
  exit 1
42
43
  end
43
44
 
45
+ # Parse and validate required test types (comma-separated).
46
+ required_test_types = parse_test_types(opts[:required_types])
47
+
44
48
  project_id = resolve_project_id(opts[:project])
45
49
 
46
50
  # Parse required surfaces (comma-separated)
@@ -56,6 +60,7 @@ module Fp
56
60
  project_id: project_id,
57
61
  required_surfaces: required_surfaces
58
62
  }
63
+ params[:required_test_types] = required_test_types unless required_test_types.empty?
59
64
  params[:why] = opts[:why] if opts[:why]
60
65
  params[:acceptance] = opts[:acceptance] if opts[:acceptance]
61
66
  params[:category] = opts[:category] if opts[:category]
@@ -94,6 +99,9 @@ module Fp
94
99
  puts " Category: #{requirement['category'] || '(none)'}"
95
100
  puts " Tags: #{(requirement['tags'] || []).join(', ')}" unless (requirement['tags'] || []).empty?
96
101
  puts " Surfaces: #{(requirement['required_surfaces'] || []).join(', ')}"
102
+ unless (requirement['required_test_types'] || []).empty?
103
+ puts " Test types: #{(requirement['required_test_types'] || []).join(', ')} (each must have evidence on every surface)"
104
+ end
97
105
  puts " Parent: #{opts[:parent] || '(none)'}" if opts[:parent]
98
106
  puts
99
107
  puts '⚠️ This requirement is a DRAFT.'
@@ -104,6 +112,19 @@ module Fp
104
112
 
105
113
  private
106
114
 
115
+ # Parse a comma-separated --required-types list into a normalized array. Returns []
116
+ # when the flag is absent (meaning "any test counts / inherit project default").
117
+ #
118
+ # No client-side enum check: valid test types are the *project's* vocabulary
119
+ # (Project#test_type_vocabulary), which the CLI doesn't know without a round-trip.
120
+ # The server validates against that list and returns a verbatim 422 the caller
121
+ # sees — the same way an unknown surface is handled.
122
+ def parse_test_types(raw)
123
+ return [] if raw.nil? || raw.to_s.strip.empty?
124
+
125
+ raw.split(',').map { |t| t.strip.downcase }.reject(&:empty?).uniq
126
+ end
127
+
107
128
  # Resolve a parent requirement slug to its ID
108
129
  def resolve_parent_id(project_id, parent_slug)
109
130
  result = client.list_requirements(project_id: project_id)
@@ -12,7 +12,7 @@ module Fp
12
12
  class ProposeChange < Base
13
13
  KNOWN_FLAGS = {
14
14
  project: :string, name: :string, why: :string, acceptance: :string,
15
- required: :string, category: :string, parent: :string, slug: :string,
15
+ required: :string, required_types: :string, category: :string, parent: :string, slug: :string,
16
16
  reason: :string, source_type: :string, source_url: :string, source_context: :string,
17
17
  removal: :boolean
18
18
  }.freeze
@@ -48,8 +48,8 @@ module Fp
48
48
  removal = opts[:removal] ? true : false
49
49
 
50
50
  if !removal && params.keys.none? { |k| proposed_field?(k) }
51
- output.error('Nothing to propose. Pass at least one of --name/--why/--acceptance/' \
52
- '--required/--category/--parent, or --removal to propose deletion.')
51
+ output.error('Nothing to propose. Pass at least one of --name/--slug/--why/--acceptance/' \
52
+ '--required/--required-types/--category/--parent, or --removal to propose deletion.')
53
53
  exit 1
54
54
  end
55
55
 
@@ -70,6 +70,10 @@ module Fp
70
70
  exit 1
71
71
  end
72
72
 
73
+ # No client-side test-type check: the valid set is the project's own vocabulary
74
+ # (Project#test_type_vocabulary), which the CLI can't know without a round-trip.
75
+ # The server validates and returns a verbatim 422.
76
+
73
77
  return unless opts[:source_type] && !VALID_SOURCE_TYPES.include?(opts[:source_type])
74
78
 
75
79
  output.error("Invalid source type '#{opts[:source_type]}'. Must be one of: #{VALID_SOURCE_TYPES.join(', ')}")
@@ -90,6 +94,9 @@ module Fp
90
94
 
91
95
  FIELD_FLAGS.each { |flag, field| params[field] = opts[flag] if opts[flag] }
92
96
  params[:required_surfaces] = opts[:required].split(',').map(&:strip) if opts[:required]
97
+ if opts[:required_types]
98
+ params[:required_test_types] = opts[:required_types].split(',').map { |t| t.strip.downcase }.reject(&:empty?)
99
+ end
93
100
  params[:parent_id] = resolve_parent_id(project_id, opts[:parent]) if opts[:parent]
94
101
 
95
102
  params[:removal] = true if opts[:removal]
@@ -102,7 +109,7 @@ module Fp
102
109
  end
103
110
 
104
111
  def proposed_field?(key)
105
- %i[title why acceptance category slug required_surfaces parent_id].include?(key)
112
+ %i[title why acceptance category slug required_surfaces required_test_types parent_id].include?(key)
106
113
  end
107
114
 
108
115
  def resolve_parent_id(project_id, parent_slug)
@@ -136,11 +143,12 @@ module Fp
136
143
  print_diff(change['changes'])
137
144
  print_source(change)
138
145
 
146
+ current_slug = requirement['slug'].to_s.empty? ? slug : requirement['slug']
139
147
  puts " Status: #{requirement['status']}"
140
148
  puts " Change: #{change['id']}"
141
149
  puts
142
150
  puts '⚠️ This requirement is back in DRAFT pending human review.'
143
- puts " Run `fp history #{slug} --project <project>` to see the full diff."
151
+ puts " Run `fp history #{current_slug} --project <project>` to see the full diff."
144
152
  puts ' A human must approve or reject it in the web app. Agents cannot review changes.'
145
153
  end
146
154
  end
@@ -18,6 +18,7 @@ module Fp
18
18
  KNOWN_FLAGS = {
19
19
  project: :string,
20
20
  surface: :string,
21
+ test_type: :string,
21
22
  file: :string,
22
23
  line: :string,
23
24
  repo: :string,
@@ -57,6 +58,10 @@ module Fp
57
58
  exit 1
58
59
  end
59
60
 
61
+ # No client-side test-type check: the valid set is the project's own vocabulary,
62
+ # which the CLI can't know without a round-trip. The server validates and returns
63
+ # a verbatim 422.
64
+
60
65
  project_id = resolve_project_id(opts[:project])
61
66
 
62
67
  # Verify the requirement exists before reporting evidence
@@ -75,6 +80,7 @@ module Fp
75
80
  repo: opts[:repo],
76
81
  state: state
77
82
  }
83
+ params[:test_type] = opts[:test_type] if opts[:test_type]
78
84
  params[:pr_url] = opts[:pr] if opts[:pr]
79
85
  params[:sha] = opts[:sha] if opts[:sha]
80
86
  params[:work_item_url] = opts[:work_item] if opts[:work_item]
@@ -112,6 +118,7 @@ module Fp
112
118
  output.success(result[:data]) do
113
119
  puts "Evidence reported for '#{slug}' on surface '#{opts[:surface]}'."
114
120
  puts " State: #{state}"
121
+ puts " Type: #{opts[:test_type]}" if opts[:test_type]
115
122
  puts " File: #{opts[:file]}#{opts[:line] ? ":#{opts[:line]}" : ''}"
116
123
  if opts[:pr]
117
124
  puts " PR: #{opts[:pr]}"
@@ -150,6 +157,9 @@ module Fp
150
157
  exit 1
151
158
  end
152
159
 
160
+ # No client-side test-type check — the server validates --test-type / marker
161
+ # types against the project's own vocabulary and returns a verbatim 422.
162
+
153
163
  begin
154
164
  testcases = Fp::JUnit.parse_file(opts[:junit])
155
165
  rescue ArgumentError => e
@@ -163,7 +173,11 @@ module Fp
163
173
  end
164
174
 
165
175
  base_dir = opts[:base_dir] || Dir.pwd
166
- result = Fp::JUnit.bind_markers(testcases, base_dir: base_dir, default_surface: opts[:surface])
176
+ # --test-type is the default kind for markers that pin none (`fp:slug`,
177
+ # `fp:slug@api`); a marker's own `:type` suffix always wins.
178
+ result = Fp::JUnit.bind_markers(testcases, base_dir: base_dir,
179
+ default_surface: opts[:surface],
180
+ default_test_type: opts[:test_type])
167
181
  bindings = result[:bindings]
168
182
  unmatched = result[:unmatched]
169
183
  missing_surface = result[:missing_surface] || []
@@ -207,6 +221,7 @@ module Fp
207
221
  repo: opts[:repo],
208
222
  state: state
209
223
  }
224
+ params[:test_type] = binding.test_type if binding.test_type
210
225
  params[:line] = binding.line if binding.line
211
226
  params[:pr_url] = opts[:pr] if opts[:pr]
212
227
  params[:sha] = opts[:sha] if opts[:sha]
@@ -238,7 +253,7 @@ module Fp
238
253
  def emit_junit_summary(opts, reported, skipped_unknown, unmatched, api_unavailable)
239
254
  data = {
240
255
  repo: opts[:repo],
241
- reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded] } },
256
+ reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, test_type: r[:binding].test_type, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded] } },
242
257
  skipped_unknown: skipped_unknown.map { |b| { slug: b.slug, surface: b.surface, file: b.file } },
243
258
  unmatched_testcases: unmatched.map { |tc| { name: tc.name, file: tc.file } }
244
259
  }
@@ -248,7 +263,8 @@ module Fp
248
263
  reported.each do |r|
249
264
  marker = r[:recorded] ? '✓' : '•'
250
265
  loc = r[:binding].line ? "#{r[:binding].file}:#{r[:binding].line}" : r[:binding].file
251
- puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface} (#{r[:state]}) #{loc}"
266
+ type = r[:binding].test_type ? " [#{r[:binding].test_type}]" : ''
267
+ puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface}#{type} (#{r[:state]}) #{loc}"
252
268
  end
253
269
 
254
270
  unless skipped_unknown.empty?
@@ -24,6 +24,7 @@ module Fp
24
24
  end
25
25
 
26
26
  requirement = result[:data]['requirement']
27
+ source_history = result[:data]['source_history'] || []
27
28
 
28
29
  # Evidence is not embedded in the requirement payload — it lives on the project's
29
30
  # evidence collection, keyed by slug. Fetch it, or `fp show` reports "(none)" for
@@ -37,7 +38,7 @@ module Fp
37
38
  payload['parent_slug'] = parent_slug if parent_slug
38
39
 
39
40
  output.success(payload) do
40
- print_requirement(requirement, evidence: evidence, parent_slug: parent_slug)
41
+ print_requirement(requirement, evidence: evidence, parent_slug: parent_slug, source_history: source_history)
41
42
  end
42
43
  end
43
44
 
@@ -60,7 +61,7 @@ module Fp
60
61
  parent && parent['slug']
61
62
  end
62
63
 
63
- def print_requirement(req, evidence: [], parent_slug: nil)
64
+ def print_requirement(req, evidence: [], parent_slug: nil, source_history: [])
64
65
  puts "#{req['title']}"
65
66
  puts "=" * req['title'].length
66
67
  puts
@@ -81,6 +82,19 @@ module Fp
81
82
  end
82
83
  puts
83
84
 
85
+ # Required test types: the kinds of test that must each cover this requirement on
86
+ # every surface. Empty means "any test counts" (or it inherits the project
87
+ # default) — shown explicitly so an agent knows whether one test is enough.
88
+ test_types = req['required_test_types'] || []
89
+ puts 'Required Test Types:'
90
+ if test_types.empty?
91
+ puts ' (any — a single test of any kind satisfies each surface)'
92
+ else
93
+ test_types.each { |t| puts " - #{t}" }
94
+ puts ' (each kind needs its own evidence on every required surface)'
95
+ end
96
+ puts
97
+
84
98
  if req['why'] && !req['why'].empty?
85
99
  puts 'Why:'
86
100
  puts " #{req['why']}"
@@ -93,6 +107,23 @@ module Fp
93
107
  puts
94
108
  end
95
109
 
110
+ # Source trail: every source (Fizzy card, Discord thread, issue) that prompted a
111
+ # change to this requirement, oldest first. Derived from events so the history is
112
+ # additive — older sources aren't lost when a new one is recorded.
113
+ puts 'Source Trail:'
114
+ if source_history.empty?
115
+ puts ' (no sources recorded)'
116
+ else
117
+ source_history.each do |src|
118
+ line = []
119
+ line << src['type'] if src['type']
120
+ line << src['url'] if src['url']
121
+ puts " - #{line.join(' ')}"
122
+ puts " #{src['context']}" if src['context']
123
+ end
124
+ end
125
+ puts
126
+
96
127
  # Evidence receipts, newest first, grouped per surface.
97
128
  puts 'Evidence:'
98
129
  if evidence.empty?
@@ -107,6 +138,7 @@ module Fp
107
138
  puts " #{surface}:"
108
139
  records.each do |e|
109
140
  puts " Status: #{e['status']}"
141
+ puts " Type: #{e['test_type']}" if e['test_type'] && e['test_type'] != 'unspecified'
110
142
  if e['source_file']
111
143
  loc = e['source_line'] ? "#{e['source_file']}:#{e['source_line']}" : e['source_file']
112
144
  puts " File: #{loc}"
data/lib/fp/commands.rb CHANGED
@@ -8,6 +8,7 @@ require_relative 'commands/show'
8
8
  require_relative 'commands/propose'
9
9
  require_relative 'commands/propose_change'
10
10
  require_relative 'commands/history'
11
+ require_relative 'commands/changes'
11
12
  require_relative 'commands/report'
12
13
  require_relative 'commands/ci_report'
13
14
  require_relative 'commands/check'
@@ -21,6 +22,9 @@ require_relative 'commands/setup'
21
22
  require_relative 'commands/whoami'
22
23
  require_relative 'commands/version'
23
24
  require_relative 'commands/help'
25
+ require_relative 'commands/attach'
26
+ require_relative 'commands/attachments'
27
+ require_relative 'commands/conversation'
24
28
 
25
29
  module Fp
26
30
  module Commands