Class: RubynCode::Megaplan::PlanProposer
- Inherits:
-
Object
- Object
- RubynCode::Megaplan::PlanProposer
- Defined in:
- lib/rubyn_code/megaplan/plan_proposer.rb
Overview
Proposes a multi-phase megaplan for a feature description.
Asks the LLM to produce a JSON payload that matches the extension’s ‘plan_proposal` shape — one folder per phase, three documents per phase, vertical-slice ordering. The handler validates the response and returns it to the IDE.
The LLM call is the slow part (~5-30s); callers should run this off the main JSON-RPC thread.
Defined Under Namespace
Classes: InvalidProposalError
Constant Summary collapse
- MAX_PHASES =
12- DEFAULT_SYSTEM_PROMPT =
<<~PROMPT.freeze You are a senior Ruby/Rails architect breaking a feature request into a megaplan. A megaplan is a multi-phase development plan where each phase is a VERTICAL SLICE that can ship independently. Trunk works at every phase boundary. No "scaffolding first, behavior later" — every phase delivers a thin, end-to-end working increment. Output a single JSON object with this exact shape: { "slug": "kebab-case-feature-slug", "feature": "Short feature description", "phases": [ { "number": 1, "slug": "kebab-case-phase-slug", "name": "Human-readable phase name", "summary": "One-sentence summary of what this phase ships", "requirements_md": "# Phase 1 — <name>: Requirements\\n\\n...", "design_md": "# Phase 1 — <name>: Design\\n\\n...", "tasks_md": "# Phase 1 — <name>: Tasks\\n\\n## [ ] 1. ...\\n\\n- [ ] 1.1 ..." } ] } Constraints: - 1 to 12 phases. Smaller, sharper phases beat fewer mega-phases. - Each phase must be a vertical slice. - tasks_md is a checklist with `[ ]` boxes (megaplan convention). - Every phase needs requirements_md, design_md, tasks_md. - Return ONLY the JSON. No markdown fences. No commentary. PROMPT
Instance Method Summary collapse
-
#initialize(llm_client: nil, system_prompt: nil, max_phases: MAX_PHASES) ⇒ PlanProposer
constructor
A new instance of PlanProposer.
-
#propose(feature) ⇒ Hash
Payload with ‘slug`, `feature`, `phases`.
-
#validate!(payload, _feature = nil) ⇒ Object
Validate a parsed plan_proposal Hash.
Constructor Details
#initialize(llm_client: nil, system_prompt: nil, max_phases: MAX_PHASES) ⇒ PlanProposer
Returns a new instance of PlanProposer.
55 56 57 58 59 |
# File 'lib/rubyn_code/megaplan/plan_proposer.rb', line 55 def initialize(llm_client: nil, system_prompt: nil, max_phases: MAX_PHASES) @llm_client = llm_client || LLM::Client.new @system_prompt = system_prompt || DEFAULT_SYSTEM_PROMPT @max_phases = max_phases end |
Instance Method Details
#propose(feature) ⇒ Hash
Returns payload with ‘slug`, `feature`, `phases`.
64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/rubyn_code/megaplan/plan_proposer.rb', line 64 def propose(feature) raise ArgumentError, 'feature is required' if feature.nil? || feature.strip.empty? response = @llm_client.chat( messages: [{ role: 'user', content: feature_prompt(feature) }], system: @system_prompt ) text = extract_text(response) payload = parse_payload(text) validate!(payload, feature) normalize(payload, feature) end |
#validate!(payload, _feature = nil) ⇒ Object
Validate a parsed plan_proposal Hash. Public so the interview path (which produces the same shape via a different LLM workflow) can reuse the rule set without reaching into a private method.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/rubyn_code/megaplan/plan_proposer.rb', line 81 def validate!(payload, _feature = nil) raise InvalidProposalError, 'payload is not an object' unless payload.is_a?(Hash) phases = payload['phases'] raise InvalidProposalError, 'phases must be an array' unless phases.is_a?(Array) raise InvalidProposalError, 'phases is empty' if phases.empty? raise InvalidProposalError, "too many phases (max #{@max_phases})" if phases.size > @max_phases phases.each_with_index do |phase, idx| %w[name summary requirements_md design_md tasks_md].each do |key| next unless phase[key].nil? || phase[key].to_s.strip.empty? raise InvalidProposalError, "phase #{idx + 1} missing #{key}" end end end |