From 4406b5bb7f0ddc92cce151a8964b09839a03e65d Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Mon, 6 Apr 2026 10:49:01 -0500 Subject: [PATCH 1/2] Expose plan types to agents via /agent-instructions and accept plan_type by name - Add Plan Types section to agent instructions listing available types - Accept 'plan_type' param (by name) on plan creation instead of UUID - Return clear error with available types when an unknown plan_type is given - Update coplan skill docs to document plan_type param - Add request specs for agent instructions endpoint Amp-Thread-ID: https://ampcode.com/threads/T-019d6373-a857-75b8-b976-dc9a989c53e0 Co-authored-by: Amp --- .../coplan/agent_instructions_controller.rb | 1 + .../coplan/api/v1/plans_controller.rb | 14 ++++-- .../coplan/agent_instructions/show.text.erb | 21 ++++++++- spec/requests/agent_instructions_spec.rb | 44 +++++++++++++++++++ spec/requests/api/v1/plans_spec.rb | 18 +++++--- 5 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 spec/requests/agent_instructions_spec.rb diff --git a/engine/app/controllers/coplan/agent_instructions_controller.rb b/engine/app/controllers/coplan/agent_instructions_controller.rb index 39d467a..3ec0d73 100644 --- a/engine/app/controllers/coplan/agent_instructions_controller.rb +++ b/engine/app/controllers/coplan/agent_instructions_controller.rb @@ -6,6 +6,7 @@ def show @auth_instructions = CoPlan.configuration.agent_auth_instructions @curl = CoPlan.configuration.agent_curl_prefix @base = request.base_url + @plan_types = PlanType.order(:name) render layout: false, content_type: "text/markdown", formats: [:text] end end diff --git a/engine/app/controllers/coplan/api/v1/plans_controller.rb b/engine/app/controllers/coplan/api/v1/plans_controller.rb index 3b4c67e..fe472f7 100644 --- a/engine/app/controllers/coplan/api/v1/plans_controller.rb +++ b/engine/app/controllers/coplan/api/v1/plans_controller.rb @@ -23,11 +23,21 @@ def show end def create + if params[:plan_type].present? + plan_type = PlanType.find_by(name: params[:plan_type]) + unless plan_type + available = PlanType.order(:name).pluck(:name) + message = "Unknown plan_type \"#{params[:plan_type]}\"." + message += " Available types: #{available.map { |n| "\"#{n}\"" }.join(", ")}." if available.any? + return render json: { error: message }, status: :unprocessable_content + end + end + plan = Plans::Create.call( title: params[:title], content: params[:content] || "", user: current_user, - plan_type_id: params[:plan_type_id].presence + plan_type_id: plan_type&.id ) render json: plan_json(plan).merge( current_content: plan.current_content, @@ -35,8 +45,6 @@ def create ), status: :created rescue ActiveRecord::RecordInvalid => e render json: { error: e.message }, status: :unprocessable_content - rescue ActiveRecord::InvalidForeignKey - render json: { error: "Invalid plan_type_id" }, status: :unprocessable_content end def update diff --git a/engine/app/views/coplan/agent_instructions/show.text.erb b/engine/app/views/coplan/agent_instructions/show.text.erb index e90123b..78326a9 100644 --- a/engine/app/views/coplan/agent_instructions/show.text.erb +++ b/engine/app/views/coplan/agent_instructions/show.text.erb @@ -29,10 +29,12 @@ Returns: `id`, `title`, `status`, `current_content` (markdown), `current_revisio ```bash <%= @curl %> -X POST \ -H "Content-Type: application/json" \ - -d '{"title": "My Plan", "content": "# My Plan\n\nContent here."}' \ + -d '{"title": "My Plan", "content": "# My Plan\n\nContent here.", "plan_type": "general"}' \ "<%= @base %>/api/v1/plans" | jq . ``` +Optional fields: `plan_type` (string) — the name of a plan type to use. See [Plan Types](#plan-types) below. + ### Update Plan Update plan metadata (title, status, tags). Only fields included in the request body are changed. @@ -80,6 +82,23 @@ Plans move through a lifecycle. **Keep the status current** — update it as the When you create a plan, it starts as `brainstorm`. Promote it to `considering` when it's ready for review. Move to `developing` when implementation begins. Don't leave plans in stale states. +### Plan Types + +Plan types provide templates and default tags for common plan categories. When creating a plan, pass `plan_type` to use a type's template as the initial content. +<% if @plan_types.any? %> + +**Available plan types:** + +| Name | Description | +|------|-------------| +<% @plan_types.each do |pt| %> +| `<%= pt.name %>` | <%= pt.description.present? ? pt.description : "—" %> | +<% end %> +<% else %> + +No plan types are currently configured. +<% end %> + ### Get Versions ```bash diff --git a/spec/requests/agent_instructions_spec.rb b/spec/requests/agent_instructions_spec.rb new file mode 100644 index 0000000..dc02d40 --- /dev/null +++ b/spec/requests/agent_instructions_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe "Agent Instructions", type: :request do + describe "GET /agent-instructions" do + it "returns markdown content" do + get agent_instructions_path + expect(response).to have_http_status(:success) + expect(response.content_type).to include("text/markdown") + expect(response.body).to include("# CoPlan API") + end + + it "includes plan types when they exist" do + create(:plan_type, name: "Design Doc", description: "For design documents") + + get agent_instructions_path + + expect(response.body).to include("### Plan Types") + expect(response.body).to include("Design Doc") + expect(response.body).to include("For design documents") + end + + it "shows message when no plan types are configured" do + get agent_instructions_path + + expect(response.body).to include("### Plan Types") + expect(response.body).to include("No plan types are currently configured") + end + + it "lists multiple plan types sorted by name" do + create(:plan_type, name: "RFC") + create(:plan_type, name: "Design Doc") + + get agent_instructions_path + + body = response.body + expect(body.index("Design Doc")).to be < body.index("RFC") + end + + it "documents plan_type in create plan section" do + get agent_instructions_path + expect(response.body).to include('"plan_type"') + end + end +end diff --git a/spec/requests/api/v1/plans_spec.rb b/spec/requests/api/v1/plans_spec.rb index 561ef9c..f0d1566 100644 --- a/spec/requests/api/v1/plans_spec.rb +++ b/spec/requests/api/v1/plans_spec.rb @@ -65,20 +65,24 @@ expect(body["current_revision"]).to eq(1) end - it "create with plan_type_id" do - plan_type = create(:plan_type) - post api_v1_plans_path, params: { title: "Typed Plan", content: "# Typed", plan_type_id: plan_type.id }, headers: headers, as: :json + it "create with plan_type by name" do + plan_type = create(:plan_type, name: "design-doc") + post api_v1_plans_path, params: { title: "Typed Plan", content: "# Typed", plan_type: "design-doc" }, headers: headers, as: :json expect(response).to have_http_status(:created) body = JSON.parse(response.body) expect(body["plan_type_id"]).to eq(plan_type.id) - expect(body["plan_type_name"]).to eq(plan_type.name) + expect(body["plan_type_name"]).to eq("design-doc") end - it "create with invalid plan_type_id returns 422" do - post api_v1_plans_path, params: { title: "Bad Type", plan_type_id: "nonexistent-id" }, headers: headers, as: :json + it "create with unknown plan_type returns 422 with available types" do + create(:plan_type, name: "design-doc") + create(:plan_type, name: "rfc") + post api_v1_plans_path, params: { title: "Bad Type", plan_type: "nope" }, headers: headers, as: :json expect(response).to have_http_status(:unprocessable_content) body = JSON.parse(response.body) - expect(body["error"]).to include("plan_type_id") + expect(body["error"]).to include("nope") + expect(body["error"]).to include("design-doc") + expect(body["error"]).to include("rfc") end it "create without title fails" do From 214b5808284c7afe5dee151682b52d1bd46ca8c2 Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Mon, 6 Apr 2026 10:54:23 -0500 Subject: [PATCH 2/2] Fix misleading template_content claim in plan type docs The docs said plan_type uses template content as initial content, but Plans::Create doesn't actually do that. Updated to accurately describe what plan_type does: categorizes the plan and applies default tags. Amp-Thread-ID: https://ampcode.com/threads/T-019d6373-a857-75b8-b976-dc9a989c53e0 Co-authored-by: Amp --- engine/app/views/coplan/agent_instructions/show.text.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/app/views/coplan/agent_instructions/show.text.erb b/engine/app/views/coplan/agent_instructions/show.text.erb index 78326a9..e8201c8 100644 --- a/engine/app/views/coplan/agent_instructions/show.text.erb +++ b/engine/app/views/coplan/agent_instructions/show.text.erb @@ -84,7 +84,7 @@ When you create a plan, it starts as `brainstorm`. Promote it to `considering` w ### Plan Types -Plan types provide templates and default tags for common plan categories. When creating a plan, pass `plan_type` to use a type's template as the initial content. +Plan types categorize plans and provide default tags. When creating a plan, pass `plan_type` to associate it with a type. <% if @plan_types.any? %> **Available plan types:**