featureparity 0.0.26 → 0.0.28

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.
data/lib/fp/junit.rb CHANGED
@@ -756,10 +756,16 @@ module Fp
756
756
  # How a test (or group) declaration announces its name, per language. Each entry is
757
757
  # [regex, kind]; the captured group is the name. `:group` entries are describe-style
758
758
  # blocks, whose name prefixes the names of the tests inside them.
759
+ #
760
+ # A quoted name may contain its own quote escaped (`'its kind\'s logo'`), so the name
761
+ # runs to the first *unescaped* closing quote and is unescaped before matching — the
762
+ # runner reports `its kind's logo`. Stopping at the first quote cut the name off at
763
+ # `kind\` and the marker silently bound nothing (Fizzy #1759).
764
+ QUOTED_NAME = %q{((?:\\\\.|(?!\\1).)+?)\\1}
759
765
  TEST_DECL_PATTERNS = [
760
- [/\b(?:it|test|bench)(?:\.\w+)*\s*\(\s*(['"`])(.+?)\1/, :test], # vitest/jest/mocha
761
- [/\bdescribe(?:\.\w+)*\s*\(\s*(['"`])(.+?)\1/, :group], # vitest/jest/mocha group
762
- [/\b(?:it|specify|scenario)\s+(['"])(.+?)\1/, :test], # rspec
766
+ [/\b(?:it|test|bench)(?:\.\w+)*\s*\(\s*(['"`])#{QUOTED_NAME}/, :test], # vitest/jest/mocha
767
+ [/\bdescribe(?:\.\w+)*\s*\(\s*(['"`])#{QUOTED_NAME}/, :group], # vitest/jest/mocha group
768
+ [/\b(?:it|specify|scenario)\s+(['"])#{QUOTED_NAME}/, :test], # rspec
763
769
  [/\b(?:def|fun)\s+`(.+?)`/, :test], # kotlin backticked
764
770
  [/\b(?:def|fun|func)\s+(test_?\w+)/i, :test] # minitest / xctest / pytest
765
771
  ].freeze
@@ -779,7 +785,7 @@ module Fp
779
785
  next unless match
780
786
 
781
787
  # Two-quote patterns capture the quote first, the name second.
782
- return [match.captures.last, kind]
788
+ return [unescape_name(match.captures.last), kind]
783
789
  end
784
790
 
785
791
  # A line of real code that declares nothing recognisable: the marker doesn't
@@ -794,5 +800,9 @@ module Fp
794
800
  # the first line of real code regardless, so this only bounds how long a comment block
795
801
  # between the marker and its test may be.
796
802
  DECL_SEARCH_WINDOW = 40
803
+
804
+ def unescape_name(name)
805
+ name.gsub(/\\(.)/, '\\1')
806
+ end
797
807
  end
798
808
  end
@@ -0,0 +1,5 @@
1
+ # Hexhaven: agent notes
2
+
3
+ Practice repo for the FeatureParity tutorial: a made-up board game's game server.
4
+ Tests: `ruby -Ilib test/rules_test.rb` (prefix `JUNIT_OUT=junit.xml` for a report).
5
+ Everything here is the `server` surface, so pin markers with `@server`.
@@ -0,0 +1,29 @@
1
+ # Hexhaven: your FeatureParity practice table
2
+
3
+ Hexhaven's game server: the referee that checks every move. Rules in `lib/`, tests in
4
+ `test/`. Plain Ruby; nothing to install.
5
+
6
+ 1. Run the tests:
7
+
8
+ ```sh
9
+ ruby -Ilib test/rules_test.rb
10
+ ```
11
+
12
+ 2. Put this above the last test in `test/rules_test.rb`, or ask your agent to
13
+ (`AGENTS.md` tells it how):
14
+
15
+ ```ruby
16
+ # fp:tallest_tower_scores_two_points@server
17
+ ```
18
+
19
+ 3. Report it:
20
+
21
+ ```sh
22
+ JUNIT_OUT=junit.xml ruby -Ilib test/rules_test.rb
23
+ fp report --junit junit.xml --repo hexhaven/server
24
+ ```
25
+
26
+ The tallest-tower cell on Server turns from **missing** to **present**. Back to the
27
+ tutorial.
28
+
29
+ Made a mess? `fp tutorial --restart`
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hexhaven — a made-up board game, and the rules its game server referees.
4
+ #
5
+ # Every rule here is a requirement in your FeatureParity tutorial project. The server checks
6
+ # every move; Web, iOS and Android each show the same rules on their own, and FeatureParity
7
+ # proves all four agree.
8
+ module Hexhaven
9
+ DIE = (1..6).freeze
10
+ HARBOR_RATE = 3
11
+ TALLEST_TOWER_MINIMUM = 5
12
+ TALLEST_TOWER_POINTS = 2
13
+
14
+ # A turn starts with two six-sided dice.
15
+ def self.roll(rng = Random.new)
16
+ [rng.rand(DIE), rng.rand(DIE)]
17
+ end
18
+
19
+ # At a harbor, three of one good buy one of any other.
20
+ def self.harbor_trade(hand, give:, get:)
21
+ raise ArgumentError, "a harbor trade needs #{HARBOR_RATE} #{give}" if hand.fetch(give, 0) < HARBOR_RATE
22
+
23
+ hand.merge(give => hand[give] - HARBOR_RATE, get => hand.fetch(get, 0) + 1)
24
+ end
25
+
26
+ # Who holds the tallest-tower bonus, given each player's tallest tower in floors. The
27
+ # first tower of five floors or more takes it; only a strictly taller one steals it.
28
+ def self.tallest_tower(towers, holder: nil)
29
+ contenders = towers.select { |_, floors| floors >= TALLEST_TOWER_MINIMUM }
30
+ return nil if contenders.empty?
31
+
32
+ best = contenders.values.max
33
+ holder = nil unless holder && towers.fetch(holder, 0) == best
34
+ holder || contenders.key(best)
35
+ end
36
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A dependency-free JUnit XML reporter for Minitest, written only when JUNIT_OUT is set.
4
+ #
5
+ # `fp report --junit` reads the report, opens each test's file, and finds the fp:<slug>
6
+ # marker above it. That is why every testcase carries the file and line it was declared
7
+ # on.
8
+
9
+ require 'minitest'
10
+ require 'fileutils'
11
+
12
+ module MinitestJUnitReporter
13
+ class Reporter < Minitest::AbstractReporter
14
+ def initialize(output_path, base_dir)
15
+ super()
16
+ @output_path = output_path
17
+ @base_dir = File.expand_path(base_dir)
18
+ @results = []
19
+ end
20
+
21
+ def record(result)
22
+ @results << result
23
+ end
24
+
25
+ def report
26
+ FileUtils.mkdir_p(File.dirname(@output_path))
27
+ File.write(@output_path, xml)
28
+ end
29
+
30
+ private
31
+
32
+ def xml
33
+ failures = @results.count { |r| r.failures.any? { |f| !f.is_a?(Minitest::Skip) } }
34
+ skips = @results.count { |r| r.failures.any? { |f| f.is_a?(Minitest::Skip) } }
35
+ time = @results.sum(&:time)
36
+
37
+ body = @results.map { |r| testcase_xml(r) }.join
38
+
39
+ <<~XML
40
+ <?xml version="1.0" encoding="UTF-8"?>
41
+ <testsuites>
42
+ <testsuite name="minitest" tests="#{@results.size}" failures="#{failures}" errors="0" skipped="#{skips}" time="#{format('%.6f', time)}">
43
+ #{body} </testsuite>
44
+ </testsuites>
45
+ XML
46
+ end
47
+
48
+ def testcase_xml(result)
49
+ file, line = declaring_location(result)
50
+ attrs = [
51
+ %(name="#{esc(result.name)}"),
52
+ %(classname="#{esc(result.klass)}"),
53
+ %(file="#{esc(relative_to_base(file))}"),
54
+ # `line` is what lets `fp` bind each marker to the specific test below it. Without
55
+ # it fp can only handle files with exactly one marker.
56
+ (%(line="#{line}") if line),
57
+ %(time="#{format('%.6f', result.time)}")
58
+ ].compact.join(' ')
59
+
60
+ skip = result.failures.find { |f| f.is_a?(Minitest::Skip) }
61
+ real_failures = result.failures.reject { |f| f.is_a?(Minitest::Skip) }
62
+
63
+ if skip
64
+ %( <testcase #{attrs}>\n <skipped message="#{esc(skip.message)}"/>\n </testcase>\n)
65
+ elsif real_failures.any?
66
+ inner = real_failures.map do |f|
67
+ tag = f.is_a?(Minitest::UnexpectedError) ? 'error' : 'failure'
68
+ %( <#{tag} message="#{esc(f.message)}"/>\n)
69
+ end.join
70
+ %( <testcase #{attrs}>\n#{inner} </testcase>\n)
71
+ else
72
+ %( <testcase #{attrs}/>\n)
73
+ end
74
+ end
75
+
76
+ # Where the test method is declared, as [path, line]. The path is what `fp` opens to
77
+ # look for markers and the line is what binds a marker to its test, so this matters
78
+ # more than the rest of the report.
79
+ def declaring_location(result)
80
+ # Minitest 5.18+ exposes source_location on the result.
81
+ return result.source_location if result.respond_to?(:source_location)
82
+
83
+ # Older minitest: ask the test class for the method's definition site.
84
+ klass = Object.const_get(result.klass.to_s)
85
+ klass.instance_method(result.name).source_location || [nil, nil]
86
+ rescue StandardError
87
+ [nil, nil]
88
+ end
89
+
90
+ def relative_to_base(path)
91
+ return '' unless path
92
+
93
+ absolute = File.expand_path(path)
94
+ relative = absolute.delete_prefix("#{@base_dir}/")
95
+ relative == absolute ? path : relative
96
+ rescue StandardError
97
+ ''
98
+ end
99
+
100
+ def esc(str)
101
+ str.to_s
102
+ .gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
103
+ .gsub('"', '&quot;').gsub("'", '&apos;')
104
+ .gsub(/[^[:print:]\t\n]/, '')
105
+ end
106
+ end
107
+
108
+ class << self
109
+ # Register as a Minitest plugin. Minitest calls plugin_<name>_init while building its
110
+ # composite reporter, which is the only point where a reporter can still be added.
111
+ # No-op unless JUNIT_OUT is set, so ordinary test runs are unaffected.
112
+ def install!
113
+ path = ENV['JUNIT_OUT']
114
+ return if path.nil? || path.strip.empty?
115
+ return if @installed
116
+
117
+ @installed = true
118
+ @output_path = File.expand_path(path)
119
+ @base_dir = ENV['JUNIT_BASE_DIR'] || Dir.pwd
120
+
121
+ Minitest.extensions << 'junit_xml' unless Minitest.extensions.include?('junit_xml')
122
+
123
+ Minitest.singleton_class.define_method(:plugin_junit_xml_init) do |_options|
124
+ Minitest.reporter << MinitestJUnitReporter.build_reporter
125
+ end
126
+ end
127
+
128
+ def build_reporter
129
+ Reporter.new(@output_path, @base_dir)
130
+ end
131
+ end
132
+ end
133
+
134
+ MinitestJUnitReporter.install!
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ # The referee's rules, each bound to the requirement it proves by the fp: comment directly
6
+ # above it. `@server` pins the surface, since this repo is the game server.
7
+ class RulesTest < Minitest::Test
8
+ # fp:turn_starts_with_two_dice@server
9
+ def test_a_turn_rolls_two_six_sided_dice
10
+ 100.times do
11
+ dice = Hexhaven.roll
12
+ assert_equal 2, dice.size
13
+ dice.each { |die| assert_includes 1..6, die }
14
+ end
15
+ end
16
+
17
+ # fp:harbor_trades_three_for_one@server
18
+ def test_three_wool_become_one_brick_at_a_harbor
19
+ hand = Hexhaven.harbor_trade({ wool: 3 }, give: :wool, get: :brick)
20
+
21
+ assert_equal({ wool: 0, brick: 1 }, hand)
22
+ end
23
+
24
+ # fp:harbor_trades_three_for_one@server
25
+ def test_a_harbor_refuses_fewer_than_three
26
+ assert_raises(ArgumentError) { Hexhaven.harbor_trade({ wool: 2 }, give: :wool, get: :brick) }
27
+ end
28
+
29
+ # 👉 Your move: put this line (uncommented) above the test, or ask your agent to.
30
+ # # fp:tallest_tower_scores_two_points@server
31
+ def test_a_six_floor_tower_takes_the_bonus_from_a_five_floor_tower
32
+ assert_equal :ada, Hexhaven.tallest_tower({ ada: 5, ben: 3 })
33
+ assert_equal :ben, Hexhaven.tallest_tower({ ada: 5, ben: 6 }, holder: :ada)
34
+ assert_nil Hexhaven.tallest_tower({ ada: 4, ben: 4 })
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ # Writes a JUnit report when JUNIT_OUT is set, which is what `fp report --junit` reads.
5
+ require_relative 'junit_reporter'
6
+ require_relative '../lib/hexhaven'
data/lib/fp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fp
4
- VERSION = '0.0.26'
4
+ VERSION = '0.0.28'
5
5
  end
data/skills/fp/SKILL.md CHANGED
@@ -202,23 +202,33 @@ Before drafting:
202
202
 
203
203
  Write each field for a reviewer, not for the implementation:
204
204
 
205
- - **Title:** one clear product promise in present tense. Name the capability or outcome, not
206
- the project (`Dispatchers see current shipment status`, not `Implement status polling`).
207
- - **Why:** one or two natural sentences saying who needs this, the problem or risk, and the
208
- outcome worth protecting. Do not repeat the title, advertise the feature, or explain how
209
- it will be built. Delete AI filler such as “ensure a seamless, robust experience.”
210
- - **Acceptance:** observable behaviour that lets someone decide whether the promise is true.
211
- State the relevant trigger or situation, the result, and material boundaries. Include
212
- failure behaviour, permissions, or cross-surface consistency when those affect the promise.
213
- - **Examples:** use concrete examples or Given/When/Then cases when they clarify a rule or an
214
- edge. Examples and executable tests ground and double-check the specification; they do not
215
- define every case or replace the general rule and continued conversation.
205
+ - **Title:** one clear product promise in present tense, with a concrete subject and active
206
+ verb. Name the capability or outcome, not the project (`Dispatchers see current shipment
207
+ status`, not `Implement status polling`).
208
+ - **Why:** one natural sentence saying who needs this, the problem or risk, and the outcome
209
+ worth protecting. Do not repeat the title, narrate the request or bug history, advertise
210
+ the feature, or explain how it will be built. Delete AI filler such as “ensure a seamless,
211
+ robust experience.”
212
+ - **Acceptance:** one independently verifiable rule of observable behaviour that lets someone
213
+ decide whether the promise is true. State the relevant trigger or situation, the result, and
214
+ material boundaries that qualify that same promise. Given/When/Then may clarify those parts, but it
215
+ is not a mandatory template. Split a second result that could fail independently into its
216
+ own requirement instead of comma-chaining it here.
217
+ - **Example:** supply one concise concrete case that would pass the general rule, and put it
218
+ after the rule in Acceptance as `Example: ...`. Use the domain's real terms and
219
+ representative data, but ask rather than invent consequential policy. Do not mistake a few
220
+ examples for the rule or let executable tests stand for every case; neither replaces the
221
+ general rule and continued conversation.
216
222
  - **Numbers and policy:** include a number only when the source made it a real product decision
217
223
  or an external constraint requires it. Do not invent implementation defaults and present
218
224
  them as scope.
219
225
  - **Implementation detail:** omit components, data models, algorithms, endpoints, polling
220
226
  intervals, and UI controls unless that interface or constraint is itself user-visible policy.
221
227
 
228
+ State the promise itself rather than commentary about it. Avoid openings such as “This
229
+ feature…”, “This requirement…”, “The goal is…”, “Users should be able to…”, and “The
230
+ system should…”. Use the real actor or product as the subject and an active verb.
231
+
222
232
  A good requirement is concise, not merely short. Keep every sentence that changes a decision;
223
233
  remove everything that only sounds thorough.
224
234
 
@@ -230,10 +240,12 @@ remove everything that only sounds thorough.
230
240
  **Acceptance:** `Use a background worker and refresh every 30 seconds.`
231
241
  This invents policy, prescribes a design, and never states the outcome.
232
242
 
233
- Before saving, read the draft as a manager deciding scope: Is the value clear? Can the result
234
- be observed? Are real product decisions distinguished from guesses? Could every clause fail
235
- independently and, if so, should it be split? Does every sentence earn its place? Revise or ask
236
- before creating the record if the answer is no.
243
+ Before saving, answer three questions against the actual fields: Who is this promise for? How
244
+ would the concrete example prove it done? Why does the outcome matter? If any answer is missing
245
+ or vague, revise the draft or ask the user. Then read it as a manager deciding scope: Is the
246
+ value clear? Can the result be observed? Are real product decisions distinguished from guesses?
247
+ Could every clause fail independently and, if so, should it be split? Does every sentence earn
248
+ its place? Do not create the record until the draft passes this review.
237
249
 
238
250
  Then create the proposal as a draft:
239
251
 
@@ -266,6 +266,12 @@ fp propose --project stowzilla \
266
266
  | `--required` | No | Comma-separated surfaces that must implement this |
267
267
  | `--acceptance` | No | Observable behaviour and material boundaries |
268
268
 
269
+ After creating the draft, `fp propose` prints its quality feedback — a verdict, the issues
270
+ found, and suggestions — in the same run (`analysis` in `--json`). Act on it: revise a weak
271
+ draft with `fp propose-change`, or withdraw one that shouldn't be a requirement. When analysis
272
+ is unavailable the draft is still created, and the output says the feedback couldn't be
273
+ produced (`analysis_error`).
274
+
269
275
  **Write for a decision-maker, not an implementer.** The result should sound natural and be
270
276
  ready for management review without someone translating AI boilerplate or reverse-engineering
271
277
  the intended outcome. Treat it as the current record of an ongoing conversation, not a finished
@@ -370,6 +376,27 @@ the review decision where one was made. An open proposal is called out at the en
370
376
 
371
377
  `--project` is optional when the repo has a `.featureparity.yml`.
372
378
 
379
+ ### fp comments
380
+
381
+ Read or join the team's conversation on a requirement, and tag people or agents in it.
382
+
383
+ ```bash
384
+ fp comments print_container_qr --project stowzilla # comments + who can be tagged
385
+ fp comments print_container_qr --body "The label redesign dropped this — retire it?" \
386
+ --tag andy@example.com
387
+ fp comments print_container_qr --body "Can you re-check this against the new label?" --tag claude
388
+ ```
389
+
390
+ A comment never changes the requirement. `--tag` takes a teammate's email or name, or one of
391
+ the team's agents by name or key name; an unknown or ambiguous tag stops the command and
392
+ posts nothing. Tagged people get an email; each tagged agent fires the project's
393
+ `requirement.agent_mentioned` webhook, whose payload names the agent, the requirement and
394
+ the comment, plus the `fp` commands to read and answer it. If a human tagged you, answer
395
+ with `fp comments <slug> --body "..."` — and if the question means the requirement is
396
+ wrong, propose the change with `fp propose-change` rather than settling it in a comment.
397
+
398
+ `fp comment` (singular) is different: it threads a comment onto an open change proposal.
399
+
373
400
  ### fp analyze
374
401
 
375
402
  Triage requirement quality: duplicates, fragments of a broader promise, vague wording,
@@ -459,6 +486,52 @@ classifications remain open for a human product decision. A stale candidate is r
459
486
  later fingerprint always requires a new assessment. Assessing never changes the requirement
460
487
  wording or CI outcome.
461
488
 
489
+ ### fp spot-check
490
+
491
+ A random drug test for evidence. A green receipt proves a marked test ran and passed, not
492
+ that it checks what the requirement promises. `fp spot-check` picks one active requirement
493
+ that has evidence on record, at random, and prints its why and acceptance, its receipts, and
494
+ the source of every `fp:<slug>` test it finds in the checkout, then a brief.
495
+
496
+ ```bash
497
+ fp spot-check # random pick; prints the seed
498
+ fp spot-check --seed 4821 # repeat that pick
499
+ fp spot-check --surface cli --json # narrow to one surface; machine-readable
500
+ fp spot-check print_container_qr print_label # chosen requirements, any status
501
+ fp spot-check --count 5 # five distinct requirements at random
502
+ fp spot-check --all # every active requirement with evidence
503
+ fp spot-check --test test/qr_test.rb # every marked test in the file
504
+ fp spot-check --test test/qr_test.rb:42 # only the test spanning that line
505
+ fp spot-check --path ../android # look for the tests in another checkout
506
+ ```
507
+
508
+ Use one way of choosing per run: slugs, `--count`, `--all`, or `--test` (which may be
509
+ repeated). The seed is printed only for the random picks. In `--json` the result is
510
+ `checks` (one entry per requirement, each with `requirement`, `evidence` and `tests`), plus
511
+ a single `brief` that applies to every check.
512
+
513
+ `--prompt` prints the whole spot check as instructions for an agent, so a person or a
514
+ schedule can hand it over as is (`fp spot-check --count 3 --prompt | claude -p`). It ends
515
+ with what to report for each requirement: `proves`, `does_not_prove` or `uncertain`, the
516
+ clauses no assertion checks, the probe made and the tests that failed, and a clean
517
+ `git status`. It can't be combined with `--json`.
518
+
519
+ `--jev` first sends each requirement's marked tests (up to 10) to Jev and adds its read:
520
+ `agree`, `gaps` or `uncertain`, how likely the tests are to cover the acceptance, the
521
+ likeliest gap (`unasserted_clause`, `asserts_other_behaviour`, `cannot_fail`,
522
+ `tests_a_double`), and a per-test likelihood. Treat it as a hint about where to aim the
523
+ probe. It doesn't replace the probe. Jev is for paid plans: a person on Pro or Lifetime,
524
+ or a project whose owner is (that is what lets an agent use it). Otherwise the spot check
525
+ prints a note and carries on without it. Nothing Jev reads is stored.
526
+
527
+ The brief: list each result and boundary the acceptance promises; name the clause each
528
+ test's assertions actually check; make one small change to the *product code* that breaks
529
+ the promise, run only those tests, and confirm one fails; revert and confirm `git status` is
530
+ clean. A test that stays green under the probe, or checks something the acceptance doesn't
531
+ say, doesn't prove the requirement. Report that to a human with what you tried. Don't
532
+ rewrite the test or the requirement as part of a spot check. The command itself is
533
+ read-only.
534
+
462
535
  #### Upgrading an existing project
463
536
 
464
537
  Changed-test auditing requires fp 0.0.17 or newer and the corresponding backend deployment.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: featureparity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.26
4
+ version: 0.0.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-27 00:00:00.000000000 Z
11
+ date: 2026-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http
@@ -103,6 +103,7 @@ files:
103
103
  - lib/fp/commands/check.rb
104
104
  - lib/fp/commands/ci_report.rb
105
105
  - lib/fp/commands/comment.rb
106
+ - lib/fp/commands/comments.rb
106
107
  - lib/fp/commands/config_cmd.rb
107
108
  - lib/fp/commands/conversation.rb
108
109
  - lib/fp/commands/export.rb
@@ -121,7 +122,9 @@ files:
121
122
  - lib/fp/commands/show.rb
122
123
  - lib/fp/commands/showroom.rb
123
124
  - lib/fp/commands/showroom_media.rb
125
+ - lib/fp/commands/spot_check.rb
124
126
  - lib/fp/commands/surfaces.rb
127
+ - lib/fp/commands/tutorial.rb
125
128
  - lib/fp/commands/version.rb
126
129
  - lib/fp/commands/whoami.rb
127
130
  - lib/fp/config.rb
@@ -130,6 +133,12 @@ files:
130
133
  - lib/fp/parity.rb
131
134
  - lib/fp/project_config.rb
132
135
  - lib/fp/source_fingerprint.rb
136
+ - lib/fp/tutorial/hexhaven/AGENTS.md
137
+ - lib/fp/tutorial/hexhaven/README.md
138
+ - lib/fp/tutorial/hexhaven/lib/hexhaven.rb
139
+ - lib/fp/tutorial/hexhaven/test/junit_reporter.rb
140
+ - lib/fp/tutorial/hexhaven/test/rules_test.rb
141
+ - lib/fp/tutorial/hexhaven/test/test_helper.rb
133
142
  - lib/fp/version.rb
134
143
  - skills/fp/SKILL.md
135
144
  - skills/fp/references/cli.md