lex-planner 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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +34 -0
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.rspec_status +40 -0
- data/.rubocop.yml +11 -0
- data/CLAUDE.md +37 -0
- data/Gemfile +14 -0
- data/lex-planner.gemspec +42 -0
- data/lib/legion/extensions/planner/actors/planner.rb +25 -0
- data/lib/legion/extensions/planner/helpers/context_gatherer.rb +88 -0
- data/lib/legion/extensions/planner/helpers/plan_schema.rb +53 -0
- data/lib/legion/extensions/planner/helpers/spec_parser.rb +63 -0
- data/lib/legion/extensions/planner/runners/planner.rb +149 -0
- data/lib/legion/extensions/planner/transport/exchanges/planner.rb +21 -0
- data/lib/legion/extensions/planner/transport/queues/planner.rb +29 -0
- data/lib/legion/extensions/planner/version.rb +9 -0
- data/lib/legion/extensions/planner.rb +26 -0
- metadata +231 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bd1eacdf6999ca3ef644d70ed3ef96c836146f150f64ba695dc28382ba9cf3ce
|
|
4
|
+
data.tar.gz: bf663219e4f2a9b008a32932b7fcb38745e7b260b9d8b2ca01f9c3d2e085bafa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 559d4836a582609af5c74bdd6dbe8cb11f6bd285888301e5d724d6c513d071a9580c5518c5bf80392fcef0144c2260be9031d9564d69eda76e3f1736c19b589c
|
|
7
|
+
data.tar.gz: f223e47745de570b480322636dfa330d79ced9fe5f29777e040fbea331ceba9feb6618f7cdd626e1fe969fa10e7a41ce63033e8b28686f19a6fd244e342d0409
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
schedule:
|
|
7
|
+
- cron: '0 9 * * 1'
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci:
|
|
11
|
+
uses: LegionIO/.github/.github/workflows/ci.yml@main
|
|
12
|
+
|
|
13
|
+
excluded-files:
|
|
14
|
+
uses: LegionIO/.github/.github/workflows/excluded-files.yml@main
|
|
15
|
+
|
|
16
|
+
security:
|
|
17
|
+
uses: LegionIO/.github/.github/workflows/security-scan.yml@main
|
|
18
|
+
|
|
19
|
+
version-changelog:
|
|
20
|
+
uses: LegionIO/.github/.github/workflows/version-changelog.yml@main
|
|
21
|
+
|
|
22
|
+
dependency-review:
|
|
23
|
+
uses: LegionIO/.github/.github/workflows/dependency-review.yml@main
|
|
24
|
+
|
|
25
|
+
stale:
|
|
26
|
+
if: github.event_name == 'schedule'
|
|
27
|
+
uses: LegionIO/.github/.github/workflows/stale.yml@main
|
|
28
|
+
|
|
29
|
+
release:
|
|
30
|
+
needs: [ci, excluded-files]
|
|
31
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
32
|
+
uses: LegionIO/.github/.github/workflows/release.yml@main
|
|
33
|
+
secrets:
|
|
34
|
+
rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Gemfile.lock
|
data/.rspec
ADDED
data/.rspec_status
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
example_id | status | run_time |
|
|
2
|
+
------------------------------------------------------------------------ | ------ | --------------- |
|
|
3
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:1:1] | passed | 0.00047 seconds |
|
|
4
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:1:2] | passed | 0.00105 seconds |
|
|
5
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:1:3] | passed | 0.00005 seconds |
|
|
6
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:1:4] | passed | 0.00004 seconds |
|
|
7
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:2:1] | passed | 0.00005 seconds |
|
|
8
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:2:2] | passed | 0.00036 seconds |
|
|
9
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:3:1] | passed | 0.00003 seconds |
|
|
10
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:3:2] | passed | 0.00004 seconds |
|
|
11
|
+
./spec/legion/extensions/planner/helpers/context_gatherer_spec.rb[1:4:1] | passed | 0.00112 seconds |
|
|
12
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:1:1] | passed | 0.00003 seconds |
|
|
13
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:1:2] | passed | 0.00003 seconds |
|
|
14
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:1:3] | passed | 0.00002 seconds |
|
|
15
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:1:4] | passed | 0.00002 seconds |
|
|
16
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:1:5] | passed | 0.00022 seconds |
|
|
17
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:1:6] | passed | 0.00002 seconds |
|
|
18
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:2:1] | passed | 0.00048 seconds |
|
|
19
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:2:2] | passed | 0.00044 seconds |
|
|
20
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:2:3] | passed | 0.00004 seconds |
|
|
21
|
+
./spec/legion/extensions/planner/helpers/plan_schema_spec.rb[1:2:4] | passed | 0.00003 seconds |
|
|
22
|
+
./spec/legion/extensions/planner/helpers/spec_parser_spec.rb[1:1:1] | passed | 0.00005 seconds |
|
|
23
|
+
./spec/legion/extensions/planner/helpers/spec_parser_spec.rb[1:1:2] | passed | 0.00005 seconds |
|
|
24
|
+
./spec/legion/extensions/planner/helpers/spec_parser_spec.rb[1:1:3] | passed | 0.00004 seconds |
|
|
25
|
+
./spec/legion/extensions/planner/helpers/spec_parser_spec.rb[1:1:4] | passed | 0.00002 seconds |
|
|
26
|
+
./spec/legion/extensions/planner/helpers/spec_parser_spec.rb[1:1:5] | passed | 0.00003 seconds |
|
|
27
|
+
./spec/legion/extensions/planner/helpers/spec_parser_spec.rb[1:2:1] | passed | 0.00064 seconds |
|
|
28
|
+
./spec/legion/extensions/planner/helpers/spec_parser_spec.rb[1:2:2] | passed | 0.00003 seconds |
|
|
29
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:1:1] | passed | 0.00023 seconds |
|
|
30
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:1:2] | passed | 0.00005 seconds |
|
|
31
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:1:3] | passed | 0.00006 seconds |
|
|
32
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:1:4] | passed | 0.00004 seconds |
|
|
33
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:1:5] | passed | 0.00004 seconds |
|
|
34
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:1:6] | passed | 0.00004 seconds |
|
|
35
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:1:7] | passed | 0.00004 seconds |
|
|
36
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:2:1] | passed | 0.00005 seconds |
|
|
37
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:2:2] | passed | 0.00004 seconds |
|
|
38
|
+
./spec/legion/extensions/planner/runners/planner_spec.rb[1:2:3] | passed | 0.00004 seconds |
|
|
39
|
+
./spec/legion/extensions/planner/version_spec.rb[1:1] | passed | 0.00002 seconds |
|
|
40
|
+
./spec/legion/extensions/planner/version_spec.rb[1:2] | passed | 0.00003 seconds |
|
data/.rubocop.yml
ADDED
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# lex-planner: Fleet Pipeline Planning
|
|
2
|
+
|
|
3
|
+
**Level 3 Documentation**
|
|
4
|
+
- **Parent**: `CLAUDE.md` (monorepo root)
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Second stage of the Fleet Pipeline. Receives assessed work items, gathers repo context (file tree, documentation, relevant source files), decomposes work into structured implementation plans using LLM structured output, and routes to the developer stage. Supports multi-solver planning (Dr. Zero pattern) when `config.planning.solvers > 1`.
|
|
9
|
+
|
|
10
|
+
**Gem**: `lex-planner`
|
|
11
|
+
**Version**: 0.1.0
|
|
12
|
+
**Namespace**: `Legion::Extensions::Planner`
|
|
13
|
+
|
|
14
|
+
## Runners
|
|
15
|
+
|
|
16
|
+
### `Runners::Planner`
|
|
17
|
+
- `plan(work_item:, **)` -- Gather context, generate LLM plan, validate, return planned work item
|
|
18
|
+
- `gather_context(work_item:, **)` -- Fetch repo file tree, docs, and relevant files; cache in Redis
|
|
19
|
+
|
|
20
|
+
## Helpers
|
|
21
|
+
|
|
22
|
+
- `Helpers::PlanSchema` -- LLM structured output schema for plans + validation
|
|
23
|
+
- `Helpers::SpecParser` -- Markdown spec/requirements parsing (absorbed from lex-factory)
|
|
24
|
+
- `Helpers::ContextGatherer` -- Fetch and cache repo context (file tree, docs, relevant files)
|
|
25
|
+
|
|
26
|
+
## Transport
|
|
27
|
+
|
|
28
|
+
- Exchange: `lex.planner` (topic, durable)
|
|
29
|
+
- Queue: `lex.planner.runners.planner` (durable, routing key `lex.planner.runners.planner.#`)
|
|
30
|
+
|
|
31
|
+
## Development
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle install
|
|
35
|
+
bundle exec rspec
|
|
36
|
+
bundle exec rubocop
|
|
37
|
+
```
|
data/Gemfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
group :test do
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rspec', '~> 3.13'
|
|
9
|
+
gem 'rspec_junit_formatter'
|
|
10
|
+
gem 'rubocop', '~> 1.75'
|
|
11
|
+
gem 'rubocop-legion', '~> 0.1', require: false
|
|
12
|
+
gem 'rubocop-rspec'
|
|
13
|
+
gem 'simplecov'
|
|
14
|
+
end
|
data/lex-planner.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/planner/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-planner'
|
|
7
|
+
spec.version = Legion::Extensions::Planner::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Legion::Extensions::Planner'
|
|
12
|
+
spec.description = 'Fleet pipeline planning: decompose work items into implementation plans'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-planner'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-planner'
|
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-planner'
|
|
20
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-planner'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-planner/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
|
|
29
|
+
spec.add_dependency 'legion-cache', '>= 1.3.11'
|
|
30
|
+
spec.add_dependency 'legion-crypt', '>= 1.4.9'
|
|
31
|
+
spec.add_dependency 'legion-data', '>= 1.4.17'
|
|
32
|
+
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
33
|
+
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
34
|
+
spec.add_dependency 'legion-settings', '>= 1.3.14'
|
|
35
|
+
spec.add_dependency 'legion-transport', '>= 1.3.9'
|
|
36
|
+
|
|
37
|
+
spec.add_development_dependency 'rspec', '~> 3.13'
|
|
38
|
+
spec.add_development_dependency 'rspec_junit_formatter'
|
|
39
|
+
spec.add_development_dependency 'rubocop', '~> 1.75'
|
|
40
|
+
spec.add_development_dependency 'rubocop-rspec'
|
|
41
|
+
spec.add_development_dependency 'simplecov'
|
|
42
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return unless defined?(Legion::Extensions::Actors::Subscription)
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Planner
|
|
8
|
+
module Actor
|
|
9
|
+
class Planner < Legion::Extensions::Actors::Subscription
|
|
10
|
+
def runner_function
|
|
11
|
+
'plan'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check_subtask?
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_task?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planner
|
|
6
|
+
module Helpers
|
|
7
|
+
# rubocop:disable Legion/HelperMigration/DirectJson, Legion/HelperMigration/DirectCache
|
|
8
|
+
module ContextGatherer
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
DOC_FILE_NAMES = %w[CLAUDE.md README.md AGENTS.md CONTRIBUTING.md CHANGELOG.md].freeze
|
|
12
|
+
CACHE_PREFIX = 'fleet:context:'
|
|
13
|
+
DEFAULT_TTL = 86_400 # 24 hours
|
|
14
|
+
|
|
15
|
+
def gather(repo:, config:, work_item_id:, keywords: [], **)
|
|
16
|
+
context = {
|
|
17
|
+
file_tree: config[:load_file_tree] ? fetch_file_tree(repo: repo) : [],
|
|
18
|
+
docs: config[:load_repo_docs] ? fetch_docs(repo: repo) : {},
|
|
19
|
+
relevant_files: fetch_relevant_files(
|
|
20
|
+
repo: repo,
|
|
21
|
+
keywords: keywords,
|
|
22
|
+
max_files: config[:max_context_files] || 50
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
cache_key = store_context!(work_item_id: work_item_id, context: context)
|
|
27
|
+
context.merge(context_ref: cache_key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def store_context!(work_item_id:, context:)
|
|
31
|
+
key = "#{CACHE_PREFIX}#{work_item_id}"
|
|
32
|
+
serialized = json_dump(context)
|
|
33
|
+
cache_set(key, serialized, ttl: DEFAULT_TTL)
|
|
34
|
+
key
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_context(context_ref:)
|
|
38
|
+
raw = cache_get(context_ref)
|
|
39
|
+
return nil if raw.nil?
|
|
40
|
+
|
|
41
|
+
json_load(raw)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def doc_file_names
|
|
45
|
+
DOC_FILE_NAMES
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def json_dump(obj)
|
|
51
|
+
Legion::JSON.dump(obj)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def json_load(str)
|
|
55
|
+
Legion::JSON.load(str)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cache_set(key, value, ttl: nil)
|
|
59
|
+
Legion::Cache.set(key, value, ttl: ttl)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cache_get(key)
|
|
63
|
+
Legion::Cache.get(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fetch_file_tree(repo:) # rubocop:disable Lint/UnusedMethodArgument
|
|
67
|
+
# In production, calls lex-github or lex-exec to get the file tree.
|
|
68
|
+
# Stubbed here — implementation depends on WS-01 (lex-exec) and WS-02 (lex-github).
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fetch_docs(repo:) # rubocop:disable Lint/UnusedMethodArgument
|
|
73
|
+
# In production, fetches CLAUDE.md, README.md, AGENTS.md from the repo.
|
|
74
|
+
# Stubbed here — implementation depends on WS-02 (lex-github).
|
|
75
|
+
{}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def fetch_relevant_files(repo:, keywords:, max_files:) # rubocop:disable Lint/UnusedMethodArgument
|
|
79
|
+
# In production, searches repo files by keyword relevance.
|
|
80
|
+
# Stubbed here — implementation depends on WS-01/WS-02.
|
|
81
|
+
[]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
# rubocop:enable Legion/HelperMigration/DirectJson, Legion/HelperMigration/DirectCache
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planner
|
|
6
|
+
module Helpers
|
|
7
|
+
module PlanSchema
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
SCHEMA = {
|
|
11
|
+
type: :object,
|
|
12
|
+
properties: {
|
|
13
|
+
approach: { type: :string },
|
|
14
|
+
files_to_modify: {
|
|
15
|
+
type: :array,
|
|
16
|
+
items: {
|
|
17
|
+
type: :object,
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: :string },
|
|
20
|
+
action: { type: :string, enum: %w[modify create delete] },
|
|
21
|
+
reason: { type: :string }
|
|
22
|
+
},
|
|
23
|
+
required: %i[path action reason]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
files_to_read: { type: :array, items: { type: :string } },
|
|
27
|
+
test_strategy: { type: :string },
|
|
28
|
+
estimated_changes: { type: :integer }
|
|
29
|
+
},
|
|
30
|
+
required: %i[approach files_to_modify test_strategy estimated_changes]
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def schema
|
|
34
|
+
SCHEMA
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_plan(plan:)
|
|
38
|
+
errors = []
|
|
39
|
+
SCHEMA[:required].each do |field|
|
|
40
|
+
errors << "Missing required field: #{field}" unless plan.key?(field) && !plan[field].nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
errors << 'files_to_modify must not be empty' if plan[:files_to_modify].is_a?(Array) && plan[:files_to_modify].empty?
|
|
44
|
+
|
|
45
|
+
errors << 'estimated_changes must be an integer' if plan.key?(:estimated_changes) && !plan[:estimated_changes].is_a?(Integer)
|
|
46
|
+
|
|
47
|
+
{ valid: errors.empty?, errors: errors }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planner
|
|
6
|
+
module Helpers
|
|
7
|
+
module SpecParser
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def parse(content:)
|
|
11
|
+
return { title: nil, sections: {} } if content.nil? || content.strip.empty?
|
|
12
|
+
|
|
13
|
+
lines = content.lines.map(&:rstrip)
|
|
14
|
+
title = extract_title(lines)
|
|
15
|
+
sections = extract_sections(lines)
|
|
16
|
+
|
|
17
|
+
{ title: title, sections: sections }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def extract_requirements(content:)
|
|
21
|
+
parsed = parse(content: content)
|
|
22
|
+
section = parsed[:sections]['Requirements']
|
|
23
|
+
return [] if section.nil?
|
|
24
|
+
|
|
25
|
+
section.lines
|
|
26
|
+
.map(&:strip)
|
|
27
|
+
.select { |line| line.start_with?('- ') }
|
|
28
|
+
.map { |line| line.sub(/\A-\s*/, '') }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def extract_title(lines)
|
|
34
|
+
title_line = lines.find { |l| l.match?(/\A#\s+/) }
|
|
35
|
+
return nil unless title_line
|
|
36
|
+
|
|
37
|
+
title_line.sub(/\A#\s+/, '').strip
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def extract_sections(lines)
|
|
41
|
+
sections = {}
|
|
42
|
+
current_section = nil
|
|
43
|
+
current_content = []
|
|
44
|
+
|
|
45
|
+
lines.each do |line|
|
|
46
|
+
if line.match?(/\A##\s+/)
|
|
47
|
+
sections[current_section] = current_content.join("\n").strip if current_section
|
|
48
|
+
current_section = line.sub(/\A##\s+/, '').strip
|
|
49
|
+
current_content = []
|
|
50
|
+
elsif current_section
|
|
51
|
+
current_content << line
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sections[current_section] = current_content.join("\n").strip if current_section
|
|
56
|
+
|
|
57
|
+
sections
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planner
|
|
6
|
+
module Runners
|
|
7
|
+
module Planner
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def plan(results: nil, work_item: nil, args: nil, **)
|
|
11
|
+
results = json_load(results) if results.is_a?(String)
|
|
12
|
+
work_item ||= results&.dig(:work_item) || args&.dig(:work_item)
|
|
13
|
+
raise ArgumentError, "work_item is nil in #{__method__}" if work_item.nil?
|
|
14
|
+
|
|
15
|
+
max_iterations = work_item.dig(:config, :planning, :max_iterations) ||
|
|
16
|
+
Legion::Settings.dig(:fleet, :planning, :max_iterations) || 5
|
|
17
|
+
|
|
18
|
+
context = Helpers::ContextGatherer.gather(
|
|
19
|
+
repo: work_item[:repo],
|
|
20
|
+
config: work_item[:config][:context] || {},
|
|
21
|
+
work_item_id: work_item[:work_item_id],
|
|
22
|
+
keywords: extract_keywords(work_item)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
plan_result = nil
|
|
26
|
+
validation = nil
|
|
27
|
+
max_iterations.times do
|
|
28
|
+
plan_result = generate_plan(work_item: work_item, context: context)
|
|
29
|
+
validation = Helpers::PlanSchema.validate_plan(plan: plan_result)
|
|
30
|
+
break if validation[:valid]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
unless validation[:valid]
|
|
34
|
+
return { success: false, reason: :invalid_plan, errors: validation[:errors],
|
|
35
|
+
work_item: work_item }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
work_item = work_item.merge(
|
|
39
|
+
pipeline: work_item[:pipeline].merge(
|
|
40
|
+
stage: 'planned',
|
|
41
|
+
plan: plan_result,
|
|
42
|
+
context_ref: context[:context_ref],
|
|
43
|
+
trace: work_item[:pipeline][:trace] + [build_trace_entry]
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
{ success: true, work_item: work_item }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def gather_context(results: nil, work_item: nil, args: nil, **)
|
|
51
|
+
results = json_load(results) if results.is_a?(String)
|
|
52
|
+
work_item ||= results&.dig(:work_item) || args&.dig(:work_item)
|
|
53
|
+
raise ArgumentError, "work_item is nil in #{__method__}" if work_item.nil?
|
|
54
|
+
|
|
55
|
+
context = Helpers::ContextGatherer.gather(
|
|
56
|
+
repo: work_item[:repo],
|
|
57
|
+
config: work_item[:config][:context] || {},
|
|
58
|
+
work_item_id: work_item[:work_item_id],
|
|
59
|
+
keywords: extract_keywords(work_item)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
{ success: true, context: context, context_ref: context[:context_ref] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def generate_plan(work_item:, context:)
|
|
68
|
+
prompt = build_plan_prompt(work_item: work_item, context: context)
|
|
69
|
+
exclude = build_model_exclusions(work_item)
|
|
70
|
+
|
|
71
|
+
Legion::LLM::Prompt.dispatch(
|
|
72
|
+
prompt,
|
|
73
|
+
schema: Helpers::PlanSchema::SCHEMA,
|
|
74
|
+
tools: [],
|
|
75
|
+
exclude: exclude,
|
|
76
|
+
intent: { capability: difficulty_to_capability(work_item) }
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_model_exclusions(work_item)
|
|
81
|
+
exclude = {}
|
|
82
|
+
work_item[:pipeline][:trace].each do |t|
|
|
83
|
+
next unless t[:model]
|
|
84
|
+
|
|
85
|
+
(exclude[t[:provider]&.to_sym] ||= []) << t[:model]
|
|
86
|
+
end
|
|
87
|
+
exclude.each_value(&:uniq!)
|
|
88
|
+
exclude
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_plan_prompt(work_item:, context:)
|
|
92
|
+
<<~PROMPT
|
|
93
|
+
You are a software architect planning an implementation for a coding task.
|
|
94
|
+
|
|
95
|
+
## Work Item
|
|
96
|
+
Title: #{work_item[:title]}
|
|
97
|
+
Description: #{work_item[:description]}
|
|
98
|
+
Source: #{work_item[:source_ref]}
|
|
99
|
+
Language: #{work_item.dig(:repo, :language) || 'unknown'}
|
|
100
|
+
Repository: #{work_item.dig(:repo, :owner)}/#{work_item.dig(:repo, :name)}
|
|
101
|
+
|
|
102
|
+
## Repository Context
|
|
103
|
+
File tree: #{context[:file_tree]&.first(100)&.join("\n") || 'Not available'}
|
|
104
|
+
Documentation: #{context[:docs]&.map { |k, v| "#{k}:\n#{v}" }&.join("\n\n") || 'Not available'}
|
|
105
|
+
|
|
106
|
+
## Task
|
|
107
|
+
Create a detailed implementation plan. Specify which files to modify/create/delete,
|
|
108
|
+
the overall approach, a test strategy, and estimated number of file changes.
|
|
109
|
+
PROMPT
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_keywords(work_item)
|
|
113
|
+
text = "#{work_item[:title]} #{work_item[:description]}"
|
|
114
|
+
text.downcase.scan(/[a-z_]+/).uniq.reject { |w| w.length < 4 }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def difficulty_to_capability(work_item)
|
|
118
|
+
difficulty = work_item.dig(:config, :estimated_difficulty) || 0.5
|
|
119
|
+
case difficulty
|
|
120
|
+
when 0.0...0.3 then :basic
|
|
121
|
+
when 0.3...0.6 then :moderate
|
|
122
|
+
else :reasoning
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_trace_entry
|
|
127
|
+
{
|
|
128
|
+
stage: 'planner',
|
|
129
|
+
node: node_name,
|
|
130
|
+
model: nil,
|
|
131
|
+
provider: nil,
|
|
132
|
+
started_at: ::Time.now.utc.iso8601,
|
|
133
|
+
completed_at: ::Time.now.utc.iso8601,
|
|
134
|
+
token_usage: nil
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def node_name
|
|
139
|
+
if defined?(Legion::Settings)
|
|
140
|
+
Legion::Settings.dig(:node, :name) || 'unknown'
|
|
141
|
+
else
|
|
142
|
+
'unknown'
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planner
|
|
6
|
+
module Transport
|
|
7
|
+
module Exchanges
|
|
8
|
+
class Planner < Legion::Transport::Exchange
|
|
9
|
+
def exchange_name
|
|
10
|
+
'lex.planner'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def exchange_options
|
|
14
|
+
{ type: 'topic', durable: true }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Planner
|
|
6
|
+
module Transport
|
|
7
|
+
module Queues
|
|
8
|
+
class Planner < Legion::Transport::Queue
|
|
9
|
+
def queue_name
|
|
10
|
+
'lex.planner.runners.planner'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def queue_options
|
|
14
|
+
{ durable: true }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def routing_key
|
|
18
|
+
'lex.planner.runners.planner.#'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def exchange
|
|
22
|
+
Exchanges::Planner
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'planner/version'
|
|
4
|
+
require_relative 'planner/helpers/plan_schema'
|
|
5
|
+
require_relative 'planner/helpers/spec_parser'
|
|
6
|
+
require_relative 'planner/helpers/context_gatherer'
|
|
7
|
+
require_relative 'planner/runners/planner'
|
|
8
|
+
|
|
9
|
+
if defined?(Legion::Transport::Exchange)
|
|
10
|
+
require_relative 'planner/transport/exchanges/planner'
|
|
11
|
+
require_relative 'planner/transport/queues/planner'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require_relative 'planner/actors/planner'
|
|
15
|
+
|
|
16
|
+
module Legion
|
|
17
|
+
module Extensions
|
|
18
|
+
module Planner
|
|
19
|
+
extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
|
|
20
|
+
|
|
21
|
+
def self.llm_required?
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-planner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
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: legion-cache
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 1.3.11
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 1.3.11
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: legion-crypt
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.4.9
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.4.9
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: legion-data
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 1.4.17
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 1.4.17
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: legion-json
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 1.2.1
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 1.2.1
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: legion-logging
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.3.2
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.3.2
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: legion-settings
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 1.3.14
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 1.3.14
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: legion-transport
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 1.3.9
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 1.3.9
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rspec
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.13'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.13'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rspec_junit_formatter
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: rubocop
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '1.75'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '1.75'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: rubocop-rspec
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - ">="
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '0'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0'
|
|
166
|
+
- !ruby/object:Gem::Dependency
|
|
167
|
+
name: simplecov
|
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - ">="
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '0'
|
|
173
|
+
type: :development
|
|
174
|
+
prerelease: false
|
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0'
|
|
180
|
+
description: 'Fleet pipeline planning: decompose work items into implementation plans'
|
|
181
|
+
email:
|
|
182
|
+
- matthewdiverson@gmail.com
|
|
183
|
+
executables: []
|
|
184
|
+
extensions: []
|
|
185
|
+
extra_rdoc_files: []
|
|
186
|
+
files:
|
|
187
|
+
- ".github/workflows/ci.yml"
|
|
188
|
+
- ".gitignore"
|
|
189
|
+
- ".rspec"
|
|
190
|
+
- ".rspec_status"
|
|
191
|
+
- ".rubocop.yml"
|
|
192
|
+
- CLAUDE.md
|
|
193
|
+
- Gemfile
|
|
194
|
+
- lex-planner.gemspec
|
|
195
|
+
- lib/legion/extensions/planner.rb
|
|
196
|
+
- lib/legion/extensions/planner/actors/planner.rb
|
|
197
|
+
- lib/legion/extensions/planner/helpers/context_gatherer.rb
|
|
198
|
+
- lib/legion/extensions/planner/helpers/plan_schema.rb
|
|
199
|
+
- lib/legion/extensions/planner/helpers/spec_parser.rb
|
|
200
|
+
- lib/legion/extensions/planner/runners/planner.rb
|
|
201
|
+
- lib/legion/extensions/planner/transport/exchanges/planner.rb
|
|
202
|
+
- lib/legion/extensions/planner/transport/queues/planner.rb
|
|
203
|
+
- lib/legion/extensions/planner/version.rb
|
|
204
|
+
homepage: https://github.com/LegionIO/lex-planner
|
|
205
|
+
licenses:
|
|
206
|
+
- MIT
|
|
207
|
+
metadata:
|
|
208
|
+
homepage_uri: https://github.com/LegionIO/lex-planner
|
|
209
|
+
source_code_uri: https://github.com/LegionIO/lex-planner
|
|
210
|
+
changelog_uri: https://github.com/LegionIO/lex-planner
|
|
211
|
+
documentation_uri: https://github.com/LegionIO/lex-planner
|
|
212
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-planner/issues
|
|
213
|
+
rubygems_mfa_required: 'true'
|
|
214
|
+
rdoc_options: []
|
|
215
|
+
require_paths:
|
|
216
|
+
- lib
|
|
217
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
218
|
+
requirements:
|
|
219
|
+
- - ">="
|
|
220
|
+
- !ruby/object:Gem::Version
|
|
221
|
+
version: '3.4'
|
|
222
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
223
|
+
requirements:
|
|
224
|
+
- - ">="
|
|
225
|
+
- !ruby/object:Gem::Version
|
|
226
|
+
version: '0'
|
|
227
|
+
requirements: []
|
|
228
|
+
rubygems_version: 3.6.9
|
|
229
|
+
specification_version: 4
|
|
230
|
+
summary: Legion::Extensions::Planner
|
|
231
|
+
test_files: []
|