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 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,11 @@
1
+ inherit_gem:
2
+ rubocop-legion: config/lex.yml
3
+
4
+ Legion/Extension/RunnerReturnHash:
5
+ Enabled: false
6
+
7
+ Legion/HelperMigration/LoggingGuard:
8
+ Enabled: false
9
+
10
+ Style/ModuleFunction:
11
+ Enabled: false
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
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Planner
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ 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: []