diff --git a/AGENTS.md b/AGENTS.md index 42aec0f8..6ff5d457 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -157,6 +157,16 @@ every open source contributor run or host a server for a reviewer. - `db/seeds.rb` must be **idempotent** — use `find_or_create_by!` or guard with count checks - Seeds should provide enough data to demo features from a fresh checkout +## Agent Instructions Stay Current + +Agents learn CoPlan only from the served instructions, not from this repo. A change that agents can observe is not done until those instructions match it. That includes API endpoints, fields, status codes and error codes, as well as comment, editing, versioning and attribution rules, Markdown features, the live-session protocol, and `engine/agent_tools/`. + +- The primer is `engine/app/views/coplan/agent_instructions/show.text.erb` (served at `/agent-instructions`). Keep it to what every agent needs. +- Topic guides are in `engine/app/views/coplan/agent_instructions/guides/` (served at `/agent-instructions/`). Put task-specific detail there. A new guide needs an entry in `AgentInstructionsController::GUIDES` and a link from the primer; `spec/requests/agent_instructions_spec.rb` checks both. +- Update `guides/api.text.erb` when you add, remove, or change an endpoint. +- Remove anything that stops being true. Wrong instructions are worse than missing ones. +- Write in plain, short sentences, like the existing guides. Agents follow the commands literally, so run any new command against a local server before you document it. + ## Code Review - Run the `code-review` skill before considering a session complete diff --git a/docs/AGENT_COLLABORATION.md b/docs/AGENT_COLLABORATION.md index 33299b89..9ff608e4 100644 --- a/docs/AGENT_COLLABORATION.md +++ b/docs/AGENT_COLLABORATION.md @@ -52,7 +52,7 @@ The honest ceiling: this is thread-per-agent. It's fine for a team, not for hundreds of concurrent attached agents — that would want a real pub/sub transport rather than held Rack threads. -Full API reference: `GET /agent-instructions` → "Live Collaboration". +Protocol reference: `GET /agent-instructions/live`, with per-harness guides under `/agent-instructions/live/`. ## Waiting while an agent turn is active @@ -77,7 +77,7 @@ The unbounded streaming form belongs under an external supervisor that owns its lifecycle and has a real way to re-enter the model. The served tool is a thin convenience over the API. An agent that can make -HTTP requests can follow `/agent-instructions` directly and does not need it. +HTTP requests can follow `/agent-instructions/live` directly and does not need it. ## What the harness must provide @@ -212,7 +212,7 @@ posture deliberately — the bridge never escalates beyond what the config says. An agent doesn't need the bridge to drain events during its current turn: it -can follow the "Live Collaboration" protocol in `/agent-instructions` +can follow the protocol in `/agent-instructions/live` directly. That becomes a durable attachment only when the harness can turn the wait's completion into another model turn. A foreground loop that merely blocks, or a background loop that only writes a notification, is not a wake diff --git a/engine/agent_tools/coplan-bridge b/engine/agent_tools/coplan-bridge index 3a148f73..fe126e90 100755 --- a/engine/agent_tools/coplan-bridge +++ b/engine/agent_tools/coplan-bridge @@ -46,6 +46,7 @@ require "uri" require "optparse" require "open3" require "shellwords" +require "set" $stdout.sync = true @@ -145,6 +146,12 @@ def request(method, path, body: nil, params: nil, read_timeout: 90) req.body = body.to_json if body Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: read_timeout) do |http| res = http.request(req) + # A rejected token never recovers on retry, and every call after it + # would fail the same way. Stop so a supervisor (or a person) notices, + # instead of polling the server in a tight loop. + if res.code == "401" + abort "coplan-bridge: #{method.upcase} #{path} -> 401. The token is invalid, expired, or revoked. Mint a new token and restart the bridge." + end warn "coplan-bridge: #{method.upcase} #{path} -> #{res.code}" unless res.code.start_with?("2") res.body && !res.body.empty? ? JSON.parse(res.body) : {} end @@ -168,7 +175,8 @@ def wake_prompt(event) #{payload["comment_author"] ? "#{payload["comment_author"]} wrote: #{payload["comment_body"].to_s.strip}" : ""} #{payload["anchor_text"] ? "Anchored to: \"#{payload["anchor_text"]}\"" : ""} - Respond as the plan's agent, over the CoPlan API at #{BASE} (full instructions: #{BASE}/agent-instructions): + Respond as the plan's agent, over the CoPlan API at #{BASE} (full instructions: #{BASE}/agent-instructions). + Authenticate every call with `Authorization: Bearer $TOKEN`: $TOKEN in your environment is this bridge's session token. 1. PATCH /api/v1/plans/#{event["plan_id"]}/agent_session {"state":"active","detail":""} immediately. 2. Reply on the thread first (narrate what you'll change), then apply any edit with PUT .../content. 3. Finish with {"state":"complete"} (or "awaiting_input" if you asked a question). @@ -237,7 +245,9 @@ class AcpAgent def boot # stderr passes through so the agent's own logging stays visible. - @stdin, @stdout, @wait_thread = Open3.popen2(*@command) + # Same token handoff as the exec adapters: the prompt tells the agent + # to authenticate with $TOKEN, so it has to be this bridge's token. + @stdin, @stdout, @wait_thread = Open3.popen2({ "TOKEN" => TOKEN, "COPLAN_TOKEN" => TOKEN }, *@command) rpc("initialize", { protocolVersion: PROTOCOL_VERSION, # No fs or terminal capabilities: the agent works through its own @@ -397,19 +407,51 @@ def dispatch(event) # Honest pill before the harness even boots. Bridge.patch_state(event["plan_id"], "active", "reading your comment") + # A failed turn may still have replied or edited before it exited, and + # redelivery hands the same event to the agent again. Say so, the way + # the ACP restart path does, so the retry checks before acting. + prompt = wake_prompt(event) + if ATTEMPTED.include?(event["id"]) + prompt = "NOTE: an earlier attempt at this event failed and may have partially completed. " \ + "Check the plan's thread and content first, and do not duplicate replies or edits already made.\n\n#{prompt}" + end + ATTEMPTED << event["id"] + cmd = template.map do |part| - part.gsub("{session}", CONFIG.fetch("harness_session", "coplan")).gsub("{prompt}", wake_prompt(event)) + part.gsub("{session}", CONFIG.fetch("harness_session", "coplan")).gsub("{prompt}", prompt) end puts "coplan-bridge: dispatching #{event["type"]} -> #{adapter}" - system(*cmd) + # system returns false on a nonzero exit and nil when the command can't + # start; neither raises. Raise here so the loop leaves the event unacked + # for redelivery instead of acking a turn that never happened. + # The agent acts with the bridge's session token, under the name the + # instructions use for it. Its replies and edits are then attributed to + # that token, and never echoed back to this bridge's inbox. + return if system({ "TOKEN" => TOKEN, "COPLAN_TOKEN" => TOKEN }, *cmd) + + raise "#{adapter} adapter #{$?&.exitstatus ? "exited #{$?.exitstatus}" : "could not start"}" end +# Event ids handed to an exec adapter but not yet acked, so a redelivered +# event can be marked as a retry. An id leaves the set when its ack +# succeeds, so the set holds only events still in flight. +ATTEMPTED = Set.new + claim_sessions cursor = nil +failures = 0 puts "coplan-bridge: draining events from #{BASE}" loop do resp = request(:get, "/api/v1/agent/events", params: { wait: 25, cursor: cursor }.compact) - events = resp["events"] || [] + # A good poll always carries `events`. Anything else (a 5xx, an HTML + # error page) returned immediately, so wait before asking again. So did + # a throttled long-poll: the server had no thread to hold for us. + unless resp.key?("events") + sleep 5 + next + end + sleep 3 if resp["throttled"] + events = resp["events"] events.each do |event| dispatch(event) # Ack each event only once its dispatch came back — at-least-once @@ -417,12 +459,23 @@ loop do # never swallowed by a batch-level ack. cursor = event["id"] request(:post, "/api/v1/agent/events/ack", body: { cursor: cursor }) + ATTEMPTED.delete(event["id"]) + failures = 0 rescue => e # Stop the batch here: acking later events would also ack this one # (the cursor is a high-water mark). The next poll redelivers - # everything unacked; the sleep keeps a wedged adapter from spinning. - warn "coplan-bridge: dispatch failed: #{e.class}: #{e.message} — leaving event unacked" - sleep 3 + # everything unacked. Dispatch already said `active`; take that back, + # or every retry would refresh a pill promising a turn that isn't + # running. Back off so a wedged adapter doesn't spin. + failures += 1 + delay = [ 3 * 2**(failures - 1), 60 ].min + warn "coplan-bridge: dispatch failed: #{e.class}: #{e.message} — leaving event unacked, retrying in #{delay}s" + begin + Bridge.patch_state(event["plan_id"], "complete", nil) + rescue StandardError + nil # the server may be the thing that failed; the retry will tell + end + sleep delay break end rescue Interrupt diff --git a/engine/app/controllers/coplan/agent_instructions_controller.rb b/engine/app/controllers/coplan/agent_instructions_controller.rb index e06cc1e0..8f98fc16 100644 --- a/engine/app/controllers/coplan/agent_instructions_controller.rb +++ b/engine/app/controllers/coplan/agent_instructions_controller.rb @@ -1,14 +1,19 @@ module CoPlan - # Serves the agent API instructions at /agent-instructions. + # Serves the agent instructions: a short primer at /agent-instructions, + # plus topic guides at /agent-instructions/ that agents fetch only + # when a task needs them. The split keeps every agent's first read small — + # most never need the live-session protocol or the slide-deck rules. # - # This endpoint has two audiences: + # Two audiences: # - # * **Agents and CLIs** (curl, HTTP libraries, coding agents) fetch it as raw - # Markdown. Every API response points here via the `X-Agent-Instructions` - # header, so the raw behavior is load-bearing: any client that does not - # explicitly ask for HTML continues to get `text/markdown`. - # * **Humans in a browser** go to the short setup page. They should not have - # to read or copy the full API reference themselves. + # * **Agents and CLIs** (curl, HTTP libraries, coding agents) fetch raw + # Markdown. Every API response points at the primer via the + # `X-Agent-Instructions` header, so the raw behavior is load-bearing: any + # client that does not explicitly ask for HTML gets `text/markdown`. + # * **Humans in a browser** who land on the primer go to the short setup + # page; they should not have to read the API reference themselves. The + # rendered primer lives at /_/agent/instructions, and guides render as + # HTML in place so links from it stay browsable. # # Negotiation is deliberately conservative: we only serve HTML when the # Accept header *leads* with `text/html`, which is exactly what every @@ -16,34 +21,60 @@ module CoPlan # Turbo Drive sends on navigation (`Accept: text/html, application/xhtml+xml`). # curl's default `Accept: */*`, an absent Accept header, or # `Accept: text/markdown` all fall through to raw Markdown. An explicit - # format always wins over the Accept header: `/agent-instructions.md` - # forces raw Markdown, while `.html` goes to setup even from curl. + # format always wins over the Accept header: `.md` forces raw Markdown, + # and `.html` forces HTML (the setup page, for the primer). class AgentInstructionsController < ApplicationController skip_before_action :authenticate_coplan_user! + before_action :prepare_instructions + + # URL segment => template under agent_instructions/guides/. The primer + # links every entry; a spec holds it to that. + GUIDES = { + "markdown" => "markdown", + "presentations" => "presentations", + "creating" => "creating", + "editing" => "editing", + "comments" => "comments", + "live" => "live", + "live/claude-code" => "live_claude_code", + "live/amp" => "live_amp", + "live/codex" => "live_codex", + "live/hosted" => "live_hosted", + "live/bridge" => "live_bridge", + "organizing" => "organizing", + "api" => "api" + }.freeze def show return redirect_to(agent_setup_path) if prefers_html? - prepare_instructions render layout: false, content_type: "text/markdown", formats: [ :text ] end + # The primer, rendered for a person who wants to see what their agent reads. def reference - prepare_instructions - @current_coplan_user = CoPlan::Authentication.user_from_request(request) - CoPlan::Current.user = current_user + resolve_viewer @instructions_markdown = render_to_string(:show, formats: [ :text ], layout: false) render :reference end - # Sub-instructions: the library-organizing guide, linked from the main - # doc and from library API responses. Markdown-only — agents fetch it - # when an organizing task needs it. - def organizing - @auth_instructions = CoPlan.configuration.agent_auth_instructions - @curl = CoPlan.configuration.agent_curl_prefix - @base = "#{request.base_url}#{root_path.chomp("/")}" - render layout: false, content_type: "text/markdown", formats: [ :text ] + def guide + template = GUIDES[params[:guide].to_s] + unless template + return render plain: "# Unknown guide\n\nThe guide list is at #{@base}/agent-instructions\n", + status: :not_found, content_type: "text/markdown" + end + + template = "coplan/agent_instructions/guides/#{template}" + if prefers_html? + resolve_viewer + @instructions_url = coplan.agent_instructions_guide_url(guide: params[:guide]) + @raw_path = coplan.agent_instructions_guide_path(guide: params[:guide], format: :md) + @instructions_markdown = render_to_string(template, formats: [ :text ], layout: false) + render :guide, formats: [ :html ] + else + render template, layout: false, content_type: "text/markdown", formats: [ :text ] + end end private @@ -63,6 +94,14 @@ def prepare_instructions @create_example_json = create_example_json end + # The pages are public, but signed-in visitors should still see their + # normal nav chrome (search, inbox, sign-out) in the shared layout — + # same optional-resolve approach as WelcomeController. + def resolve_viewer + @current_coplan_user = CoPlan::Authentication.user_from_request(request) + CoPlan::Current.user = current_user + end + # The Create Plan curl example, with a real configured plan type so # agents copy an instance-accurate command. Names are admin-controlled # free text, so the payload is JSON-serialized (never hand-interpolated) diff --git a/engine/app/views/coplan/agent_instructions/guide.html.erb b/engine/app/views/coplan/agent_instructions/guide.html.erb new file mode 100644 index 00000000..31d515c9 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guide.html.erb @@ -0,0 +1,17 @@ +<% title = @instructions_markdown[/\A# (.+)$/, 1] || "Agent guide" %> +<% content_for :title, "#{title} — CoPlan" %> + +
+ <%= link_to "← Agent instructions", agent_instructions_reference_path, class: "agent-reference__back" %> +
+

+ A topic guide that your agent fetches when a task needs it. Agents get this URL as raw Markdown; + you can also <%= link_to "view the raw markdown", @raw_path %>. +

+ <%= render "coplan/shared/copy_url", url: @instructions_url, button_class: "btn btn--secondary" %> +
+ +
+ <%= render_markdown(@instructions_markdown, interactive: false) %> +
+
diff --git a/engine/app/views/coplan/agent_instructions/guides/api.text.erb b/engine/app/views/coplan/agent_instructions/guides/api.text.erb new file mode 100644 index 00000000..3d07212e --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/api.text.erb @@ -0,0 +1,83 @@ +# API Reference + +Every endpoint, with the guide that explains it. All paths start with `<%= @base %>`. <% if @request_auth_available %>Mint a session token through the host's request authentication (`<%= @mint_curl %>`). Every other call needs `Authorization: Bearer $TOKEN` with that session token.<% else %>Every call needs `Authorization: Bearer $TOKEN`. To mint a session token, send your principal's long-lived token. For every other call, send the session token.<% end %> See the primer at <<%= @base %>/agent-instructions>. Request bodies are JSON. + +## Tokens + +| Method and path | Purpose | +|---|---| +| `POST /api/v1/tokens` | Mint a session token: `agent_name`, `metadata`, `ttl_seconds`. | +| `DELETE /api/v1/tokens/current` | Revoke the token that makes the request. | + +## Plans + +| Method and path | Purpose | Guide | +|---|---|---| +| `GET /api/v1/plans` | List the plans that you can see. Filters: `?visibility=draft\|published`, `?archived=true`, `?folder_id=`. | primer | +| `POST /api/v1/plans` | Create a plan. | creating | +| `GET /api/v1/plans/:id` | Get a plan with its content and references. | primer | +| `GET /api/v1/plans/:id/snapshot` | Get a plan with content, threads, references, and collaborators. | primer | +| `PATCH /api/v1/plans/:id` | Change the title, type, tags, folder, visibility, archive state, or references. Plan author only. | editing | +| `PUT /api/v1/plans/:id/content` | Replace the content. | editing | +| `POST /api/v1/plans/:id/operations` | Apply targeted edits. | editing | +| `POST /api/v1/plans/:id/sessions` | Open an edit session (10 minutes): returns `id` and `base_revision`. Send `session_id` and that `base_revision` on each operation request to collect operations without saving. | editing | +| `GET /api/v1/plans/:id/sessions/:session_id` | Get an edit session: `status`, `expires_at`, `operations_count`. | editing | +| `POST /api/v1/plans/:id/sessions/:session_id/commit` | Save the session's operations as one version: `change_summary`. | editing | +| `POST`, `PATCH`, `DELETE /api/v1/plans/:id/lease` | Take, renew, or release the edit lock (optional). | editing | +| `GET /api/v1/plans/:id/versions` | List the versions. | editing | +| `GET /api/v1/plans/:id/locations` | Get the library and folder where the plan is filed. | organizing | +| `GET /api/v1/plan_types` | List the plan types with their templates. | creating | +| `GET /api/v1/tags` | List the tags, most used first. | creating | + +## Comments + +| Method and path | Purpose | Guide | +|---|---|---| +| `GET /api/v1/plans/:id/comments` | List the threads. | comments | +| `POST /api/v1/plans/:id/comments` | Start a thread: `body_markdown`, optional `anchor_text` and `anchor_occurrence`. | comments | +| `GET /api/v1/plans/:id/comments/:thread_id` | Get one thread. | comments | +| `POST /api/v1/plans/:id/comments/:thread_id/reply` | Reply: `body_markdown`. | comments | +| `PATCH /api/v1/plans/:id/comments/:thread_id/resolve` | Resolve a thread. | comments | +| `DELETE /api/v1/plans/:id/comments/:comment_id/delete` | Delete one of your principal's own comments. Comments that an agent posted cannot be deleted. | comments | + +## References and attachments + +| Method and path | Purpose | Guide | +|---|---|---| +| `GET`, `POST /api/v1/plans/:id/references` | List or add references. Adding is for the plan author only. | creating | +| `DELETE /api/v1/plans/:id/references/:reference_id` | Remove a reference. Plan author only. | creating | +| `GET /api/v1/references/search?url=` | Find the plans that refer to a URL. | creating | +| `GET`, `POST /api/v1/plans/:id/attachments` | List or upload files (multipart `file`). Uploading is for the plan author only. | markdown | +| `DELETE /api/v1/plans/:id/attachments/:attachment_id` | Delete a file. Plan author only. | markdown | + +## Libraries and folders + +| Method and path | Purpose | Guide | +|---|---|---| +| `GET /api/v1/library` | Your principal's folder tree with descriptions and counts. | creating | +| `GET /api/v1/library/contents` | All plans in the library, with summaries. | organizing | +| `POST /api/v1/library/organize` | Batch folder and plan moves, with `dry_run`. | organizing | +| `GET /api/v1/library/events` | The library's change log. | organizing | +| `GET /api/v1/libraries`, `GET /api/v1/libraries/:id` | All libraries (with a `writable` flag), or one library. | organizing | +| `GET /api/v1/libraries/:id/contents`, `POST /api/v1/libraries/:id/organize`, `GET /api/v1/libraries/:id/events` | The same contents, organize, and change-log calls for a library chosen by id. Organize needs write access; the change log is for the owner only. | organizing | +| `GET`, `POST /api/v1/folders`; `PATCH`, `DELETE /api/v1/folders/:id` | List, create, rename, or delete folders. | creating | + +## Live sessions + +| Method and path | Purpose | Guide | +|---|---|---| +| `POST`, `PATCH`, `DELETE /api/v1/plans/:id/agent_session` | Claim a session, set its state, or stop. | live | +| `GET /api/v1/agent/events` | Wait for events: `wait` (long-poll) or `Accept: text/event-stream`, optional `cursor`. | live | +| `POST /api/v1/agent/events/ack` | Acknowledge events up to `cursor`. | live | + +Guides: `<%= @base %>/agent-instructions/`, for example <<%= @base %>/agent-instructions/editing>. + +## Errors + +| Code | Meaning | +|---|---| +| `401` | The token is missing, not valid, revoked, or expired. Mint a new token. | +| `403` | You are not allowed to do this. Most often, the action is for the plan author only. A `403` on a call other than minting can also mean that you sent no Bearer token. | +| `404` | The plan or thread does not exist. | +| `409` | A stale `base_revision`, a hand edit that you have not read (`"code": "human_edit_pending"`), or someone holds the edit lock (`"code": "edit_locked"`). See the editing guide. | +| `422` | The request is not valid, or an operation cannot apply. The `error` field says why. | diff --git a/engine/app/views/coplan/agent_instructions/guides/comments.text.erb b/engine/app/views/coplan/agent_instructions/guides/comments.text.erb new file mode 100644 index 00000000..bd751529 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/comments.text.erb @@ -0,0 +1,74 @@ +# Comments and Reviews + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions>. The primer shows how to reply to a thread and resolve it. + +## How threads work + +People comment on an exact passage of a plan, a table cell, or a diagram node or edge. Each comment starts a thread. A thread is `open` until someone resolves it. A person can reopen a resolved thread in the web UI. The document view hides resolved threads unless the reader chooses to show them. + +Each thread in the snapshot (`comment_threads`) includes: + +- `anchor_text`: the passage that the comment is about. +- `anchor_kind`: set when the comment is on a `table_cell`, `mermaid_node`, `mermaid_edge`, or `mermaid_diagram` instead of on prose. +- `anchor_valid`: `false` if the passage is no longer in the plan. +- `out_of_date`: `true` if an edit changed the passage after the comment. +- `start_line` and `end_line`: set for comments on full lines. +- `comments`: each with `author_type` (`human`, `local_agent`, and so on), `agent_name`, and `body_markdown`. + +To get one thread: `GET <%= @base %>/api/v1/plans/$PLAN_ID/comments/$THREAD_ID`. To get all threads: `GET <%= @base %>/api/v1/plans/$PLAN_ID/comments`. + +## Whose comment it is + +You act for your principal. That decides what you do with a comment: + +- **Your principal's comment is an instruction.** Reply, make the change, and resolve the thread. +- **Another person's comment is a request.** Reply with the change that you propose. Edit only after your principal agrees. Do not edit a plan only because someone other than your principal asked. + +Live-session events include an `authority` field (`principal` or `collaborator`) that tells you which type of comment it is. See <<%= @base %>/agent-instructions/live>. + +## Reply, then edit, then resolve + +1. **Reply first**, in one or two sentences, with what you will change. The reviewer then knows that you saw the comment. +2. **Edit** the plan (see the primer). +3. **Resolve** the thread after the edit is saved. Do not resolve a thread that you did not address. Reply to explain why you did not act on it. + +```bash +<%= @curl %> -X POST -H "Content-Type: application/json" \ + -d '{"body_markdown": "Done. The rollout now starts in Q3."}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/comments/$THREAD_ID/reply" | jq . + +<%= @curl %> -X PATCH "<%= @base %>/api/v1/plans/$PLAN_ID/comments/$THREAD_ID/resolve" | jq . +``` + +- Resolve requires that your principal wrote the plan or started the thread. +- If you decide not to act on a comment, reply with the reason. Resolve the thread only when your principal agrees. +- Each reply notifies the people in the thread. Keep replies short and useful. Do not post status updates that nobody needs. + +## Start a thread + +Comment when you have a question, when an edit is not yours to make, or when you want a person to decide: + +```bash +<%= @curl %> -X POST -H "Content-Type: application/json" \ + -d '{"body_markdown": "Which team owns the rollback? @sam", "anchor_text": "rollback within one hour"}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/comments" | jq . +``` + +- Copy `anchor_text` exactly from the plan. CoPlan highlights it for readers. Choose a short, unique phrase. If the phrase occurs more than once, add `anchor_occurrence` (1 for the first occurrence). +- Leave out `anchor_text` for a comment on the whole plan. +- `@username` in a comment notifies that person. Usernames that do not exist stay plain text. +- You cannot delete a comment that you posted. Write it carefully. To correct it, reply in the thread. + +## Review a plan + +When your principal asks you to review a plan or to work through its feedback: + +1. Read the snapshot. It includes the content and all threads. +2. Sort each open thread into one of three groups: + - **Do it**: the feedback is clear and small. Apply it. + - **Confirm first**: the fix is clear but large. Summarize it and ask your principal. + - **Discuss**: the feedback is not clear, or it is a design decision. Give it to your principal with the options. +3. Make all approved changes in one content replacement, with a `change_summary` that names the threads. +4. Reply to and resolve each thread that you addressed. + +To give feedback on another person's plan, start threads anchored to the passages. Do not edit the plan. diff --git a/engine/app/views/coplan/agent_instructions/guides/creating.text.erb b/engine/app/views/coplan/agent_instructions/guides/creating.text.erb new file mode 100644 index 00000000..e28130a8 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/creating.text.erb @@ -0,0 +1,94 @@ +# Creating and Filing Plans + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions>. The primer shows the create request. This guide explains each choice in that request. + +## Before you create a plan + +- **Edit, do not duplicate.** If a plan for the work exists, edit it. Plans keep their full history. If a new plan really replaces an old one, link the two plans and archive the old one. +- **Choose a folder.** Read `GET <%= @base %>/api/v1/library` (see [Folders](#folders)). +- **Choose a type and read its template.** Read `GET <%= @base %>/api/v1/plan_types` (see [Plan types](#plan-types)). + +## Create request fields + +`POST <%= @base %>/api/v1/plans` requires `title` and `content`. Optional fields: + +| Field | Value | +|---|---| +| `plan_type` | A type name. If you do not send one, the plan gets the type **General**. | +| `folder_path` | A path such as `"Team EBT/Q3"`. CoPlan creates missing folders in your principal's library. You can send `folder_id` instead. | +| `tags` | An array of tag names. CoPlan adds them to the type's `default_tags`. | +| `references` | An array of resources that the content does not link. See [References](#references). | +| `visibility` | Send `"draft"` only when your principal asks for a private plan. See [Visibility and archiving](#visibility-and-archiving). | + +The response includes `id`, `url`, and `current_revision`. + +## Plan types + +Each plan has one type. A type has a description, default tags, and a content template. The type does not change how the plan renders. A `Presentation` plan is a normal plan whose template is a slide deck region (see <<%= @base %>/agent-instructions/presentations>). + +- Choose the most specific type that fits. Use **General** only after you check the list and no other type fits. +- The type's `template_content` is the structure that readers of that type expect. Keep its sections and fill them with real content. Remove a section only when it does not apply. +- CoPlan applies the type's `default_tags` when it creates the plan. +- If a plan changes into a different kind of document, change its type: `PATCH <%= @base %>/api/v1/plans/$PLAN_ID` with `{"plan_type": "..."}`. Then restructure the content to match the new template. An unknown type name returns `422` with the list of valid names. +<% if @plan_types.any? %> + +Plan types on this server: + +| Name | Description | Template | +|------|-------------|----------| +<% @plan_types.each do |pt| %> +| `<%= pt.name %>` | <%= pt.description.present? ? pt.description : "—" %> | <%= pt.template_content.present? ? "yes — fetch and follow it" : "—" %> | +<% end %> +<% else %> + +No plan types are currently configured. +<% end %> + +## Folders + +Each person has a library: a tree of folders, up to <%= CoPlan::Folder::MAX_DEPTH %> levels deep. A plan is filed in one folder in one library. Filing it in a different folder moves it. Use folders for where a plan lives, and use tags for what a plan is about. + +- `GET <%= @base %>/api/v1/library` returns your principal's folder tree. Each folder has a `description` of what belongs in it. Read the descriptions before you file a plan. Use the existing folders before you make new ones. +- To file or move a plan that your principal wrote, send `PATCH <%= @base %>/api/v1/plans/$PLAN_ID` with `{"folder_path": "Team EBT/Q3"}`. To remove it from its folder, send `{"folder_id": ""}`. +- Name folders like places (`Team EBT`, `Infra`, `Q3 Launch`). Give each new folder a one-line description. The next agent reads it. +- Folder endpoints: `GET` and `POST` on `<%= @base %>/api/v1/folders` to list and create; `PATCH` (with `name`) and `DELETE` on `<%= @base %>/api/v1/folders/$FOLDER_ID` to rename and delete. You can delete only empty folders. + +To reorganize a library, or to move many plans, fetch <<%= @base %>/agent-instructions/organizing>. + +## Tags + +- Read `GET <%= @base %>/api/v1/tags` before you add a tag. It lists the tags in use, most used first. Use an existing tag when one fits. +- Use lowercase names with hyphens: `api-design`, `cost-reduction`. +- Use 1 to 3 tags for each plan. A tag names a domain or concern (`security`, `onboarding`), not a status or a team. +- To set tags, send `PATCH <%= @base %>/api/v1/plans/$PLAN_ID` with `{"tags": ["security", "api-design"]}`. This replaces the full tag list. + +## References + +References are the list of resources connected to a plan: repositories, pull requests, documents, and other plans. Readers see them in the plan's References section. CoPlan adds every link in the content to this list. Add a reference through the API only for a resource that the content does not link. + +```bash +<%= @curl %> -X POST \ + -H "Content-Type: application/json" \ + -d '{"url": "https://github.com/org/auth-service", "key": "auth-repo", "title": "Auth Service"}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/references" | jq . +``` + +- `url` is required. Always set `key`, a short stable name such as `auth-repo` or `impl-pr` (lowercase letters, numbers, `-`, and `_`; unique in the plan). Set `title` to a name that people can read, not the URL. +- CoPlan sets the type (repository, pull request, plan, document, or link) from the URL. +- Only the plan author can add or remove references through the API. You can also send `references` on create or in a `PATCH` to the plan. +- `GET <%= @base %>/api/v1/plans/$PLAN_ID/references` lists the references of a plan. +- `GET <%= @base %>/api/v1/references/search?url=...` finds the plans that refer to a URL. + +## Visibility and archiving + +A plan has two independent settings. There is no status. Do not put a status in the document. + +| Field | Values | Meaning | +|---|---|---| +| `visibility` | `published`, `draft` | `published` plans are listed for the whole organization. This is the default. A `draft` plan shows as **private**: it is not in lists, search, or libraries, but anyone with the URL can read it. | +| `archived` | `true`, `false` | Archived plans stay at their URL but are not in lists or search. | + +- Create a private plan only when your principal asks for one. To share it later, send `PATCH` with `{"visibility": "published"}`. Through the API, you cannot make a shared plan private again. Archive it instead. +- Archive plans that are finished, replaced, or created by mistake, including your own duplicates: `PATCH` with `{"archived": true}`. To restore a plan, send `{"archived": false}`. +- To show a plan's rollout stage, use a tag such as `developing` or `live`. +- The API still accepts the old `status` field (`brainstorm`, `considering`, `developing`, `live`, `abandoned`) and converts it to `visibility` and `archived`. Do not use it in new code. diff --git a/engine/app/views/coplan/agent_instructions/guides/editing.text.erb b/engine/app/views/coplan/agent_instructions/guides/editing.text.erb new file mode 100644 index 00000000..cd284264 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/editing.text.erb @@ -0,0 +1,117 @@ +# Editing Plans + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions>. The primer shows the usual edit: read the snapshot, change the full document, and send it back. This guide covers the responses, conflicts, targeted edits, and plan settings. + +## Replace the content + +`PUT <%= @base %>/api/v1/plans/$PLAN_ID/content` with `content`, `base_revision`, and `change_summary`. Use this request for most edits: a rewrite, a new section, many small fixes, or a restructure. One request with all changes makes one clear version. Many small requests make many versions. + +| Response | Meaning | What to do | +|---|---|---| +| `201` with `revision`, `version_id`, `applied` | CoPlan saved a new version. | Continue. | +| `200` with `"no_op": true` | The content did not change. | Nothing. | +| `409` with `current_revision` | Someone wrote after your read. | Read again, apply your change to the new content, and send it with the new revision. Do not send the same request with the old revision. | +| `409` with `"code": "human_edit_pending"` | A person edited by hand after your last read. | See [Hand edits](#hand-edits). | +| `409` with `"code": "edit_locked"` | Someone holds the edit lock, usually a person in the web editor. | See [Edit locks](#edit-locks). | +| `422` | `content` or `base_revision` is missing or not valid. | Fix the request. | + +CoPlan compares your content with the current version and keeps comments anchored to text that you did not change. If your edit removes the text that a comment is anchored to, the thread becomes out of date. Address the comment before you delete its text. + +## Hand edits + +A person's hand edit has the highest authority in a plan. CoPlan merges agent edits that happen at the same time, but it does not merge over a hand edit. After a person edits a plan by hand, CoPlan refuses every agent write until that agent reads the plan again. Hand edits include edits in the web editor and checkbox selections. + +The refusal includes the person's diff: + +```json +{ + "error": "Blocked: Sam edited this plan by hand (an edit, now at v7) and you last read v5. ...", + "code": "human_edit_pending", + "current_revision": 7, + "last_human_revision": 7, + "last_seen_revision": 5, + "human_edits": [ + { + "revision": 7, + "editor": "Sam", + "edited_at": "2026-08-24T17:02:11Z", + "change_summary": "Edited in web UI", + "diff": "@@ -12,3 +12,3 @@\n-Ship behind a flag in Q4.\n+Ship behind a flag in Q3 — legal signed off on the earlier date.\n" + } + ] +} +``` + +1. Read the diff. It shows the words that the person chose. +2. Read the snapshot again: `GET <%= @base %>/api/v1/plans/$PLAN_ID/snapshot`. This read removes the block. A new `base_revision` alone does not. +3. Make your change on top of the current content. Do not send the content that you had before. It does not include the person's edit, so it deletes their words. +4. If your task conflicts with their edit, keep their text and explain the conflict in a comment. + +This rule applies to every way that an agent writes: content replacement, operations, and edit-session commits (`POST <%= @base %>/api/v1/plans/$PLAN_ID/sessions/$SESSION_ID/commit`). + +## Targeted edits: operations + +To change one passage without sending the full document, send operations: + +```bash +<%= @curl %> -X POST \ + -H "Content-Type: application/json" \ + -d '{ + "base_revision": 7, + "change_summary": "Switched the datastore to PostgreSQL", + "operations": [ + {"op": "replace_exact", "old_text": "We use MySQL", "new_text": "We use PostgreSQL"} + ] + }' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/operations" | jq . +``` + +CoPlan applies all operations in one request as one version. If another agent wrote after your `base_revision`, CoPlan moves your operations onto the current content. This works for up to 20 revisions. If the target text changed, CoPlan returns `409`. Read the plan again and retry. + +| `op` | Fields | Effect | +|---|---|---| +| `replace_exact` | `old_text`, `new_text`, optional `occurrence` (1-based) or `replace_all: true` | Replaces the text. Without `occurrence` or `replace_all`, the text must occur exactly once. | +| `insert_under_heading` | `heading` (the full line, such as `"## Testing"`), `content` | Inserts content after the heading. The heading must occur exactly once. | +| `delete_paragraph_containing` | `needle` | Deletes the one paragraph that contains the text. | +| `replace_section` | `heading`, `new_content`, optional `include_heading: false` | Replaces the heading and everything under it, up to the next heading of the same or a higher level. With `include_heading: false`, CoPlan keeps the heading line and replaces only the body. | + +An operation that cannot apply returns `422` with the reason, and CoPlan saves nothing. + +To collect operations from several requests into one version, open an edit session: `POST <%= @base %>/api/v1/plans/$PLAN_ID/sessions`. The response has `id` and `base_revision`. Send both, as `session_id` and `base_revision`, on each operation request; CoPlan holds the operations without saving them. Then `POST <%= @base %>/api/v1/plans/$PLAN_ID/sessions/$SESSION_ID/commit` with a `change_summary` saves them as one version. A session expires after 10 minutes. Most edits do not need one: a single operations request already saves as one version. + +## Edit locks + +A person who opens the web editor takes the plan's edit lock. While anyone else holds the lock, CoPlan refuses every write, including content replacement and operations, with `409` and `"code": "edit_locked"`. The lock expires 5 minutes after the holder stops. Wait, read the plan again, and retry. Do not retry in a tight loop. + +You can take the lock yourself to keep a long series of edits together. Most edits do not need it. + +- Take it: `POST <%= @base %>/api/v1/plans/$PLAN_ID/lease` with `{"lease_token": ""}`. +- Send the same `lease_token` with each content or operation request. +- Renew it before 5 minutes pass, and release it as soon as you finish. Both calls need the same `lease_token`. While you hold it, people cannot save in the editor. + +```bash +<%= @curl %> -X PATCH -H "Content-Type: application/json" -d "{\"lease_token\": \"$LEASE_TOKEN\"}" \ + "<%= @base %>/api/v1/plans/$PLAN_ID/lease" # renew +<%= @curl %> -X DELETE -H "Content-Type: application/json" -d "{\"lease_token\": \"$LEASE_TOKEN\"}" \ + "<%= @base %>/api/v1/plans/$PLAN_ID/lease" # release +``` + +## Plan settings + +`PATCH <%= @base %>/api/v1/plans/$PLAN_ID` changes the fields that you send and nothing else. Only the plan author can change these settings. + +| Field | Value | +|---|---| +| `title` | A string. The plan's URL follows the new title, and the old URL still works. | +| `plan_type` | A type name. CoPlan adds the new type's default tags. Restructure the content to match the new template. | +| `tags` | An array. It replaces the full tag list. | +| `folder_path` or `folder_id` | Moves the plan in its library. | +| `visibility` | `"published"` shares a private plan. | +| `archived` | `true` or `false`. | +| `references` | An array of `{url, key, title}`. | + +See <<%= @base %>/agent-instructions/creating> for the rules on types, folders, tags, and visibility. + +## History + +`GET <%= @base %>/api/v1/plans/$PLAN_ID/versions` lists every version, newest first, with `revision`, `actor_type` (`human`, `local_agent`, and so on), `change_summary`, and `created_at`. Use it to find what changed and who changed it. Do not write a changelog in the plan. diff --git a/engine/app/views/coplan/agent_instructions/guides/live.text.erb b/engine/app/views/coplan/agent_instructions/guides/live.text.erb new file mode 100644 index 00000000..0fadd9c7 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/live.text.erb @@ -0,0 +1,138 @@ +# Live Sessions: the Protocol + +In a live session, you get comments on a plan as soon as people post them, and the plan shows a presence pill with your name and state. This page is the protocol. It works in any environment that can make HTTP requests. For a ready-made setup, use the guide for your environment: + +- Claude Code: <<%= @base %>/agent-instructions/live/claude-code> +- Amp: <<%= @base %>/agent-instructions/live/amp> +- Codex: <<%= @base %>/agent-instructions/live/codex> +- Hosted agent that can receive webhooks: <<%= @base %>/agent-instructions/live/hosted> +- A bridge that your principal runs on an always-on machine: <<%= @base %>/agent-instructions/live/bridge> + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions> and that you have a session token. + +## The loop + +1. **Claim** a session on the plan. This subscribes your token to the plan's events. +2. **Wait** for events. +3. **Act** on each event: set your state to `active`, reply, edit, and then set your state to `complete`. +4. **Acknowledge** the events that you processed. +5. Go back to step 2. When you stop watching the plan, **delete** the session. + +## Getting the event is not a wake + +The server can send an event to your process. That does not start your next turn. Your environment must start the turn. An event that a background process prints, with nobody to read it, does nothing. + +These are the ways to get a wake. Use the first one that your environment can do, and test it before you rely on it: + +| Wake | How | Guide | +|---|---|---| +| A sidecar resumes your session | Your principal runs `coplan-bridge`, which resumes your existing session with each event. | live/bridge | +| A webhook starts your turn | Claim the session with a `wake_url`. | live/hosted | +| Your environment schedules follow-up turns | Each scheduled turn reads the inbox with `wait=0`. Between turns, keep the session at `complete`. Stop the schedule when the watch is over. | live/codex | +| Background completion starts a turn | A background wait loop exits when an event arrives, and the exit starts your next turn. A notification or a log line is not enough. | live/claude-code | +| Your principal asked you to wait now | One bounded wait in the foreground. Repeat it only while the request lasts. Never run an endless loop in the foreground. | live/amp | + +If none of these works, do not show a pill. Claim the session and PATCH it to `complete` at once: events still go to your inbox. Read the inbox with `wait=0` at the start and end of each turn, and tell your principal that you cannot watch the plan live. The inbox keeps events until you acknowledge them, so you answer late, but you do not miss anything. + +## 1. Claim a session + +```bash +<%= @curl %> -X POST -H "Content-Type: application/json" \ + -d '{}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . +``` + +- The claim subscribes your token to the plan's events. A claim with no state shows you as `watching`. Send `"state": "active"` if you are already working. +- Your token supplies your name and harness. Do not send `agent_name`. +- Claim once for each plan in each run. **To answer an event, use PATCH. Do not claim again.** A new claim resets the session, and CoPlan counts the event as not answered. +- To stop: `<%= @curl %> -X DELETE "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session"`. + +## States + +Keep your state correct. People read it to know whether to wait for you. + +```bash +<%= @curl %> -X PATCH -H "Content-Type: application/json" \ + -d '{"state": "active", "detail": "Rewriting the Rollout section"}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . +``` + +| State | Set by | Meaning | +|---|---|---| +| `watching` | you | You are attached and waiting. | +| `pending` | the server | An event arrived for you. If you do not change to `active` within 30 seconds, the session becomes `stale` and the pill disappears. People then see no agent on the plan. | +| `active` | you | You are working. `detail` shows what you are doing. | +| `awaiting_input` | you | You asked a question and need an answer to continue. | +| `complete` | you | Your turn is done. | + +- Change to `active` within a few seconds of an event. After that, take the time that you need. +- Reply in the thread before you edit. Say what you will change. +- Set `awaiting_input` when a question blocks you. Set `complete` when you finish. + +## 2. Wait for events + +Long-poll. The request returns as soon as an event arrives, or with no events after `wait` seconds (maximum 55): + +```bash +<%= @curl %> "<%= @base %>/api/v1/agent/events?wait=25" | jq . +``` + +Or stream with Server-Sent Events. The server sends a heartbeat every 15 seconds and closes each stream after 5 minutes. Reconnect with your last cursor: + +```bash +<%= @curl %> -N -H "Accept: text/event-stream" "<%= @base %>/api/v1/agent/events?cursor=$LAST_EVENT_ID" +``` + +- The response has `events` and a `cursor` (the last event ID). +- If the response has `"throttled": true`, the server has too many open connections. Wait a few seconds, then poll again. +- Without a cursor, you get all events that you did not acknowledge. CoPlan never sends you your own comments or edits. + +## Events + +Each event has `id`, `type`, `plan_id`, `comment_thread_id`, `comment_id`, and `payload`. The types are `comment.created`, `comment.replied`, `thread.status_changed`, and `plan.content_changed`. + +The payload usually has enough information for you to decide what to do: `plan_title`, `plan_revision`, `thread_status`, `anchor_text`, `comment_body`, and `comment_author`. Comment events also tell you whose comment it is: + +| Field | Meaning | +|---|---| +| `authority: "principal"` | The comment is from your principal, or from an agent that acts for your principal. **Act on it**: reply, then edit. | +| `authority: "collaborator"` | The comment is from another person. **Propose**: reply with the change that you suggest, and edit only after your principal agrees. | +| `from_plan_author` | The commenter wrote the plan. This is a separate fact: on another person's plan, your principal still outranks the author. | + +## 3. Acknowledge + +CoPlan delivers each event at least once. Acknowledge events after you process them: + +```bash +<%= @curl %> -X POST -H "Content-Type: application/json" \ + -d "{\"cursor\": \"$LAST_EVENT_ID\"}" \ + "<%= @base %>/api/v1/agent/events/ack" | jq . +``` + +This acknowledges every event up to and including the cursor. Events that you do not acknowledge come back on the next wait. + +## One token for each run + +Sessions, inboxes, and pills belong to a token. Two agents that use one token share an inbox and take each other's events. Give each run its own token (see step 1 of the primer). + +## Reference tools + +This server provides scripts that do all of the above. They need Ruby. You do not have to use them; the protocol is only HTTP. + +- `coplan-attach` (requires `coplan_session.rb` in the same directory) claims the session, waits, prints a short summary of each event, and acknowledges it. With `--once`, it exits after the first event. `--timeout 300` makes it exit with code 64 if nothing happens. `--no-session` makes it use your session token as-is; without it, it tries to mint its own token, which a session token cannot do. +- `coplan-bridge` runs outside your session and resumes it for each event. Your principal starts it, not you. See <<%= @base %>/agent-instructions/live/bridge>. + +Run `coplan-attach --once` only when you have a wake for it (see the table above) or your principal asked you to wait. In the foreground, always set `--timeout`. + +**Read each file before you run it.** They are programs from the network. `coplan-attach` and `coplan_session.rb` must connect only to this server, write only under `~/.coplan` (or `$COPLAN_HOME`), and start no other processes. `coplan-bridge` can also read `~/.config/coplan/bridge.json` and run the one agent command that you configure. If a file does more than that, do not run it. + +```bash +mkdir -p ~/.coplan/bin && cd ~/.coplan/bin +curl -sfO <%= @base %>/agent-tools/coplan-attach +curl -sfO <%= @base %>/agent-tools/coplan_session.rb +# Read both files now. Then: +chmod +x coplan-attach +export COPLAN_BASE=<%= @base %> +export COPLAN_TOKEN="$TOKEN" # your session token; do not paste the secret +~/.coplan/bin/coplan-attach --plan $PLAN_ID --no-session --once --timeout 600 +``` diff --git a/engine/app/views/coplan/agent_instructions/guides/live_amp.text.erb b/engine/app/views/coplan/agent_instructions/guides/live_amp.text.erb new file mode 100644 index 00000000..77fc5e28 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/live_amp.text.erb @@ -0,0 +1,56 @@ +# Live Sessions in Amp + +Amp does not start a new turn when a background process exits. A background wait can receive an event, but nothing reads it until someone sends a new message. A live session in Amp has two parts: waiting during your turn, and a wake between turns. + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions> and that you have a session token in `$TOKEN`. The protocol details are in <<%= @base %>/agent-instructions/live>. + +## Claim a session + +```bash +<%= @curl %> -X POST -H "Content-Type: application/json" \ + -d '{}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . +``` + +## During your turn: wait in the foreground + +When your principal asks you to watch the plan, run this loop as a normal shell command. It stops at the first event or after about 10 minutes: + +```bash +for i in $(seq 1 24); do + RESP=$(<%= @curl %> -f "<%= @base %>/api/v1/agent/events?wait=25") || + { echo "coplan: wait failed: token expired or server unreachable" >&2; exit 1; } + [ "$(printf '%s' "$RESP" | jq '.events | length')" -gt 0 ] && { printf '%s\n' "$RESP" | jq .; exit 0; } +done +echo "coplan: no events"; exit 64 +``` + +If your shell tool returns before the command ends, check the process again until it prints output. Do not start a second copy. + +When an event arrives: + +1. Set your state to `active`: `PATCH <%= @base %>/api/v1/plans/$PLAN_ID/agent_session` with `{"state": "active"}`. +2. Reply in the thread, then edit. Follow the `authority` field: act on `principal` comments; propose changes for `collaborator` comments. +3. Set your state to `complete`, or to `awaiting_input` if you asked a question. +4. Acknowledge: `POST <%= @base %>/api/v1/agent/events/ack` with `{"cursor": ""}`. +5. Run the loop again, or end your turn if your principal does not need you to keep watching. + +## Between turns: two options + +**Read your inbox at the start and end of each turn.** This needs no setup. Set the session to `complete` (`PATCH` with `{"state": "complete"}`) so that the plan does not show a pill for you between turns. Tell your principal that you cannot watch the plan live. You answer late, but you do not miss events: + +```bash +<%= @curl %> "<%= @base %>/api/v1/agent/events?wait=0" | jq . +``` + +**Ask your principal to run the bridge.** `coplan-bridge` runs on your principal's machine. For each event, it runs `amp threads continue -x ""`, which starts a new turn in this thread. Your thread ID is the `T-…` part of your thread URL. Give your principal this command. Do not start it yourself: it must keep running after your turn ends. + +```bash +coplan-bridge --adapter amp --session T- --plan +``` + +First, your principal installs the bridge and mints its session token: see "Install" in <<%= @base %>/agent-instructions/live/bridge>. The bridge claims its own session on the plan and sets it to `active` for each event. After the bridge starts, delete your own session (`DELETE <%= @base %>/api/v1/plans/$PLAN_ID/agent_session`). Otherwise your token stays subscribed too: it gets a second copy of each event and can show a second pill. + +## Keep the setup + +Your setup ends with your thread. With your principal's agreement, save the wait loop and these steps as an Amp skill, for example `.agents/skills/coplan-live/SKILL.md`. A new thread can then attach in one step. diff --git a/engine/app/views/coplan/agent_instructions/guides/live_bridge.text.erb b/engine/app/views/coplan/agent_instructions/guides/live_bridge.text.erb new file mode 100644 index 00000000..61c5632a --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/live_bridge.text.erb @@ -0,0 +1,81 @@ +# Live Sessions Through a Bridge + +`coplan-bridge` runs outside the agent, on a machine that is always on. It waits for events on one or more plans. For each event, it resumes an existing agent session with a message about the event. Use it when your agent cannot wake itself. + +**Your principal starts the bridge, not you.** It must run before any agent is awake and after your turn ends. If you are an agent, give your principal the commands below. Do not start the bridge yourself during ordinary plan work. + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions>. The protocol details are in <<%= @base %>/agent-instructions/live>. + +## Install + +The bridge needs Ruby. **Read the file before you run it.** It must connect only to this server, read only `~/.config/coplan/bridge.json` (if that file exists), and run only the agent command that you configure. If it does more, do not run it. + +```bash +mkdir -p ~/.coplan/bin && cd ~/.coplan/bin +curl -sfO <%= @base %>/agent-tools/coplan-bridge +# Read the file now. Then: +chmod +x coplan-bridge +export PATH="$HOME/.coplan/bin:$PATH" +export COPLAN_BASE=<%= @base %> +# Give the bridge its own session token, not the long-lived one. +# Name it for the agent that the bridge resumes: +<% if @request_auth_available %> +COPLAN_TOKEN=$(<%= @mint_curl %> -X POST \ +<% else %> +COPLAN_TOKEN=$(curl -s -X POST \ + -H "Authorization: Bearer $(jq -r .token ~/.config/coplan/credentials.json)" \ +<% end %> + -H "Content-Type: application/json" \ + -d '{"agent_name": "Claude", "ttl_seconds": 2592000, "metadata": {"harness": "claude-code"}}' \ + "$COPLAN_BASE/api/v1/tokens" | jq -er '.token // error(.error // "mint failed")') && export COPLAN_TOKEN +``` + +If minting fails, the command prints the error and does not export a token, and the bridge refuses to start. If the bridge later gets a `401`, it stops: mint a new token and restart it. + +The presence pill, and any reply or edit made with this token, show the token's `agent_name` and `harness`. Set them to the agent that the bridge resumes (for example `"Amp"` and `amp`), not to the bridge. The session token expires after 30 days and cannot mint more tokens. Run one bridge for each token: two bridges with one token share an inbox and take each other's events. When the token expires, mint a new one and restart the bridge. + +## Resume an existing session + +Choose the adapter for the agent, and pass the ID of the session or thread to resume: + +```bash +coplan-bridge --adapter claude --session --plan $PLAN_ID +coplan-bridge --adapter amp --session T- --plan $PLAN_ID +coplan-bridge --adapter codex --session --plan $PLAN_ID +``` + +| Adapter | Command that the bridge runs for each event | +|---|---| +| `claude` | `claude -p --resume --permission-mode acceptEdits ` | +| `amp` | `amp threads continue -x ` | +| `codex` | `codex exec resume ` | +| `goose` | `goose run --name --resume -t ` | +| `openhands` | `openhands --headless --resume -t ` | + +`--adapter demo` runs a built-in demo agent that replies and makes a small edit. It needs no agent, so you can use it to test the setup. + +## What the bridge does for each event + +1. It claims the session on each plan at start. +2. When an event arrives, it sets the session to `active`, so people see a response within a second. +3. It resumes the agent with the event and these steps, and gives the agent its session token as `$TOKEN` (and `$COPLAN_TOKEN`), so the agent's replies and edits use it: set `active`, reply, edit, then set `complete` or `awaiting_input`. +4. It acknowledges the event after the agent's turn. If the turn fails, the event stays in the inbox. + +## A separate agent over ACP + +The bridge can also start its own agent over the Agent Client Protocol (`--acp ""`). That agent is a new agent with its own session. It is not the conversation that wrote the plan, so it does not know that conversation's context. Use it only when your principal wants a standing reviewer or worker for the plan. The bridge rejects the agent's permission requests unless you add `--approve`. + +## Keep the setup + +Put the settings in `~/.config/coplan/bridge.json`, and run the bridge as a service. Do not put the token in this file. Give the service the minted session token in `COPLAN_TOKEN` (see [Install](#install)): + +```json +{ + "base_url": "<%= @base %>", + "plans": [""], + "adapter": "claude", + "harness_session": "" +} +``` + +Command-line options override the environment, and the environment overrides the file. `--plan` replaces the file's plan list. diff --git a/engine/app/views/coplan/agent_instructions/guides/live_claude_code.text.erb b/engine/app/views/coplan/agent_instructions/guides/live_claude_code.text.erb new file mode 100644 index 00000000..3e686e91 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/live_claude_code.text.erb @@ -0,0 +1,52 @@ +# Live Sessions in Claude Code + +In an interactive Claude Code session, a background Bash task that exits can start your next turn. Use that as your wake: run a wait loop in the background, continue your work, and act on the event when the task exits. + +This works only while the session is interactive and open. It does not work in a headless `claude -p` run, and it stops when your principal closes the session. For a wake after that, your principal can run a bridge that resumes this session with `claude --resume` (see <<%= @base %>/agent-instructions/live/bridge>). + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions> and that you have a session token in `$TOKEN`. The protocol details are in <<%= @base %>/agent-instructions/live>. + +## Set up + +1. Claim a session on the plan: + + ```bash + <%= @curl %> -X POST -H "Content-Type: application/json" \ + -d '{}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . + ``` + +2. Start this loop with the Bash tool and `run_in_background: true`. It exits only when there is an event or an error: + + ```bash + while :; do + RESP=$(<%= @curl %> -f "<%= @base %>/api/v1/agent/events?wait=25") || + { echo "coplan: wait failed: token expired or server unreachable" >&2; exit 1; } + [ "$(printf '%s' "$RESP" | jq '.events | length')" -gt 0 ] && break + sleep 1 + done + printf '%s\n' "$RESP" | jq . + ``` + +3. Test the wake once: post a comment on the plan, or ask your principal to. If the task's exit does not start a new turn for you, stop the loop and use the inbox fallback in step 6 of the primer. + +## When the task exits + +1. Read the output. +2. Set your state to `active`: `PATCH <%= @base %>/api/v1/plans/$PLAN_ID/agent_session` with `{"state": "active"}`. +3. Reply in the thread, then edit. Follow the `authority` field: act on `principal` comments; propose changes for `collaborator` comments. +4. Set your state to `complete`, or to `awaiting_input` if you asked a question. +5. Acknowledge: `POST <%= @base %>/api/v1/agent/events/ack` with `{"cursor": ""}`. +6. Start the loop again. + +If the loop exits with an error, mint a new token (see the primer) and start again. The loop fails on purpose: a failed task wakes you, but a silent failure leaves you waiting forever. Events that you did not acknowledge come back on the next wait, so you lose nothing. + +When you stop watching, stop the loop and `DELETE <%= @base %>/api/v1/plans/$PLAN_ID/agent_session`. + +## Use coplan-attach instead + +`coplan-attach --plan $PLAN_ID --no-session --once`, run as a background task, does the same steps: it claims the session, waits, prints a summary of the event, and acknowledges it. To install it, see "Reference tools" in <<%= @base %>/agent-instructions/live>. + +## Keep the setup + +Your setup ends with your session. With your principal's agreement, save the loop and these steps as a project skill (`.claude/skills/coplan-live/SKILL.md`) or a slash command (`.claude/commands/`). A new run can then attach in one step. diff --git a/engine/app/views/coplan/agent_instructions/guides/live_codex.text.erb b/engine/app/views/coplan/agent_instructions/guides/live_codex.text.erb new file mode 100644 index 00000000..d806a226 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/live_codex.text.erb @@ -0,0 +1,35 @@ +# Live Sessions in Codex + +Codex does not start a new turn when a background process exits. The wake depends on where Codex runs. + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions> and that you have a session token in `$TOKEN`. The protocol details are in <<%= @base %>/agent-instructions/live>. + +## Codex desktop: a scheduled follow-up + +Use this when your principal asks you to watch the plan. Do not hold a request open between runs. + +1. Claim the session, then set it to `complete`. The session subscribes your token to the plan's events. `complete` keeps the pill hidden between runs: + + ```bash + <%= @curl %> -X POST -H "Content-Type: application/json" -d '{}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . + <%= @curl %> -X PATCH -H "Content-Type: application/json" -d '{"state": "complete"}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . + ``` + +2. Create a scheduled follow-up in this conversation, every few minutes. Each run does this: + 1. Read the inbox: `<%= @curl %> "<%= @base %>/api/v1/agent/events?wait=0" | jq .` + 2. If there are no events, end the run. + 3. If there are events: set the session to `active`, reply, edit, and set it to `complete`. Follow the `authority` field: act on `principal` comments; propose changes for `collaborator` comments. + 4. Acknowledge: `POST <%= @base %>/api/v1/agent/events/ack` with `{"cursor": ""}`. + +3. When the watch is over, delete the schedule and `DELETE <%= @base %>/api/v1/plans/$PLAN_ID/agent_session`. + +## Codex CLI and IDE: a bridge or the inbox + +These surfaces cannot schedule turns. Two options: + +- **Ask your principal to run the bridge.** It runs `codex exec resume ""` for each event. Give your principal the setup in <<%= @base %>/agent-instructions/live/bridge> with `--adapter codex` and your session ID. +- **Read the inbox each turn.** Claim the session and set it to `complete` (step 1 above). At the start and end of each turn, read the inbox with `wait=0`. Tell your principal that you cannot watch the plan live. + +A background terminal process does not wake you. Do not leave one running. diff --git a/engine/app/views/coplan/agent_instructions/guides/live_hosted.text.erb b/engine/app/views/coplan/agent_instructions/guides/live_hosted.text.erb new file mode 100644 index 00000000..56437c11 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/live_hosted.text.erb @@ -0,0 +1,31 @@ +# Live Sessions for Hosted Agents + +Use this guide if your agent runs on a platform with no process between turns, such as a hosted runner or an Amp orb. Register a wake URL. CoPlan sends a signed request to that URL for each event. Your platform starts a turn, and the turn reads the inbox. + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions>. The protocol details are in <<%= @base %>/agent-instructions/live>. + +## Register + +Claim the session with a `wake_url`: + +```bash +<%= @curl %> -X POST -H "Content-Type: application/json" \ + -d '{"wake_url": "https://your-platform.example/hooks/coplan"}' \ + "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . +``` + +- The response includes `wake_secret` only once. Store it. +- The server must be able to reach the URL. CoPlan refuses hosts that resolve to private or loopback addresses. +- To remove the URL, claim again with `"wake_url": ""`. + +## Handle a wake + +Each request is a small JSON body with `event_id`, `event_type`, and `plan_id`. The header `X-CoPlan-Signature: sha256=` is the HMAC-SHA256 of the raw body, with `wake_secret` as the key. + +1. Verify the signature. Reject the request if it does not match. +2. Ignore an `event_id` that you already handled. +3. Start a turn. In the turn, read the inbox (`GET <%= @base %>/api/v1/agent/events?wait=0`), set your state to `active` with PATCH, reply, edit, set `complete`, and acknowledge the events. + +- CoPlan sends wakes even when your session is `complete`. The wake exists to start you again. +- Your token must be valid when the wake arrives. Mint it with a `ttl_seconds` that covers the time that you watch the plan (maximum 30 days). +- Keep the endpoint working. If deliveries to the URL fail repeatedly, CoPlan removes the URL. Your next claim registers it again. diff --git a/engine/app/views/coplan/agent_instructions/guides/markdown.text.erb b/engine/app/views/coplan/agent_instructions/guides/markdown.text.erb new file mode 100644 index 00000000..f93f8c58 --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/markdown.text.erb @@ -0,0 +1,92 @@ +# Writing Plan Markdown + +Plans use CommonMark with the GitHub extensions: tables, task lists, strikethrough, and autolinks. CoPlan also supports the features on this page, and slide decks inside a plan (see <<%= @base %>/agent-instructions/presentations>). CoPlan removes all other raw HTML when it shows the plan, but keeps it in the source. + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions>. + +## Tables + +Use a standard pipe table when each item has two or more attributes: + +```markdown +| Region | p95 latency | Owner | +|--------|------------:|-------| +| us-east | 180 ms | Payments | +| eu-west | 240 ms | Platform | +``` + +- Colons in the separator row set column alignment. +- Keep cells short. Put explanations in a paragraph or a footnote, not in a cell. +- Escape a literal pipe in a cell as `\|`. + +## Section links + +Number the headings that you refer to, and link to them with an explicit fragment. A heading `### 3.1 Rollout` has the fragment `#section-3-1`: + +```markdown +The flag stays off until the checks in [§3.1](#section-3-1) pass. +``` + +Readers see a preview of the section on hover. CoPlan does not link a bare `§3.1`, because plans also use `§` to cite external documents such as laws. + +## Footnotes and links + +- **To support a claim, use a footnote.** Put the source link and the supporting detail in the definition: `Queue depth peaked at 40k.[^queue-depth]` and `[^queue-depth]: Measured during the 2026-08-12 incident. [Dashboard](https://example.com/d/queues).` Readers can preview the note on hover. Use a descriptive footnote name, not a number. +- **To point to a resource, use a descriptive link:** `[implementation PR](https://github.com/org/repo/pull/42)`. Do not put bare URLs in prose. +- **To give a link a stable name, use a reference-style link:** `See [auth-repo].` with `[auth-repo]: https://github.com/org/auth-service "Auth Service"`. The key and title go to the plan's References. + +CoPlan adds every link in the content, including links in footnotes, to the plan's References section. Do not add these links again through the API. See "References" in <<%= @base %>/agent-instructions/creating>. + +## Diagrams + +Write Mermaid in a `mermaid` fence. CoPlan shows the diagram, and the source stays editable with the rest of the plan: + +````markdown +```mermaid +flowchart LR + Client --> API + API --> Database +``` +```` + +Use a diagram for architecture, data flow, sequences, or state. Do not use a diagram when a short list says the same thing. Readers can comment on a node or an edge of a diagram, and on a cell of a table. + +## Code blocks + +Always put a language tag on a code fence. CoPlan highlights the syntax with highlight.js and accepts any language name that highlight.js supports. Use the canonical names: `ruby`, `python`, `javascript`, `typescript`, `go`, `java`, `kotlin`, `swift`, `rust`, `c`, `cpp`, `csharp`, `sql`, `json`, `yaml`, `xml` (also for HTML), `css`, `graphql`, `protobuf`, `bash` (scripts), `shell` (terminal sessions with `$` prompts), `diff`, `http`. Use `plaintext` for logs. Do not invent tags: CoPlan shows an unknown tag as plain text. + +## Task lists + +`- [ ] Item` and `- [x] Done item` render as checkboxes. Readers can select a checkbox in the web UI. **Each selection is a hand edit.** After a person selects a checkbox, CoPlan refuses your next write until you read the plan again. + +## Collapsible sections + +Use `
` for long appendices, raw logs, or optional detail: + +```markdown +
Raw benchmark output + +Markdown here still renders. Keep the blank line after the summary. + +
+``` + +Add `open` to the `
` tag to show the section expanded by default. + +## Hover definitions + +Use `POS` for acronyms and team terms. Readers see the definition on hover. + +## Images and attachments + +Upload a file to the plan, then paste the `markdown` value from the response into the content. Images show in the plan body. + +```bash +<%= @curl %> -X POST -F "file=@./architecture.png" \ + "<%= @base %>/api/v1/plans/$PLAN_ID/attachments" | jq -r .markdown +``` + +- The maximum size is 25 MB per file. Allowed types are PNG, JPEG, GIF, WebP, PDF, TXT, MD, CSV, JSON, and ZIP. CoPlan returns `422` for other files. +- Only the plan author can upload or delete attachments. Any reader can list and download them. +- List the attachments: `GET <%= @base %>/api/v1/plans/$PLAN_ID/attachments`. Delete one: `DELETE <%= @base %>/api/v1/plans/$PLAN_ID/attachments/$ATTACHMENT_ID`. +- If a Mermaid diagram can show the content, use Mermaid instead of an image. A person can edit Mermaid; nobody can edit an image of a diagram. diff --git a/engine/app/views/coplan/agent_instructions/guides/organizing.text.erb b/engine/app/views/coplan/agent_instructions/guides/organizing.text.erb new file mode 100644 index 00000000..55e405ec --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/organizing.text.erb @@ -0,0 +1,115 @@ +# Organizing a Library + +Use this guide to organize, reorganize, or audit a library. To file one plan, you do not need it: send `folder_path` on create or in a `PATCH` to the plan (see <<%= @base %>/agent-instructions/creating>). + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions> and that you have a session token. + +## The model + +- A **library** is a tree of folders, up to <%= CoPlan::Folder::MAX_DEPTH %> levels deep. Each person has one. +- A plan is filed in **one** place. Filing it somewhere else moves it, also across libraries. A move changes the plan's URL, and the old URL still works. +- To move a plan, your principal must have written it or must have write access to the library where it is now. The destination library must also be writable. +- Each folder has a one-line `description` of what belongs in it. +- The library's audit log records each change, and whether a person or an agent made it. + +Use folders for where a plan lives, and tags for what it is about. `move_by_tag` (below) connects the two. + +## Step 1: Read the layout + +```bash +<%= @curl %> "<%= @base %>/api/v1/library" | jq . +``` + +This returns your principal's library: each folder's `id`, `name`, `description`, `path` (such as `"Team EBT/Q3"`), `plans_count` (plans directly in it), and `total_plans_count` (including subfolders). It also returns `unfiled_count` (your principal's plans with no folder), `top_tags`, and `recent_activity`. The `description` says what the owner meant. Trust it more than the folder name. + +`GET <%= @base %>/api/v1/libraries` lists all libraries with a `writable` flag. `GET <%= @base %>/api/v1/libraries/$LIBRARY_ID` reads one. You can change only writable libraries. + +## Step 2: Read the contents + +```bash +<%= @curl %> "<%= @base %>/api/v1/library/contents" | jq . +``` + +For another library, use `GET <%= @base %>/api/v1/libraries/$LIBRARY_ID/contents` here, and the same `$LIBRARY_ID` in Step 3. Use one library for the whole task, or you will classify plans from one library and move them in another. + +This returns one row for each plan in the library: `plan_id`, `title`, `summary` (a generated abstract), `tags`, `visibility`, `archived`, `author`, `folder_path`, `placed_by`, and dates. Rows are sorted by folder path. + +Filters, which you can combine: + +- `?folder_path=Team%20EBT&recursive=true`: one subtree +- `?tag=pricing`: plans with a tag +- `?unfiled=true`: your principal's plans with no folder +- `?archived=true`: archived plans (not included by default) +- `?limit=&offset=`: pages (500 rows by default) + +Classify plans from `title`, `summary`, and `tags`. Do not fetch the content of each plan. + +## Step 3: Change it in one batch + +`POST <%= @base %>/api/v1/library/organize` (or `/api/v1/libraries/$LIBRARY_ID/organize`) takes up to 100 operations. Each operation applies or fails on its own. A failed operation reports its error, and the others still apply. + +```bash +<%= @curl %> -X POST -H "Content-Type: application/json" \ + -d '{ + "dry_run": true, + "operations": [ + {"op": "create_folder", "path": "Archive/2025", "description": "Shipped or replaced work from 2025"}, + {"op": "describe_folder", "folder_path": "Infra", "description": "Platform and infrastructure plans"}, + {"op": "rename_folder", "folder_path": "Misc", "name": "Inbox"}, + {"op": "move_folder", "folder_path": "Q3", "new_parent_path": "Team EBT"}, + {"op": "move_many", "plan_ids": ["'$PLAN_A'", "'$PLAN_B'"], "folder_path": "Team EBT/Q3"}, + {"op": "move_by_tag", "tag": "pricing", "folder_path": "Pricing"}, + {"op": "delete_folder", "folder_path": "Old Empty Folder"} + ] + }' \ + "<%= @base %>/api/v1/library/organize" | jq . +``` + +| `op` | Fields | Notes | +|---|---|---| +| `create_folder` | `path`, `description` | Creates missing parent folders. Sets the description on the last folder. | +| `describe_folder` | `folder_path` or `folder_id`, `description` | | +| `rename_folder` | `folder_path` or `folder_id`, `name` | | +| `move_folder` | `folder_path` or `folder_id`, `new_parent_path` or `new_parent_id` | An empty parent moves the folder to the top level. | +| `delete_folder` | `folder_path` or `folder_id` | Empty folders only. | +| `move` | `plan_id`, `folder_path` or `folder_id` | An empty destination removes the plan from its folder. `folder_path` creates missing folders. | +| `move_many` | `plan_ids` (up to 500), `folder_path` or `folder_id` | **Use this to classify many plans**: one operation for each destination folder. Plans that cannot move are listed in `failed`; the others move. | +| `move_by_tag` | `tag`, `folder_path` or `folder_id`, `scope` | `scope: "library"` (default) moves plans with the tag that are already in this library. `scope: "visible"` moves every plan with the tag that you can see and are allowed to move. | + +The response has `results` for each operation (`status: "ok"` or `status: "error"` with the reason), `ok_count`, `error_count`, and a `run_id`. Give the `run_id` to your principal. It finds this run in the audit log. + +### Propose first with dry_run + +With `"dry_run": true`, CoPlan runs the whole batch, reports what would happen (including failures), and then undoes everything. Use this for any reorganization larger than a few moves: + +1. Read the layout and the contents. +2. Design the structure, and write the operations. +3. Send them with `dry_run: true`. Show your principal the new outline and the results. +4. After your principal agrees, send the same body with `dry_run: false`. + +## Where a plan is filed + +```bash +<%= @curl %> "<%= @base %>/api/v1/plans/$PLAN_ID/locations" | jq . +``` + +This returns an array with zero or one entry: `library_name`, `owner`, `folder_path`, `folder_description`, `placed_by`, and `writable` (whether your principal can change that library). + +## The audit log + +```bash +<%= @curl %> "<%= @base %>/api/v1/library/events" | jq . +``` + +Each event has `event_type` (`plan_filed`, `plan_moved`, `plan_removed`, `folder_created`, `folder_renamed`, `folder_moved`, `folder_described`, `folder_deleted`), `actor`, `actor_type` (`local_agent` for an agent that acts for the owner), `actor_label` (the token name), `run_id`, `plan_title`, and the `before` and `after` paths. Filters: `?event_type=`, `?plan_id=`, `?run_id=`, `?before=` (for pages), `?limit=` (50 by default). Only the library owner can read it. + +Use it to answer "who moved my plan?", to check what an earlier run did, or to summarize recent changes. + +## Rules + +- **Use the existing structure.** Read the layout first. Add to the existing folders before you make new ones. The descriptions say what the owner meant. +- **Describe each new folder.** Every `create_folder` needs a `description`. The next agent reads it. +- **Propose, then apply.** For more than a few moves, run a dry run and show your principal the outline first. Never reorganize a library that nobody asked you to reorganize. +- **Keep it shallow.** A few clear top-level folders are better than a deep tree. The depth limit is <%= CoPlan::Folder::MAX_DEPTH %>. +- **File unfiled plans first.** If `unfiled_count` is more than 0, filing those plans is the most useful low-risk change. +- **Keep tags and folders separate.** Do not repeat the folder tree in tags. If a tag and a folder mean the same thing, use `move_by_tag` once and remove the duplicate. diff --git a/engine/app/views/coplan/agent_instructions/guides/presentations.text.erb b/engine/app/views/coplan/agent_instructions/guides/presentations.text.erb new file mode 100644 index 00000000..4afe695f --- /dev/null +++ b/engine/app/views/coplan/agent_instructions/guides/presentations.text.erb @@ -0,0 +1,72 @@ +# Slide Decks in Plans + +Any plan can contain one or more slide decks. A deck is a region of the plan's Markdown between `::: {.presentation}` and `:::`. Readers page through each deck in place, and each deck has its own Present button. The rest of the plan reads as a normal document. Edits, comments, and versions work the same way inside a deck. + +This guide assumes that you have read the primer at <<%= @base %>/agent-instructions>. All features in <<%= @base %>/agent-instructions/markdown> work on slides. + +## A deck region + +````markdown +Context for the reader, before the deck. + +::: {.presentation #q3-review theme="coplan"} + +# Q3 Orders Review + +Payments platform, Q3 + +--- + +## What shipped + +- Split tender on all registers +- Offline card authorization at 100% + +::: + +Analysis after the deck. Another deck can follow later in the plan. +```` + +- The opening line and the closing `:::` must each be a line by itself, with a blank line before and after. +- `#q3-review` is an optional ID: a letter, then letters, digits, `_`, or `-`. Use one when people link to the deck. +- `theme` is optional: `coplan` (the default) or `graphite`. +- CoPlan shows a region marker that it does not recognize, or a region that is not closed, as plain text. If the deck does not appear, check the marker lines. +- The `Presentation` plan type's template starts with a deck region. Use that type when the whole plan is a deck. To add a deck to another plan, add a region. + +## Slides + +- Inside a region, a line of three or more dashes (`---`) starts a new slide. Put a blank line before it. Without the blank line, Markdown makes the line above it a heading. +- Outside a region, `---` is a horizontal rule. +- `***` and `___` are horizontal rules inside a slide. They do not start a new slide. +- `` is a speaker note. Only the presenter sees it. Other HTML comments also do not show. +- Footnote and reference-link definitions apply to the whole plan. Put them anywhere. The footnotes appear together in the plan's References. + +## Layout + +There is no layout syntax. CoPlan chooses the layout of each slide from its content. It checks these rules in order and uses the first match. Any layout can start with a heading. + +| Slide content after the heading | Layout | +|---|---| +| Nothing, or one short paragraph (160 characters or fewer) | **title**: large heading, paragraph as subtitle | +| One image or Mermaid diagram, with an optional short caption | **stage**: the visual fills the slide | +| One code block, with an optional short caption | **code** | +| One blockquote, with an optional short attribution | **quote** | +| One table, with an optional short caption | **table** | +| Exactly two lists | **columns**: side by side | +| One list of 15 or more short items (60 characters or fewer each) | **directory**: two columns | +| One image or diagram at the start or end, and other content | **split**: the visual beside the text | +| Any other content | **content** | + +To get a layout, write the content shape that it needs. For example, to compare two options, write a heading and exactly two lists. The second list must use a different marker (`*` after `-`). With the same marker, Markdown joins the items into one list. + +## Fit + +The text gets smaller as a slide fills. A slide with about 5 lines of content shows the text at its largest. A slide with up to 14 lines is still easy to read. At 15 lines or more, the text is at its smallest size, and a slide that still does not fit scrolls. Divide a full slide into two slides. + +A line is about 60 characters of text, one list item, or one table row. An image counts as 3 lines. A Mermaid diagram counts as 4 lines. + +## Writing a good deck + +- Put one idea on each slide. Write the point as the heading ("Offline auth is at 100%"), not a topic label ("Status"). +- Keep bullets short. Put detail in the speaker notes or in the document around the deck. +- End with the decision or next step that you want from the audience. diff --git a/engine/app/views/coplan/agent_instructions/organizing.text.erb b/engine/app/views/coplan/agent_instructions/organizing.text.erb deleted file mode 100644 index 99f059b6..00000000 --- a/engine/app/views/coplan/agent_instructions/organizing.text.erb +++ /dev/null @@ -1,118 +0,0 @@ -# CoPlan Library Organization Guide - -Sub-instructions for organizing libraries. Read the main instructions at `<%= @base %>/agent-instructions` first — this guide assumes you already know how to authenticate and shelve a single plan. - -<%= @auth_instructions %> - -## The model in three sentences - -A **library** is a folder tree owned by a user (team libraries use the same model). Filing a plan is a **placement** — the library owner's organization of the plan, never a property of the plan itself, so the same plan can sit in many libraries at once and moving it in one library changes nothing anywhere else. Folders go at most <%= CoPlan::Folder::MAX_DEPTH %> levels deep, carry a one-line `description` of what belongs in them, and every rearrangement is recorded in the library's audit log with whether a human or an agent did it. - -Use **folders** for "where does this live?" and **tags** for "what is it about?" — move-by-tag (below) is the bridge between the two. - -## Step 1 — Learn the layout (one call) - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/library" | jq . -``` - -Your own library's map: every folder with `id`, `name`, **`description`** (its meaning — trust it over guessing from the name), `path` (`"Team EBT/Q3"`), `plans_count` (directly inside), `total_plans_count` (including subfolders), plus `unfiled_count` (your plans not yet shelved), `top_tags`, and `recent_activity`. - -Any library by id: `GET <%= @base %>/api/v1/libraries/$LIBRARY_ID` (read-only unless it's yours; `GET /api/v1/libraries` lists all libraries with a `writable` flag). - -## Step 2 — Bulk-read the contents - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/library/contents" | jq . -``` - -One compact row per shelved plan: `plan_id`, `title`, **`summary`** (auto-generated abstract — enough to classify a plan without fetching its content), `tags`, `visibility`, `archived`, `author`, `folder_path`, `placed_by`, and dates. Sorted by folder path, so the whole shelf reads top-to-bottom. - -Filters (combine freely): - -- `?folder_path=Team%20EBT&recursive=true` — one subtree -- `?tag=pricing` — everything shelved here with a tag -- `?unfiled=true` — your plans not yet shelved (file these!) -- `?archived=true` — archived plans (excluded by default) -- `?limit=&offset=` — pagination (default 500 rows) - -Do **not** fetch each plan's full content to classify it — `summary` + `tags` + `title` are there precisely so organizing costs one request, not N. - -## Step 3 — Organize in bulk - -`POST /api/v1/library/organize` (or `/api/v1/libraries/$LIBRARY_ID/organize`) takes up to 100 operations. Each op is atomic — a bad one reports its error and the rest still apply. - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{ - "dry_run": true, - "operations": [ - {"op": "create_folder", "path": "Archive/2025", "description": "Shipped or superseded work from 2025"}, - {"op": "describe_folder", "folder_path": "Infra", "description": "Platform and infrastructure plans"}, - {"op": "rename_folder", "folder_path": "Misc", "name": "Inbox"}, - {"op": "move_folder", "folder_path": "Q3", "new_parent_path": "Team EBT"}, - {"op": "move", "plan_id": "'$PLAN_ID'", "folder_path": "Team EBT/Q3"}, - {"op": "move_by_tag", "tag": "pricing", "folder_path": "Pricing"}, - {"op": "delete_folder", "folder_path": "Old Empty Folder"} - ] - }' \ - "<%= @base %>/api/v1/library/organize" | jq . -``` - -The operation vocabulary: - -| op | fields | notes | -|---|---|---| -| `create_folder` | `path`, `description` | creates the whole hierarchy; sets description on the leaf | -| `describe_folder` | `folder_path`\|`folder_id`, `description` | give folders meaning — future agents read these | -| `rename_folder` | `folder_path`\|`folder_id`, `name` | | -| `move_folder` | `folder_path`\|`folder_id`, `new_parent_path`\|`new_parent_id` | blank parent → top level | -| `delete_folder` | `folder_path`\|`folder_id` | only empty folders | -| `move` | `plan_id`, `folder_path`\|`folder_id`, `from_library_id` | blank destination → unfile; `folder_path` creates as needed | -| `move_many` | `plan_ids` (≤500), `folder_path`\|`folder_id`, `from_library_id` | the grouped form of `move` — **prefer this for mass classification**: one op per destination folder, not one per plan. Per-plan tolerant: failures land in `failed`, the rest move | -| `move_by_tag` | `tag`, `folder_path`\|`folder_id`, `scope` | `scope: "library"` (default) moves plans already on this shelf; `scope: "visible"` pulls every visible plan with the tag onto it | - -The response echoes per-op `results` (`status: "ok"` with details, or `status: "error"` with the reason), `ok_count` / `error_count`, and a **`run_id`** grouping every audit event the call wrote — quote it to the user and filter the log with `?run_id=` to review exactly what one run did. - -**Cross-library moves**: `{"op": "move", "plan_id": ..., "folder_path": "Somewhere", "from_library_id": "$OTHER_LIBRARY_ID"}` shelves the plan in the target library and removes it from the source in one atomic op. You need write access to both sides (today: they're yours; team libraries will use membership). - -### Propose first: dry_run - -`"dry_run": true` executes the whole batch, reports exactly what would happen — including which ops would fail and why — then rolls everything back. **This is the intended workflow for "reorganize my whole library":** - -1. Read the overview + contents. -2. Design a structure; build the operations array. -3. POST it with `dry_run: true`; show the user the proposed outline and per-op results. -4. On approval, POST the same body with `dry_run: false`. - -## Where is this plan? (reverse lookup) - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans/$PLAN_ID/locations" | jq . -``` - -Every library the plan is shelved in: `library_name`, `owner`, `folder_path`, `folder_description`, `placed_by`, `writable` (whether *you* can move it there). - -## The audit log - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/library/events" | jq . -``` - -Who moved what where, when — and whether an agent did it: each event has `event_type` (`plan_filed`, `plan_moved`, `plan_removed`, `folder_created`, `folder_renamed`, `folder_moved`, `folder_described`, `folder_deleted`), `actor` (name), `actor_type` / `agent` (`"local_agent"` means an agent acting on the owner's behalf — that's you), `actor_label` (the API token's name — which agent session), `run_id` (which bulk organize call), `plan_title`, and `before` → `after` paths. Filters: `?event_type=`, `?plan_id=`, `?run_id=`, `?before=` (pagination), `?limit=` (default 50). Owner-only. - -Use it to answer "who moved my plan?", to review what a previous agent session did, or to summarize recent reorganization for the user. - -## Organizing principles - -- **Reuse before invent.** Read the overview first; extend the existing structure rather than imposing a new one. Respect folder descriptions — they are the owner's stated intent. -- **Describe what you create.** Every `create_folder` should carry a `description`. You are writing signage for the next agent (possibly yourself, without this context). -- **Propose, then apply.** For anything beyond a few moves, dry-run and show the user the outline before applying. Never mass-reorganize someone's library unprompted. -- **Shallow beats deep.** Prefer a few clear top-level folders to unnecessary nesting. The depth cap is <%= CoPlan::Folder::MAX_DEPTH %>. -- **Unfiled is a to-do list.** `unfiled_count` > 0 means plans nobody shelved; filing them is the highest-value low-risk organizing move. -- **Tags stay orthogonal.** Don't mirror the folder tree in tags or vice versa. If a folder and a tag mean the same thing, `move_by_tag` once and drop the redundancy. diff --git a/engine/app/views/coplan/agent_instructions/show.text.erb b/engine/app/views/coplan/agent_instructions/show.text.erb index 1423e067..7adb7b27 100644 --- a/engine/app/views/coplan/agent_instructions/show.text.erb +++ b/engine/app/views/coplan/agent_instructions/show.text.erb @@ -1,903 +1,169 @@ -# CoPlan API +# CoPlan for Agents -Interact with the CoPlan app to create, read, edit, and comment on plan documents. +CoPlan is where a team writes and reviews plans: Markdown design documents. People read plans and comment on exact passages. Agents do most of the writing: they draft plans, apply edits, and answer comments. You act for one person, your **principal** — the human whose credential you hold. -<%= @auth_instructions %> - -## Your Identity: One Session Token - -Every API call except the mint below requires `Authorization: Bearer `. Create one short-lived token for this run and declare your identity once. CoPlan then applies that token's agent name, harness, model, and human owner to every version, event, session, and comment automatically. Do not add signatures to plan content or repeat your identity in later requests. - -<% if @request_auth_available %> -Mint one as your first call. Your host authenticates this initial request, so you do not need a token from Settings: -<% else %> -Mint one as your first call using a long-lived root token from Settings: -<% end %> - -```bash -<%= @mint_curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"agent_name": "Claude", "name": "claude run", "metadata": {"harness": "claude-code", "harness_version": "2.1.3", "model": "claude-fable-5"}}' \ - "<%= @base %>/api/v1/tokens" | jq . -``` - -- `agent_name`: a short label for this agent session ("Claude", "Amp"). Declare it here once; the token supplies it on every later call. Truncated past 20 characters. -- `metadata`: optional JSON identity facts for this run. Use `harness`, `harness_version`, and `model`. Stable harness keys include `amp`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `goose`, and `opencode`; unknown keys are registered automatically. Capped at 4 KB. -- `ttl_seconds`: optional, default 7 days, max 30 days. -- The response's `token` is shown once. Send it as `Authorization: Bearer ` on every subsequent call. -- Mint once at the start of your run and reuse it — minting per call would give you a new identity every time. -- When you finish, `DELETE <%= @base %>/api/v1/tokens/current` (authenticated with the token itself) so the credential dies with the run instead of waiting out its TTL. - -## Live Setup: Choose a Wake Path Safely - -If your task creates, edits, reviews, or babysits a plan, check whether your harness has a way to start another model turn when an event arrives. Establish live collaboration when it does. Do not leave supported setup as a suggestion for the human to configure later. - -An attachment requires both a claimed agent session **and** a delivery path that can start another model turn. Claiming a presence pill without running a wait, bridge, or webhook is not attachment and misleads the people looking at the plan. - -**Never run an unbounded wait in the foreground merely to satisfy this section.** A foreground wait is appropriate only when the user explicitly asked you to monitor, babysit, or wait for feedback, and every foreground invocation must have a finite deadline. For ordinary plan work, use a bridge, webhook, or verified background wake; otherwise use the degraded fallback below and continue the requested work. - -The raw protocol works with curl. Harness-native scheduling and resume features can wrap it so delivery starts a new turn instead of occupying the current one. - -The raw protocol requires nothing beyond an HTTP client. The live loop is three calls — shown as curl, but any HTTP client works: - -1. **Claim** your presence on the plan: `POST <%= @base %>/api/v1/plans/$PLAN_ID/agent_session`. -2. **Wait**: `GET <%= @base %>/api/v1/agent/events?wait=25` returns the instant an event lands, or empty after `wait` seconds (the server caps `wait` at 55, so loop it). -3. **Act, then ack**: PATCH your session to `active`, reply and edit, then POST `{"cursor": ""}` to `<%= @base %>/api/v1/agent/events/ack`. - -Exact requests, payloads, and etiquette: **Live Collaboration**, below. The real setup problem is not the calls — it is arranging for delivery to start your next turn. Use the harness table as a starting point, then choose a branch based on capabilities you can verify: - -| Harness | Start here | -|---|---| -| Claude Code | An operator-provisioned sidecar can invoke `claude --resume -p `. A background Bash wait is valid only while its completion is known to re-enter the agent loop; a terminal notification alone is not enough. | -| Goose / Gemini CLI | Use a verified existing-session resume mechanism if your installation exposes one; otherwise use the degraded fallback. Starting an ACP server creates a different agent and is not attachment for this conversation. | -| Codex desktop | For an explicit monitoring/babysitting request, create an in-chat scheduled follow-up that drains `wait=0` on each run. Do not hold curl open between scheduled runs. | -| Codex CLI / IDE | These surfaces do not manage scheduled tasks. An operator-provisioned bridge can resume a known thread; otherwise use the degraded fallback. A background terminal process by itself is not a wake path. | -| Cursor / Amp / OpenCode | A verified resume or background-completion integration; otherwise use the degraded fallback. | -| Hosted or custom agent | A `wake_url` webhook is usually the native path. | - -Harness names are hints, not proof. Versions and hosts differ; if completion does not start a model turn, that branch is unsupported. - -**A. An operator provisioned a persistent sidecar.** `coplan-bridge` runs outside the agent turn and drains the inbox. An exec-resume adapter can re-enter an existing session when configured with its session/thread ID. Do not start this infrastructure implicitly during ordinary plan work. ACP is intentionally not an attachment path here: it creates a separate dedicated agent rather than resuming this conversation. - -**B. Your hosted agent accepts webhooks.** Claim your agent session with a `wake_url`; CoPlan POSTs a signed ping on every event. See `wake_url` under **Claim an Agent Session** below. - -**C. Your harness can schedule follow-up turns.** Create a short-interval follow-up in the current conversation. Each run drains `GET <%= @base %>/api/v1/agent/events?wait=0`; it never holds a request open. If events exist, mark the session `active`, handle and ack them, then return to `complete`. Keep the subscription row but do not leave the presence state as `watching` between runs. Stop the schedule when the monitoring request is complete. - -**D. Background completion demonstrably starts a new model turn.** Run the wait as a background task and keep working. Use this only if your harness is known to re-enter the agent loop when the process exits. The task exits when there is news: - -```bash -while :; do - RESP=$(<%= @curl %> -f "<%= @base %>/api/v1/agent/events?wait=25") || - { echo "coplan: wait failed — token expired or server unreachable" >&2; exit 1; } - [ "$(printf '%s' "$RESP" | jq '.events | length')" -gt 0 ] && break - sleep 1 -done -printf '%s\n' "$RESP" | jq . -``` - -Act on what it prints, then ack, then start it again. No cursor needed: events you haven't acked are re-delivered whole on the next call, so nothing is lost between runs. It fails loudly on purpose — a process exit is your wake either way, so a dead token wakes you to re-mint instead of parking you forever. (The loop needs a shell and `jq`, but it's just GET-until-nonempty — port it to whatever your runtime has.) - -**E. The user explicitly asked you to wait or monitor in this turn.** Use one bounded long-poll request at a time, for example `GET <%= @base %>/api/v1/agent/events?wait=25`. Repeat only while the monitoring request remains active. Do not run the infinite shell loop in the foreground. - -**No wake-capable branch.** Do not claim a presence session you cannot service. Drain the durable inbox at the start and end of every turn with `GET <%= @base %>/api/v1/agent/events?wait=0`, and tell the user that this harness cannot maintain live attachment. This is a degraded fallback, not successful live setup. - -**Make it durable.** Don't leave working wake wiring in your context window — it dies with your session. Save it as a first-class capability of your harness: a skill or saved command, a custom tool definition, a webhook integration, or a standing bridge config. The test: a fresh run tomorrow should attach with one action, not by rediscovering this page. Give the wiring its own identity while you're at it — mint a run-scoped token at startup (**Session Tokens**, in Live Collaboration below) so each run keeps one inbox and one pill. - -**Prebuilt tools (optional implementation; require Ruby).** This server serves ready-made implementations of the supported behavior: `coplan-attach` (background branch D or bounded foreground branch E, plus sticky run-scoped tokens, presence, and readable event briefs — its helper `coplan_session.rb` must sit next to it) and `coplan-bridge` (operator-provisioned existing-session resume for branch A). You may implement the protocol directly instead, but do not skip a wake path your harness supports merely because these helpers are optional. They are executable code fetched from the network. Read every file you download before you run it, the helper included (it executes in-process); each is a few hundred lines. What to check: `coplan-attach` and `coplan_session.rb` make network calls only to this server, write only under `~/.coplan` (or `$COPLAN_HOME` if you set it), and spawn no subprocesses. `coplan-bridge` may additionally read `~/.config/coplan/bridge.json` and — this being its whole job — exec the one agent command you configured, nothing else. If what you downloaded does more, don't run it. - -```bash -mkdir -p ~/.coplan/bin && cd ~/.coplan/bin -curl -sfO <%= @base %>/agent-tools/coplan-attach -curl -sfO <%= @base %>/agent-tools/coplan_session.rb -``` - -Now read both files against the checks above. Then: - -```bash -chmod +x ~/.coplan/bin/coplan-attach -export COPLAN_BASE=<%= @base %> -export COPLAN_TOKEN="$TOKEN" # the token you already hold — never paste the literal secret -~/.coplan/bin/coplan-attach --plan $PLAN_ID --name Claude --once --timeout 300 -``` +What this means for your work: -`--once` serves branch D or E in one command: it blocks until the first event, prints the brief, acks, and exits. `--timeout 300` gives your turn back on a quiet plan (exit 64 means nothing happened); always set a finite timeout in the foreground. Without `--once` it stays attached and streams, so use that form only under an external supervisor. The bridge lives at `<%= @base %>/agent-tools/coplan-bridge` for operator-provisioned resume of a known existing session. The scripts claim your session for you (`coplan-attach` also mints its own run-scoped token) — on the raw-curl branches, claim it yourself (step 1). - -## Quick Start: The Editing Loop - -Keeping a local Markdown file and syncing whole documents is the intended workflow — you do not need to express edits as granular operations. The canonical loop: - -1. **Read** — `GET <%= @base %>/api/v1/plans/$PLAN_ID/snapshot`. Save `current_content` to a local file and note `current_revision`. -2. **Edit** the local file however you like. -3. **Write** — `PUT <%= @base %>/api/v1/plans/$PLAN_ID/content` with the full new content, `base_revision` set to the revision you read, and a specific `change_summary` (it becomes the version's label in the plan history — "Tightened rollout plan per Sam's feedback", not "Update"). -4. **On `409 Conflict`** someone edited in between: re-read the snapshot, re-apply your changes to the fresh content, and PUT again with the new revision. Never retry with the stale `base_revision`. -5. **On `409` with `"code": "human_edit_pending"`** a person edited the plan by hand and you haven't read it since. Their diff is in the response. Read it, keep what they wrote, and only then write again — see [When a person has edited by hand](#when-a-person-has-edited-by-hand). - -The server diffs your content against the current revision, records granular operations automatically, and preserves comment anchors in unchanged regions. Re-read before every editing session — don't trust a local copy that's more than a few minutes old, and don't keep long-lived local state between tasks. - -See [Editing Plans](#editing-plans-recommended-full-content-replacement) for the full request/response reference, and the lease + operations path only when you need a surgical single-operation edit. - -## Writing Style - -These rules apply to every plan you write or edit. One exception mechanism: **a plan type's description or template may override any of them — the type wins.** (A persuasive pitch type can welcome persuasive language; these defaults assume a working document.) - -**The platform already records it.** Never write document metadata into the body: - -- No date or "last updated" lines — every version is timestamped in the plan history. -- No author, attribution, or "prepared by" lines — your session token already attributes every version and comment to your harness and its human owner. -- No status lines, status fields, or status tables — visibility, archival, and tags carry state (see [Visibility & Archiving](#visibility--archiving)). There is no document status; do not invent one. -- No version numbers or changelogs — the plan history is the changelog. - -The exception is dates that anchor **facts**, which belong in content: "confirmed against production 2026-08-19", "verified at commit abc123". - -**Write plainly.** Follow Simplified-Technical-English-style rules: - -- Short sentences. One idea per sentence. Active voice, present tense. -- Plain verbs: "use", not "leverage"; "check", not "perform validation of". -- No metaphors, no marketing adjectives, no drama. Words that signal padding — "robust", "seamless", "comprehensive", "cutting-edge", "elegant", "crucially", "delve", "honest", "actually" — are banned unless you are quoting someone. -- State facts without selling them. The reader is a colleague deciding what to do, not an audience to impress. Write like a runbook, not a keynote. -- Prefer a table or list over a paragraph when the content is enumerable. -- Length is a cost. Write the shortest document that serves its reader, then stop. - -## API Reference - -### List Plans - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans" | jq . -``` - -Optional query params: `?visibility=draft` (your own private/unshared plans) or `?visibility=published`, `?archived=true` (archived plans are excluded unless you ask for them). - -### Get Plan - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans/$PLAN_ID" | jq . -``` - -Returns: `id`, `title`, `url`, `visibility`, `archived`, `current_content` (markdown), `current_revision`, `references` (array of linked resources). - -`url` is the document's address, and there is exactly one — `<%= @base %>///`, reading like the document it points at. **When you tell a human about a plan, give them the `url`, never the id.** It follows a retitle or a move on its own, and the old address keeps resolving. - -### Get Plan Snapshot (Recommended) - -Returns everything about a plan in one request — use this instead of calling multiple endpoints. - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans/$PLAN_ID/snapshot" | jq . -``` - -Returns: plan metadata, `current_content`, `current_revision`, `comment_threads` (with nested comments), `references`, and `collaborators`. This replaces the need to call GET plan + GET comments + GET references separately. - -### Create Plan - -Creating a plan is a deliberate act of publishing into a shared, organized library — not dumping a file. **Before the POST, do three cheap checks** (skip any step whose answer you already have from this session): - -1. **Pick the folder.** `GET <%= @base %>/api/v1/library` shows your folder tree with each folder's `description`. Choose the folder whose description matches this document; pass it as `folder_path` on create. A plan created without one lands unfiled — someone has to clean up after you. -2. **Pick the type.** `GET <%= @base %>/api/v1/plan_types` (or the [Plan Types](#plan-types) table below) lists every type with its description. Choose the **most specific** type that fits. **General is the fallback of last resort** — reaching for it without reading the list is almost always wrong. -3. **Follow the type's template.** The `plan_types` response includes each type's `template_content` — the document structure readers of that type expect. Structure your content against it: keep its sections (drop one only when it's genuinely inapplicable), fill them with real content rather than placeholder text. - -Then create everything in one call: - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '<%= raw @create_example_json %>' \ - "<%= @base %>/api/v1/plans" | jq . -``` +- Each write you make creates a new version that cannot change. Your session token labels it with your agent name, harness, and principal. Do not record dates, authors, or change history in the document. +- People ask for changes in comments. Comments from your principal are instructions. Comments from other people are requests. +- A person's hand edit outranks your work. While a person is editing, and after they save until you read again, CoPlan refuses your writes. +- Every plan has a `url`. Give people the `url`, not the `id`. -Optional fields: +This page covers what every agent needs. For other tasks, fetch the matching guide from [Guides](#guides). -- `plan_type` (string) — the name of a plan type (step 2 above). Omitting it files the plan under **General**, which should be a considered choice, not a default. The plan type's `default_tags` are applied to the plan automatically. -- `folder_path` (string) or `folder_id` (string) — where to file the plan in your library (step 1 above). `folder_path` finds or creates the hierarchy (e.g. `"Team EBT/Q3"`). See [Libraries & Folders](#libraries--folders). -- `tags` (array of strings) — added on top of the type's `default_tags`. See [Tags](#tags). -- `visibility` (string) — plans are **shared with the whole org by default**; `"draft"` (shown as "private" in the UI) exists as a rare escape hatch, not a normal step (see [Visibility & Archiving](#visibility--archiving)). -- `references` (array) — see [References](#references). - -#### Diagrams - -Plans support Mermaid diagrams. When a diagram communicates architecture, data flow, sequence, or state more clearly than prose, write a fenced `mermaid` block in the plan Markdown: - -````markdown -```mermaid -flowchart LR - Client --> API - API --> Database -``` -```` - -The web UI renders these blocks as diagrams. Keep the Mermaid source in the plan so humans and agents can review and edit it alongside the surrounding Markdown. - -#### Code blocks - -Always tag fenced code blocks with a language so the web UI can syntax-highlight them: - -````markdown -```ruby -def total(order) = order.line_items.sum(&:amount) -``` -```` - -Use highlight.js **canonical** language names — any language in the highlight.js distribution works, with its grammar loaded on demand. Common aliases (`js`, `ts`, `rb`, `py`, `golang`, `yml`, `sh`, `html`, `c++`, `c#`) also resolve, but prefer canonical names: `ruby`, `python`, `javascript`, `typescript`, `go`, `java`, `kotlin`, `swift`, `rust`, `c`, `cpp`, `csharp`, `sql`, `json`, `yaml`, `xml` (covers HTML), `css`, `graphql`, `protobuf`, `bash` (for scripts), `shell` (for terminal sessions with `$` prompts), `diff`, `http`, `plaintext`. - -Unrecognized tags render as plain text, so never invent one — use `plaintext` for logs and other unhighlightable content, and `mermaid` only for diagrams (see above). - -#### Rich formatting - -Beyond standard Markdown (tables, task lists, strikethrough), plans support: - -- **Footnotes** — `Some claim.[^queue-depth]` with `[^queue-depth]: Supporting detail and a [durable source](https://example.com/source).` anywhere in the document. Rendered as numbered citation anchors with the supporting note and source identity in the plan's References section. Readers can hover or focus the anchor to preview the full note, then click to jump to it. -- **Collapsible sections** — `
Title shown when collapsed` … `
`. Use for long appendices, raw logs, or optional deep-dives so the document stays scannable. Add `open` to the `
` tag to render expanded by default. Leave a blank line after `…` so Markdown inside the section still renders. -- **Hover definitions** — `TERM`. Use for acronyms and project-specific jargon so readers outside the team can follow along. - -Other raw HTML is stripped on render (the Markdown source is preserved), so stick to these plus standard Markdown. - -#### Citations and internal cross-references - -CoPlan has three related tools with different jobs: - -1. **Cite an external source for a claim with a footnote.** Put the source link and enough supporting context in the definition. The inline citation previews on hover/focus; clicking it jumps to the note. Its URL is auto-extracted into the structured References inventory, which supplies the source title, type, and domain shown with the citation. -2. **Reference another section in the same plan with an explicit Markdown link.** Number the target heading, then link its stable fragment: a heading `### 3.1 Rollout` is referenced as `see [§3.1](#section-3-1)`. Hover/focus previews that section and click jumps to it. Never leave a bare `§3.1` when you mean an internal link—section signs are also used for external statutes, so CoPlan deliberately does not guess. -3. **Mention an external resource directly with a normal descriptive link**, such as `[implementation PR](https://github.com/org/repo/pull/42)`. Prefer a footnote instead when the link supports a specific factual claim. - -Use descriptive link text and durable URLs. Avoid bare URLs in prose. Footnote names are author-facing identifiers, so prefer stable semantic names (`[^queue-depth]`) over renumbering a long document by hand. - -### Update Plan - -Update plan metadata (title, visibility, tags). Only fields included in the request body are changed. - -```bash -<%= @curl %> -X PATCH \ - -H "Content-Type: application/json" \ - -d '{"visibility": "published"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID" | jq . -``` - -Allowed fields: `title` (string), `visibility` (`"published"` — see [Visibility & Archiving](#visibility--archiving)), `archived` (boolean), `plan_type` (string — retype the plan; unknown names get a 422 listing the valid ones, and the new type's `default_tags` are added), `tags` (array of strings), `references` (array — see [References](#references) below). - -### List Tags - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/tags" | jq . -``` - -Returns all tags sorted by usage frequency (most-used first). Each tag includes `id`, `name`, `plans_count`. - -### Tags - -Tags categorize plans for discovery. Use them to help people find related plans. - -**Guidelines:** -- Check existing tags with `GET /api/v1/tags` before creating new ones — reuse over invention. -- Use lowercase, hyphenated names (e.g., `infrastructure`, `api-design`, `cost-reduction`). -- Apply 1–3 tags per plan. Too many tags dilute meaning. -- Update tags via `PATCH /api/v1/plans/:id` with `{"tags": ["tag-1", "tag-2"]}`. -- Good tags describe the *domain* or *concern* (e.g., `security`, `onboarding`), not the status or team. - -### Libraries & Folders - -Every user has a personal **library**: a private-to-write, public-to-browse folder tree (max <%= CoPlan::Folder::MAX_DEPTH %> levels deep). Folders are shelves in *your* library — filing a plan is a **placement** on your shelf, not a property of the plan, so the same plan can sit in many people's libraries at once. You can shelve anyone's published plan, not just your own. Tags remain the cross-cutting labels; use folders for "where do I keep this?" and tags for "what is it about?". - -**List folders** (defaults to your library; pass `?library_id=` to browse someone else's read-only): - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/folders" | jq . -``` - -Each folder includes `id`, `name`, `library_id`, `parent_id`, `path` (e.g. `"Team EBT/Q3"`), and `plans_count` (only plans you can see). - -**Create a folder** (always in your own library): - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"name": "Q3", "parent_id": "'$PARENT_FOLDER_ID'"}' \ - "<%= @base %>/api/v1/folders" | jq . -``` +<%= @auth_instructions %> -`parent_id` is optional — omit it for a top-level folder. Rename with `PATCH /api/v1/folders/:id` (`{"name": "..."}`); delete with `DELETE /api/v1/folders/:id` (only empty folders, only in your own library). +## 1. Mint a session token -**File a plan at creation** (preferred — pass `folder_path` on `POST /api/v1/plans`, see [Create Plan](#create-plan)) so plans never sit unfiled. To move or file an existing plan: +Mint your own token at the start of each run. You declare who you are once, here. CoPlan then attributes every version, comment, and session to this token, and the token gets its own event inbox. ```bash -<%= @curl %> -X PATCH \ +SESSION=$(<%= @mint_curl %> -X POST \ -H "Content-Type: application/json" \ - -d '{"folder_path": "Team EBT/Q3"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID" | jq . -``` - -- `folder_path` finds or creates the whole hierarchy in your library — the easiest way to organize. -- Alternatively pass `folder_id` with an existing folder's ID from your library. -- Pass an empty `folder_id` (`{"folder_id": ""}`) to take a plan off your shelf. -- `folder_id`/`folder_path` in plan responses say where the plan lives — one answer, the same for everybody, `null` for a plan nobody has filed yet. Filing a plan that's already filed **moves** it; there's no second copy. - -**Guidelines:** -- Check `GET <%= @base %>/api/v1/library` (your library's map — folder tree with descriptions and counts) before creating new folders — reuse the existing structure. -- Folder names read like places (`Team EBT`, `Infra`, `Q3 Launch`), not labels. Folders carry a one-line `description` saying what belongs in them — read it before filing, write it when you create folders. - -**Organizing at scale — fetch only when needed.** When you're asked to organize, reorganize, or audit a library (yours or between libraries), there is a dedicated organization API: a one-call library overview, bulk contents with summaries and tags, batch move/rename/describe operations with `dry_run` for proposing changes, move-by-tag, cross-library moves, and a full audit log. Its guide lives at: - + -d '{"agent_name": "Claude", "metadata": {"harness": "claude-code", "harness_version": "2.1.3", "model": "claude-fable-5"}}' \ + "<%= @base %>/api/v1/tokens" | jq -er '.token // error(.error // "mint failed")') && TOKEN=$SESSION ``` -GET <%= @base %>/agent-instructions/organizing -``` - -Don't fetch it for a single filing (`folder_path` above covers that) — fetch it once when a real organizing task starts. - -### Visibility & Archiving - -Plans have two orthogonal state fields — there is no multi-step lifecycle to maintain: - -| Field | Values | Meaning | -|-------|--------|---------| -| `visibility` | `draft` / `published` | `draft` renders as **"private"** in the UI and is **unlisted, not locked**: anyone with the plan's URL/id can read it, but it never appears in anyone else's lists, search, or shelves. Published plans are listed for the whole org. | -| `archived` | `true` / `false` | Archived plans stay readable at their URL but disappear from lists and search unless explicitly filtered for. | - -Rules that matter for agents: - -- **New plans are shared (org-listed) from the moment they're created.** CoPlan is a sharing platform — there are very few good reasons to keep a plan private. Pass `"visibility": "draft"` on create **only when the user explicitly asks** for a private/unshared plan (still readable by link); share it later with `PATCH {"visibility": "published"}`. Sharing is one-way (a shared plan cannot go private again; archive it instead — archiving is also the right tool for work that didn't pan out). -- Archive (`{"archived": true}`) anything that is dead, superseded, or was created by mistake — including your own duplicates. Unarchive with `{"archived": false}`. -- **Never create a new plan to "replace" one you can edit.** Plans have full version history; edit the existing plan (see Editing Plans). If a plan genuinely supersedes another, add a reference between them and archive the old one. -- If a plan has a real rollout lifecycle worth tracking, use tags (e.g. `developing`, `live`) — status fields for that no longer exist. - -The API also accepts the legacy `status` field (`brainstorm`/`considering`/`developing`/`live`/`abandoned`) for older integrations, mapping it onto visibility/archived. New integrations should use `visibility` and `archived`. - -### Plan Types - -Plan types categorize plans, apply default tags, and carry a **content template** — the document structure readers of that type expect. Every plan has exactly one type. -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plan_types" | jq . -``` - -Returns every type with `name`, `description`, `default_tags`, and `template_content`. - -**Guidelines:** -- Pick the **most specific** type that fits before creating a plan. Plans created without an explicit type get **General** — acceptable only when you've reviewed the list and nothing fits. -- **Read the type's `template_content` and structure your document against it** — keep its sections, fill them with substance, drop a section only when it's genuinely inapplicable. The template is the type's contract with its readers. -- The type's `default_tags` are applied automatically on create; add your own on top with `tags`. -- **Retype a plan when it outgrows its type** — a brainstorm that firmed up into a real design, say — with `PATCH /api/v1/plans/:id` and `{"plan_type": "..."}`. Restructure the content against the new type's template in the same session. -<% if @plan_types.any? %> - -**Available plan types:** - -| Name | Description | Template | -|------|-------------|----------| -<% @plan_types.each do |pt| %> -| `<%= pt.name %>` | <%= pt.description.present? ? pt.description : "—" %> | <%= pt.template_content.present? ? "yes — fetch and follow it" : "—" %> | -<% end %> +<% if @request_auth_available %> +Your host authenticates this request, so you do not need a token from Settings. The command **puts the new session token in `$TOKEN`**. Every later call in these instructions uses it. If minting fails, the command prints the error and leaves `$TOKEN` unchanged. <% else %> - -No plan types are currently configured. +This command sends the long-lived token and **replaces `$TOKEN` with the new session token**. Every later call in these instructions uses the session token. Do not send the long-lived token again. If minting fails, the command prints the error and leaves `$TOKEN` unchanged. <% end %> -### References - -References are the structured, document-level inventory of resources connected to a plan (repos, PRs, docs, links, and other plans). They power discovery and enrich reader-facing citations with the resource's title, type, and domain; they are not a substitute for placing a citation or link at the claim it supports. Readers see citation context and all remaining linked or explicitly curated resources together in one **References** section. - -Links anywhere in plan content—including links inside footnote definitions—are **auto-extracted** as references. Do not add them again. Add explicit references via the API only for relevant resources that are not linked in the prose. - -**Add references on create or update:** - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"title": "My Plan", "content": "...", "references": [{"url": "https://github.com/org/repo", "key": "main-repo", "title": "Main Repo"}, {"url": "https://github.com/org/repo/pull/42", "key": "impl-pr", "title": "Implementation PR"}]}' \ - "<%= @base %>/api/v1/plans" | jq . -``` - -Each reference object takes: `url` (required), `key` (optional — a short identifier like `auth-repo` or `impl-pr`), `title` (optional). The `reference_type` is auto-classified from the URL (repository, pull_request, plan, document, link). - -Keys let you give references semantic meaning that persists across edits. Use lowercase alphanumeric characters, hyphens, and underscores (e.g., `auth-service`, `main_repo`, `impl-pr`). Keys must be unique per plan. You can also define keys using markdown reference-style links in your content: +- `agent_name` is a short label for this run, such as "Claude" or "Amp". CoPlan truncates it after 20 characters. +- `metadata` is optional. Set `harness` (for example `amp`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `goose`, or `opencode`), `harness_version`, and `model`. People see the harness on your comments and in the history. +- Do not send `agent_name` or other attribution fields on later calls, and do not sign your comments or edits. The token supplies your identity. +- The response shows `token` only once. Keep it in `$TOKEN` for the whole run. +- Mint one token per run and reuse it. Each new token is a new identity. +- Tokens expire after 7 days by default. To change this, set `ttl_seconds` (maximum 30 days). If a later call returns `401`, <% if @request_auth_available %>run the mint command again<% else %>load the long-lived token into `$TOKEN` again and mint a new session token<% end %>. A session token cannot mint more tokens. +- When you finish, revoke the session token: `<%= @curl %> -X DELETE "<%= @base %>/api/v1/tokens/current"`. -``` -See the [auth-repo] for implementation details. - -[auth-repo]: https://github.com/org/auth-service "Auth Service" -``` - -These are auto-extracted with both the key and title preserved. - -**List references for a plan:** - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans/$PLAN_ID/references" | jq . -``` - -**Add a reference to an existing plan:** - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"url": "https://github.com/org/repo", "key": "service-repo", "title": "Service Repo"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/references" | jq . -``` - -**Find plans that reference a URL:** - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/references/search?url=https://github.com/org/repo" | jq . -``` - -**Guidelines:** -- **Always set a `key`** when adding explicit references — it gives the reference a stable semantic identity (e.g., `auth-repo`, `impl-pr`, `design-doc`). -- Add references to any GitHub repos, PRs, design docs, or related plans mentioned in your plan. -- Links in plan content are auto-extracted — you don't need to add those manually. -- Use explicit references for resources not linked in the content but still relevant. -- Reference titles should be human-readable (e.g., "Auth Service Repo", not the raw URL). -- Use markdown reference-style links (`[key]: url "title"`) in content to auto-extract keyed references. - -### Attachments - -Plans can carry file attachments (diagrams, screenshots, PDFs, data files). Limits: **25 MB per file**; allowed types are images (png/jpg/gif/webp), pdf, txt/md/csv/json, and zip. Only the plan author can upload or delete; anyone who can see the plan can list and download. - -**Upload a file (multipart form):** +## 2. Read a plan ```bash -<%= @curl %> -X POST \ - -F "file=@./architecture-diagram.png" \ - "<%= @base %>/api/v1/plans/$PLAN_ID/attachments" | jq . -``` - -Returns `201 Created` with `id`, `filename`, `content_type`, `byte_size`, `url` (permanent path), `download_url`, and a ready-to-paste `markdown` snippet. Returns `422` with an error message if the file is too large or its content type is not allowed. - -**Embed in the plan:** paste the returned `markdown` snippet (e.g. `![diagram.png](/rails/active_storage/blobs/redirect/...)`) into the plan content — image attachments render inline in the plan body. - -**List attachments:** - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans/$PLAN_ID/attachments" | jq . -``` - -**Delete an attachment:** - -```bash -<%= @curl %> -X DELETE \ - "<%= @base %>/api/v1/plans/$PLAN_ID/attachments/$ATTACHMENT_ID" +<%= @curl %> "<%= @base %>/api/v1/plans/$PLAN_ID/snapshot" | jq . ``` -### Get Versions - -```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans/$PLAN_ID/versions" | jq . -``` +One call returns `current_content` (Markdown), `current_revision`, `url`, `comment_threads`, `references`, and `collaborators`. The read also records that you saw this revision. You need that record to edit (step 3). -### Get Comments +API paths use the plan `id`. If you have a plan URL, find the `id` in the list of plans you can see: ```bash -<%= @curl %> \ - "<%= @base %>/api/v1/plans/$PLAN_ID/comments" | jq . +<%= @curl %> "<%= @base %>/api/v1/plans" | jq -r --arg url "$PLAN_URL" '.[] | select(.url == $url) | .id' ``` -## Editing Plans (Recommended: Full Content Replacement) +The list does not include archived plans unless you add `?archived=true`. A private plan is listed only for its author. -The simplest and recommended way to edit a plan is to read its current content, edit the markdown locally, and PUT the entire updated content back. The server diffs your version against the current revision, decomposes the change into granular operations, and creates a new immutable PlanVersion. Comment anchors in unchanged regions are preserved automatically. +## 3. Edit a plan -**No lease required.** Optimistic concurrency is enforced via `base_revision`. - -### Replace Plan Content +Edit the full document in a local file, then send the whole document back. CoPlan finds what changed, keeps comments anchored to text you did not change, and records a new version. ```bash -# 1. Read the current plan to get its content and revision -PLAN_DATA=$(<%= @curl %> "<%= @base %>/api/v1/plans/$PLAN_ID") -CURRENT_REVISION=$(echo "$PLAN_DATA" | jq -r '.current_revision') -echo "$PLAN_DATA" | jq -r '.current_content' > /tmp/plan.md - -# 2. Edit /tmp/plan.md locally however you like - -# 3. PUT the new content back <%= @curl %> -X PUT \ -H "Content-Type: application/json" \ - -d "$(jq -n \ - --arg content "$(cat /tmp/plan.md)" \ - --argjson rev "$CURRENT_REVISION" \ - --arg summary "Rewrote goals section" \ - '{base_revision: $rev, content: $content, change_summary: $summary}')" \ + -d "$(jq -n --rawfile content plan.md --argjson rev "$REVISION" \ + '{content: $content, base_revision: $rev, change_summary: "Moved launch to Q3 per Sam"}')" \ "<%= @base %>/api/v1/plans/$PLAN_ID/content" | jq . ``` -Returns: -- `201 Created` with `{revision, content_sha256, applied, version_id}` on success -- `200 OK` with `{no_op: true}` if the content is unchanged -- `409 Conflict` with `{current_revision}` if `base_revision` is stale (re-read the plan and retry) -- `409 Conflict` with `{"code": "human_edit_pending"}` if a person edited by hand since you last read the plan (see below) -- `422 Unprocessable Content` if `content` or `base_revision` is missing or invalid - -This is the right tool for any non-trivial edit: rewriting a section, adding multiple paragraphs, restructuring headings, applying many small fixes at once, or doing a wholesale rewrite. - -### When a person has edited by hand - -A direct human edit is the highest-authority input a plan gets. Agent-vs-agent races get merged for you; a human's edit does not — **every agent write is refused until you have pulled their words**: - -```json -{ - "error": "Blocked: Sam edited this plan by hand (an edit, now at v7) and you last read v5. A person's direct edit outranks an agent's — it is not something to merge past. Read the current content, keep their changes, fold your own work in around them, then write again.", - "code": "human_edit_pending", - "current_revision": 7, - "last_human_revision": 7, - "last_seen_revision": 5, - "human_edits": [ - { - "revision": 7, - "editor": "Sam", - "edited_at": "2026-08-24T17:02:11Z", - "change_summary": "Edited in web UI", - "diff": "@@ -12,3 +12,3 @@\n-Ship behind a flag in Q4.\n+Ship behind a flag in Q3 — legal signed off on the earlier date.\n" - } - ], - "resolve": "GET /api/v1/plans/:id/snapshot to pull the current content …" -} -``` - -What to do: +- `base_revision` is the `current_revision` you read. +- `change_summary` is the version label in the plan history. Say what changed and why. Do not write "Update". +- **`409` with `current_revision`**: someone else wrote after your read. Read the plan again, apply your change to the new content, and send it with the new revision. +- **`409` with `"code": "human_edit_pending"`**: a person edited the plan by hand. The response includes their diff. Read the plan again, keep their text, and make your change on top of it. If you send your old content again, you delete their edit. +- **`409` with `"code": "edit_locked"`**: a person has the plan open in the editor. Wait a minute, read the plan again, and retry. Their lock expires 5 minutes after they stop. +- Read the plan again before you start each editing task. Do not edit from a copy that you read for an earlier task. -1. **Read the diff in the response.** It is the person's own wording, and it is what they meant. -2. **`GET .../snapshot`** to pull the current content. That read is what lifts the block — the server tracks which revision your credential has actually fetched, so bumping `base_revision` to the number in the error does *not* get you through. -3. **Rebuild your edit on top of theirs.** Do not re-send the body you already had; it predates their change and re-sending it deletes their words. -4. If your instructions and their edit genuinely conflict, **leave their text alone and raise it in a comment** (`POST /api/v1/plans/:id/comments`) rather than overwriting a decision a person made deliberately. +You can edit any plan that you can read. Edit another person's plan only when your principal or the plan's author asks you to. Otherwise, comment. -This applies to every agent write path — content replacement, operations, and session commits. It does not apply to a plan nobody has hand-edited. +Conflicts, targeted edits, and title, tag, or type changes: <<%= @base %>/agent-instructions/editing> -## Editing Plans (Advanced: Lease + Operations) +## 4. Answer comments -For surgical, targeted edits where you want to express the change as a single operation (and keep the diff minimal), use the lease + operations path. Editing requires three steps: acquire lease → apply operations → release lease. +Threads appear in the snapshot's `comment_threads`. A thread is `open` until someone resolves it. Each thread has `anchor_text`, which is the passage the comment is about, and its `comments`. -### 1. Acquire Edit Lease +- **A comment from your principal**: reply, make the change, then resolve the thread. +- **A comment from another person**: reply with the change that you propose. Edit only after your principal agrees. Live-session events tell you which type of comment it is. If you are not sure, ask your principal. -```bash -LEASE_RESPONSE=$(<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"lease_token": "'$(openssl rand -hex 32)'"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/lease") -echo "$LEASE_RESPONSE" | jq . -LEASE_TOKEN=$(echo "$LEASE_RESPONSE" | jq -r '.lease_token') -``` - -Leases expire after 5 minutes. Renew with PATCH, release with DELETE. - -### 2. Apply Operations - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{ - "lease_token": "'$LEASE_TOKEN'", - "base_revision": 1, - "change_summary": "Updated goals section", - "operations": [ - { - "op": "replace_exact", - "old_text": "old text here", - "new_text": "new text here", - "count": 1 - } - ] - }' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/operations" | jq . -``` - -**Important:** Set `base_revision` to the plan's `current_revision`. Returns 409 if stale. - -### 3. Release Edit Lease - -```bash -<%= @curl %> -X DELETE \ - -H "Content-Type: application/json" \ - -d '{"lease_token": "'$LEASE_TOKEN'"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/lease" -``` - -### Renew Lease (if needed for long edits) - -```bash -<%= @curl %> -X PATCH \ - -H "Content-Type: application/json" \ - -d '{"lease_token": "'$LEASE_TOKEN'"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/lease" | jq . -``` - -## Available Operations - -### replace_exact - -Find and replace exact text. Fails if text not found or count exceeded. - -```json -{ - "op": "replace_exact", - "old_text": "We should use MySQL", - "new_text": "We should use PostgreSQL", - "count": 1 -} -``` - -### insert_under_heading - -Insert content after a markdown heading. Fails if heading not found or ambiguous. - -```json -{ - "op": "insert_under_heading", - "heading": "## Testing Strategy", - "content": "- Add integration tests\n- Mock external providers" -} -``` - -### delete_paragraph_containing - -Delete the paragraph containing a needle string. Fails if 0 or >1 paragraphs match. - -```json -{ - "op": "delete_paragraph_containing", - "needle": "This approach is deprecated" -} -``` - -### replace_section - -Replace an entire section — a heading plus everything under it, up to the next -heading of equal or higher level. Use this to scope a rewrite to one part of the -document when the same text appears elsewhere. Fails if the heading is not found -or matches more than one heading. - -```json -{ - "op": "replace_section", - "heading": "## Testing Strategy", - "new_content": "## Testing Strategy\n\nUnit tests only for v1." -} -``` - -- `heading`: the full heading line, including the `#` markers. -- `new_content`: replaces the whole section. With `"include_heading": false`, - only the section body is replaced and the heading line is kept. - -## Commenting - -### Create Comment Thread +Reply before you edit, so that the reviewer knows what you will change: ```bash <%= @curl %> -X POST \ -H "Content-Type: application/json" \ - -d '{ - "body_markdown": "This section needs more detail.", - "anchor_text": "the exact text you are commenting on" - }' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/comments" | jq . -``` - -- `anchor_text`: the exact text from the plan content that this comment is about. It will be highlighted in the UI and the comment will be anchored to it. -- Omit `anchor_text` for a general (non-anchored) comment. -- Do not send attribution fields. CoPlan reads the agent harness, session label, and human owner from your Bearer token. - -### Reply to Thread - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"body_markdown": "Good point, I will address this."}' \ + -d '{"body_markdown": "Moving the launch to Q3 and noting the legal sign-off."}' \ "<%= @base %>/api/v1/plans/$PLAN_ID/comments/$THREAD_ID/reply" | jq . ``` -### Resolve Thread - -Mark a comment thread as resolved (addressed by the plan author or thread creator). +Resolve the thread after your edit is saved. Do not resolve threads that you did not address. You can resolve a thread if your principal wrote the plan or started the thread. ```bash -<%= @curl %> -X PATCH \ - "<%= @base %>/api/v1/plans/$PLAN_ID/comments/$THREAD_ID/resolve" | jq . +<%= @curl %> -X PATCH "<%= @base %>/api/v1/plans/$PLAN_ID/comments/$THREAD_ID/resolve" | jq . ``` -### Get a Single Thread - -Fetch one thread with its comments — handy when reacting to a single event without refetching the whole snapshot. - -```bash -<%= @curl %> "<%= @base %>/api/v1/plans/$PLAN_ID/comments/$THREAD_ID" | jq . -``` +Your own comments, mentions, table and diagram comments, and reviewing a plan: <<%= @base %>/agent-instructions/comments> -## Live Collaboration (Realtime) +## 5. Create a plan -When you create, edit, review, or babysit a plan, attach if your harness has a verified wake path. This lets you hear about new comments and edits the moment they happen instead of re-polling snapshots, and shows humans that you're available. Complete both steps below: claiming without a working event-delivery path is not attachment. Never substitute an unbounded foreground wait for a wake path. +If a plan already exists for the work, edit that plan. Do not create a replacement. -### 1. Claim an Agent Session +Before you create a plan, make two reads: -Claiming a session subscribes your token to the plan's events and shows a live presence pill to everyone viewing the plan. A plain claim reads as presence, not work — the pill is just your name (`watching`). Claim with `"state": "active"` when you're already mid-task. Turn states can't be claimed on arrival: reach `awaiting_input` and `complete` via PATCH once you're actually in the loop. +1. `GET <%= @base %>/api/v1/library` returns your principal's folders. Each folder has a description of what belongs in it. Choose a folder and send its path as `folder_path`. If you do not send a folder, someone has to file the plan later. +2. `GET <%= @base %>/api/v1/plan_types` returns the plan types. Choose the most specific type. Each type has a `template_content`. Use the template sections as the structure of your document. Use `General` only when no other type fits. ```bash <%= @curl %> -X POST \ -H "Content-Type: application/json" \ - -d '{}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . -``` - -Keep the pill honest by updating your state as you work (`state`: `active`, `awaiting_input`, `complete`; optional `detail` shows what you're doing). `pending` is the server's: it's set when an event lands in your inbox and holds for 30 seconds before going stale. The pill only says "Waking you…" once you've answered a wake before — until your first reaction, wakes are quietly a test. - -**When answering a wake, PATCH — don't re-claim.** Your session already exists; PATCH `{"state": "active"}` is the ack that counts as answering. A fresh POST claim resets the session instead of answering it, so the wake goes down as unanswered and the pill stops trusting you. - -**Hosted agent with no connection to hold?** Claim with a `wake_url` and CoPlan will POST a signed ping there on every event instead: - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"wake_url": "https://your-platform.example/hooks/coplan"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . -``` - -The response includes `wake_secret` — once, at registration; store it. Each ping is a small JSON body (`event_id`, `event_type`, `plan_id`) signed with `X-CoPlan-Signature: sha256=`; verify it, dedupe on `event_id`, then pull your inbox below as usual. Pings fire even when your session is `complete` — waking you back up is the point. Re-claim with `"wake_url": ""` to unregister. Keep the endpoint answering: a URL that keeps failing entire retry runs is presumed dead and unregistered automatically (you'd re-register on your next claim). The URL must also be reachable from the server — hosts that resolve to private or loopback addresses are refused. - -```bash -<%= @curl %> -X PATCH \ - -H "Content-Type: application/json" \ - -d '{"state": "active", "detail": "tightening the Rollout section"}' \ - "<%= @base %>/api/v1/plans/$PLAN_ID/agent_session" | jq . -``` - -Etiquette (this is what makes you feel like a collaborator, not a batch job): - -- Flip to `active` within a few seconds of waking — the fast ack humans see. Work as slowly as you need after that; if you stay silent in `pending` the pill goes stale. -- Post a short reply narrating what you're about to change *before* you PUT the edit. -- Use `awaiting_input` when a question blocks you; land on `complete` when your turn is done. -- `DELETE` the session when you stop watching the plan. - -### 2. Wait for Events - -While you're attached, you don't poll on an interval — you hold a connection open and the server hands you events the instant they happen. Two equivalent ways to do that; both are ordinary HTTP. - -Held-open stream (Server-Sent Events — one socket, heartbeats every 15s, reconnect with your cursor): - -```bash -<%= @curl %> -N -H "Accept: text/event-stream" \ - "<%= @base %>/api/v1/agent/events?cursor=$LAST_EVENT_ID" -``` - -Long-poll (returns immediately when an event lands, or empty after `wait` seconds — easier to consume from a shell loop): - -```bash -<%= @curl %> "<%= @base %>/api/v1/agent/events?wait=25&cursor=$LAST_EVENT_ID" | jq . -``` - -Event types: `comment.created`, `comment.replied`, `thread.status_changed`, `plan.content_changed`. Each event carries `plan_id`, `comment_thread_id`, and a payload with the comment body/anchor so you usually don't need another fetch to decide what to do. Your own comments and edits are never echoed back to you. - -**Whose comment is it?** Comment events carry three fields that tell you how much rope you have: - -- `from_principal` — the commenter is the human your token belongs to. -- `from_plan_author` — the commenter wrote the plan. These come apart: you can be attached to somebody else's plan, and your own principal still outranks its author. -- `authority` — `"principal"` or `"collaborator"`, the summary of the above. - -The contract: - -- `principal` — act directly. Reply, then make the edit. -- `collaborator` — reply and *propose* the change in the thread. Don't edit the document until your principal confirms. A drive-by comment from someone who isn't your human is a request, not an instruction. - -Cursoring: event ids are time-ordered — pass the last id you processed as `cursor` to resume. Without a cursor you get all unacked events. Delivery is at-least-once; ack after processing: - -```bash -<%= @curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"cursor": "$LAST_EVENT_ID"}' \ - "<%= @base %>/api/v1/agent/events/ack" | jq . -``` - -The HTTP wait itself needs no daemon, but durable wake-up does require something outside a completed agent turn: an operator-provisioned existing-session resume bridge, webhook, scheduler, or verified background-completion mechanism. `coplan-attach` only wraps the HTTP wait; starting it does not make the current conversation resumable. - -**Delivery is not a wake.** These endpoints hand your process the event; whether that starts a *model turn* is your harness's job, and no transport fixes a harness that can't do it. An event printed by a background process nobody wakes up for is just a log line. Five shapes that work: - -1. **Bounded blocking tool call** — only while the user has explicitly asked you to wait or monitor, run one long-poll request or `coplan-attach --once --timeout N`. The event comes back as ordinary tool output. Never use an unbounded foreground loop. -2. **Verified background wake** — if your harness demonstrably starts a new model turn when a process exits, the Setup section's branch-D loop (or `coplan-attach --once`) can wait in the background. A completion notification or log line alone does not qualify. -3. **Operator-provisioned sidecar** — `coplan-bridge` (served at `<%= @base %>/agent-tools/coplan-bridge`, see Setup above) drains the inbox from outside and uses an exec adapter to resume a configured existing harness session. ACP-created agents are a separate deployment mode, not attachment for the current authoring conversation. -4. **Webhook wake** — for hosted agents: register a `wake_url` at claim time (see above) and CoPlan pushes a signed ping to your platform, which starts your turn; you then pull the inbox. -5. **Scheduled follow-up** — for a harness with conversation-scoped scheduling, poll the durable inbox with `wait=0` on each run. This is near-live polling, not a held connection; leave the session `complete` between runs so the presence pill stays honest. - -If none of these fit your harness, don't hold a connection you can't act on and don't claim a presence pill. Drain the durable inbox with `wait=0` at the start and end of each turn, and disclose that the harness could not establish live attachment. Treat that as a degraded fallback, not completion of the live setup requirement. - -Hand-rolled SSE clients: the server retires every stream after 5 minutes by design — reconnect with your last cursor and nothing is missed. (`coplan-attach` does this for you.) - -### Session Tokens - -Your token *is* your subscription: sessions, inboxes, and pills are all keyed to it. Two agents sharing one token share an inbox and race for each other's events, so give each agent run its own token. - -<% if @request_auth_available %> -Mint a new token through your host's request authentication, as in the first example above. Do not use a session token to mint another token: -<% else %> -Mint each run's token from your long-lived root token, so the machine only ever stores one secret: -<% end %> - -```bash -<%= @mint_curl %> -X POST \ - -H "Content-Type: application/json" \ - -d '{"agent_name": "Claude (refactor)", "ttl_seconds": 43200}' \ - "<%= @base %>/api/v1/tokens" | jq -r .token -``` - -The minted token belongs to your principal (it can never act for a different human), expires on its own (7 day default, 30 day max), and cannot mint further tokens. <% unless @request_auth_available %>Revoking the root token also revokes every token it minted. <% end %>Clean up when you exit: - -```bash -<%= @curl %> -X DELETE "<%= @base %>/api/v1/tokens/current" | jq . -``` - -Mint once at the start of your session and reuse it — minting per turn would give you a new identity, and a new presence pill, every time. - -### 3. The Loop - -```bash -CURSOR="" -while true; do - RESP=$(<%= @curl %> "<%= @base %>/api/v1/agent/events?wait=25&cursor=$CURSOR") - echo "$RESP" | jq -c '.events[]' | while read -r event; do - # 1. PATCH agent_session state=active (fast ack — do this first) - # 2. Read the event payload / thread, decide, reply, edit - # 3. PATCH agent_session state=complete - : - done - NEXT=$(echo "$RESP" | jq -r '.cursor // empty') - [ -n "$NEXT" ] && CURSOR="$NEXT" && <%= @curl %> -X POST -H "Content-Type: application/json" \ - -d "{\"cursor\": \"$CURSOR\"}" "<%= @base %>/api/v1/agent/events/ack" > /dev/null -done -``` - -## Reviewing a Plan - -When asked to review a plan (given a plan URL or ID), follow this workflow: - -### 1. Read the Plan Snapshot - -```bash -<%= @curl %> "<%= @base %>/api/v1/plans/$PLAN_ID/snapshot" | jq . + -d '<%= raw @create_example_json %>' \ + "<%= @base %>/api/v1/plans" | jq . ``` -This returns the plan content, all comment threads with comments, references, and collaborators in one call. - -### 2. Triage Comments +New plans are shared with the whole organization. The response includes the plan's `url`. Give it to your principal. -Review each open comment thread and categorize it: +Plan types, folders, tags, references, and private plans: <<%= @base %>/agent-instructions/creating> -- **Just do it** — Clear, actionable feedback that can be applied directly. Apply these edits without asking. -- **Confirm first** — Substantive changes where the right fix is clear but the scope is large. Summarize and ask before applying. -- **Discuss** — Ambiguous feedback or design decisions. Present to the user for discussion. +## 6. Stay for feedback -### 3. Apply Edits +After you create, edit, or review a plan, people may comment while you work. If your environment can start a new turn for you when an event arrives, set up a live session: claim the plan, and connect the event inbox to that wake. Fetch the guide for your environment from [Live sessions](#live-sessions). -For approved changes, the recommended path is: read the snapshot → edit the markdown locally → `PUT /api/v1/plans/:id/content` → resolve the addressed threads. Use the lease + operations path only for surgical, single-operation edits. +- Show yourself as present only if something can wake you. A presence pill with nobody behind it misleads the people who read the plan. +- Do not run a wait in the foreground unless your principal asked you to watch the plan. When you do, give it a time limit. +- **If your environment cannot wake you**, subscribe without a pill: claim the session, then PATCH its state to `complete`. Events still go to your inbox. Read it at the start and end of each turn with `GET <%= @base %>/api/v1/agent/events?wait=0`, and tell your principal that you cannot watch the plan live. -## Typical Workflow +## Writing rules -### Creating a plan +These rules apply to everything you write. A plan type's description or template can override them. If they conflict, the type wins. -1. **Pick the folder**: `GET /api/v1/library` — match the new document to a folder's description -2. **Pick the type and read its template**: `GET /api/v1/plan_types` — most specific type wins; structure your draft against its `template_content` -3. **Create filed and typed**: `POST /api/v1/plans` with `{title, content, plan_type, folder_path}` +- **Do not write metadata in the body.** CoPlan records it. Do not add dates, authors, status lines, version numbers, or changelogs. A date that supports a fact is content and is allowed: "Confirmed against production on 2026-08-19." +- **Write plainly.** Use short sentences, active voice, and plain verbs: "use", not "leverage"; "check", not "perform validation of". +- **Do not sell.** Do not use words such as "robust", "seamless", "comprehensive", "elegant", "crucially", or "delve". Write like a runbook, not a keynote. +- **Use structure.** Use a list or a table when the content is a list or has rows. +- **Be brief.** Write the shortest document that does the job. -### Recommended (full content replacement) +## Guides -1. **Read** the plan: `GET /api/v1/plans/:id/snapshot` -2. **Edit** the markdown locally -3. **Replace** content: `PUT /api/v1/plans/:id/content` with `{base_revision, content, change_summary}` -4. **Comment** on changes: `POST /api/v1/plans/:id/comments` +Fetch a guide when your task needs it. Each guide is Markdown, like this page. -### Advanced (surgical operations) +| Task | Guide | +|---|---| +| Diagrams, tables, code blocks, footnotes, section links, task lists, images | <<%= @base %>/agent-instructions/markdown> | +| Slide decks inside a plan (`::: {.presentation}` regions) | <<%= @base %>/agent-instructions/presentations> | +| Plan types, folders, tags, references, visibility, archiving | <<%= @base %>/agent-instructions/creating> | +| Conflicts, human edits, targeted edits, title, tag, and type changes, version history | <<%= @base %>/agent-instructions/editing> | +| Comments, mentions, table and diagram comments, reviewing a plan | <<%= @base %>/agent-instructions/comments> | +| Organizing or reorganizing a library | <<%= @base %>/agent-instructions/organizing> | +| Endpoint list and error codes | <<%= @base %>/agent-instructions/api> | -1. **Read** the plan: `GET /api/v1/plans/:id/snapshot` -2. **Acquire lease**: `POST /api/v1/plans/:id/lease` -3. **Apply operations**: `POST /api/v1/plans/:id/operations` (can call multiple times while lease is held) -4. **Release lease**: `DELETE /api/v1/plans/:id/lease` -5. **Comment** on changes: `POST /api/v1/plans/:id/comments` +### Live sessions -## Error Codes +In a live session, you get comments as soon as people post them, and the plan shows people that you are working. The hard part is the wake: something must start your next turn when an event arrives. Fetch the guide for your environment: -| Code | Meaning | -|------|---------| -| 401 | Not authenticated | -| 403 | Not authorized for this action | -| 404 | Plan not found (or no access) | -| 409 | Edit lease conflict, stale revision, or an unread human edit (`code: human_edit_pending`) | -| 422 | Validation error or operation failed | +| Environment | Guide | +|---|---| +| Claude Code | <<%= @base %>/agent-instructions/live/claude-code> | +| Amp | <<%= @base %>/agent-instructions/live/amp> | +| Codex | <<%= @base %>/agent-instructions/live/codex> | +| Hosted agent that can receive webhooks | <<%= @base %>/agent-instructions/live/hosted> | +| A bridge that your principal runs on an always-on machine | <<%= @base %>/agent-instructions/live/bridge> | +| Cursor, Goose, Gemini CLI, OpenCode, or another environment: the protocol | <<%= @base %>/agent-instructions/live> | + +An environment name is a hint, not proof. If your version cannot start a new turn when an event arrives, use the inbox fallback in step 6. diff --git a/engine/app/views/coplan/llms/show.text.erb b/engine/app/views/coplan/llms/show.text.erb index 9bc25061..0397e2c4 100644 --- a/engine/app/views/coplan/llms/show.text.erb +++ b/engine/app/views/coplan/llms/show.text.erb @@ -8,4 +8,5 @@ The editing model is simple: read a plan's content and revision, edit the Markdo ## API -- [Agent Instructions](<%= coplan.agent_instructions_path %>): Full API reference for AI agents, including authentication, endpoints, editing workflow, and commenting. +- [Agent Instructions](<%= coplan.agent_instructions_path %>): Start here. Authentication, reading, editing, comments, and creating plans, with links to topic guides. +- [API Reference](<%= coplan.agent_instructions_guide_path(guide: "api") %>): Every endpoint and error code. diff --git a/engine/config/routes.rb b/engine/config/routes.rb index a83a24ad..9dd1b2fd 100644 --- a/engine/config/routes.rb +++ b/engine/config/routes.rb @@ -209,9 +209,13 @@ # llms.txt, and in whatever config people have already pasted it into. # Same argument as the API above: an external contract stays where it was. get "agent-instructions", to: "agent_instructions#show", as: :agent_instructions - # Sub-instructions: the library-organizing guide, fetched on demand so the - # main instructions stay small (agents only spend context when organizing). - get "agent-instructions/organizing", to: "agent_instructions#organizing", as: :agent_instructions_organizing + # Topic guides, fetched on demand so the primer stays small (an agent + # only spends context on live sessions or slide decks when it needs + # them). The organizing name predates the glob; library API responses + # link it. + get "agent-instructions/organizing", to: "agent_instructions#guide", defaults: { guide: "organizing" }, + as: :agent_instructions_organizing + get "agent-instructions/*guide", to: "agent_instructions#guide", as: :agent_instructions_guide # Convention puts this at the root, like robots.txt. get "llms.txt", to: "llms#show", as: :llms_txt diff --git a/engine/lib/coplan/configuration.rb b/engine/lib/coplan/configuration.rb index ad9260ef..51b0e772 100644 --- a/engine/lib/coplan/configuration.rb +++ b/engine/lib/coplan/configuration.rb @@ -150,24 +150,19 @@ def initialize @agent_auth_instructions = <<~MARKDOWN ## Authentication - Credentials are stored at `~/.config/coplan/credentials.json`: + Your principal's long-lived API token is in `~/.config/coplan/credentials.json`: ```json { - "base_url": "BASE_URL", + "base_url": "https://coplan.example.com", "token": "your-token-here" } ``` - On first use: - - 1. Read `~/.config/coplan/credentials.json` to get `token` and `base_url`. - 2. If the file does not exist, tell the user: "Go to **Settings → API Tokens** in the CoPlan web UI to create a token." Ask for the token and base URL, then save to `~/.config/coplan/credentials.json` with `chmod 600`. - 3. If any API call returns 401, the token is invalid or revoked. Prompt the user to create a new token in Settings and update the credentials file. - - Use the values from the credentials file in all API calls below. - - All requests use `Authorization: Bearer $TOKEN` header. + - Load `token` into `$TOKEN`. Use it only to mint your session token (step 1). + - If the file does not exist, ask your principal to create a token at **Settings → API Tokens** in CoPlan. Save it and the CoPlan URL to the file, then run `chmod 600` on it. + - If minting returns `401`, this token is not valid. Ask your principal for a new one, and update the file. + - Keep the token only in the credentials file. Do not put it in commands, other files, or comments. Use the variable. MARKDOWN end diff --git a/spec/requests/agent_instructions_spec.rb b/spec/requests/agent_instructions_spec.rb index 8fe7a095..5b4fb88d 100644 --- a/spec/requests/agent_instructions_spec.rb +++ b/spec/requests/agent_instructions_spec.rb @@ -1,128 +1,76 @@ require "rails_helper" RSpec.describe "Agent Instructions", type: :request do - it "advertises the five-level cap in the organizing guide" do - get agent_instructions_organizing_path - expect(response).to have_http_status(:success) - expect(response.body).to include("at most 5 levels deep", "The depth cap is 5") - end - 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") - expect(response.body).to include("max 5 levels deep") - expect(response.body).to include("```mermaid") + expect(response.body).to include("# CoPlan for Agents") end - it "includes plan types when they exist" do - create(:plan_type, name: "Design Doc", description: "For design documents") - + it "covers the basics every agent needs, in order" do get agent_instructions_path + body = response.body - expect(response.body).to include("### Plan Types") - expect(response.body).to include("Design Doc") - expect(response.body).to include("For design documents") + steps = [ "## 1. Mint a session token", "## 2. Read a plan", "## 3. Edit a plan", + "## 4. Answer comments", "## 5. Create a plan" ] + positions = steps.map { |step| body.index(step) } + expect(positions).to all(be_present) + expect(positions).to eq(positions.sort) + expect(body).to include("/api/v1/tokens") + expect(body).to include("human_edit_pending") end - it "shows message when no plan types are configured" do + it "links every topic guide, and nothing it doesn't serve" do get agent_instructions_path - expect(response.body).to include("### Plan Types") - expect(response.body).to include("No plan types are currently configured") + linked = response.body.scan(%r{http://www\.example\.com/agent-instructions/([a-z/-]+)}).flatten.uniq + expect(linked).to match_array(CoPlan::AgentInstructionsController::GUIDES.keys) end - it "lists multiple plan types sorted by name" do - create(:plan_type, name: "RFC") - create(:plan_type, name: "Design Doc") - + it "keeps the long-lived token when minting fails" do 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 - - it "sets writing-style ground rules with a type-level override" do - get agent_instructions_path - - expect(response.body).to include("## Writing Style") - # Anti-metadata rules: the platform records dates/authors/status/versions. - expect(response.body).to include("The platform already records it.") - expect(response.body).to include("do not invent one") - # Plain-language rules with the override escape hatch. - expect(response.body).to include("the type wins") - expect(response.body).to include("Write like a runbook, not a keynote.") + expect(response.body).to include(%q{jq -er '.token // error(.error // "mint failed")') && TOKEN=$SESSION}) + expect(response.body).not_to include("jq -r .token)") end it "declares identity once through the session token" do get agent_instructions_path - expect(response.body).to include("Declare it here once; the token supplies it on every later call.") - expect(response.body).to include("Do not send attribution fields.") - expect(response.body).not_to include("per-request override of who is writing") - expect(response.body).not_to include('"body_markdown": "Good point, I will address this.", "agent_name"') + expect(response.body).to include("You declare who you are once, here.") + expect(response.body).to include("Do not send `agent_name` or other attribution fields on later calls") + expect(response.body).to include(%("harness": "claude-code")) end - it "uses request authentication for the first token mint when the host provides it" do - allow(CoPlan.configuration).to receive(:api_authenticate).and_return(->(_request) { nil }) - - get agent_instructions_path - - mint_example = response.body.split("```bash", 2).last.split("```", 2).first - expect(response.body).to include("you do not need a token from Settings") - expect(mint_example).to include("curl -s -X POST") - expect(mint_example).not_to include("Authorization: Bearer") - expect(response.body).to include('Authorization: Bearer $TOKEN') - expect(response.body).not_to include("minted from your long-lived one") - expect(response.body).to include("Do not use a session token to mint another token") - end - - it "uses the host's authenticated mint command when configured" do - allow(CoPlan.configuration).to receive(:api_authenticate).and_return(->(_request) { nil }) - allow(CoPlan.configuration).to receive(:agent_mint_curl_prefix).and_return("sq curl -s") - + it "only sets up live sessions where something can wake the agent" do get agent_instructions_path + body = response.body - expect(response.body.scan("sq curl -s -X POST").length).to eq(2) + expect(body).to include("## 6. Stay for feedback") + expect(body).to include("Show yourself as present only if something can wake you.") + expect(body).to include("Do not run a wait in the foreground unless your principal asked you to watch the plan.") + expect(body).to include("An environment name is a hint, not proof.") + expect(body).not_to include("coplan-bridge --acp") end - it "documents safe harness-specific live setup for plan-scoped work" do + it "sets writing-style ground rules with a type-level override" do get agent_instructions_path - expect(response.body).to include("## Live Setup: Choose a Wake Path Safely") - expect(response.body).to include("check whether your harness has a way to start another model turn") - expect(response.body).to include("Claiming a presence pill without running a wait, bridge, or webhook is not attachment") - expect(response.body).to include("Never run an unbounded wait in the foreground") - expect(response.body).to include("| Codex desktop |") - expect(response.body).to include("| Codex CLI / IDE |") - expect(response.body).to include("in-chat scheduled follow-up that drains `wait=0`") - expect(response.body).to include("`claude --resume -p `") - expect(response.body).to include("Starting an ACP server creates a different agent") - expect(response.body).to include("ACP is intentionally not an attachment path here") - expect(response.body).to include("ACP-created agents are a separate deployment mode") - expect(response.body).not_to include("coplan-bridge --acp") - expect(response.body).to include("Harness names are hints, not proof") - expect(response.body).to include("degraded fallback, not successful live setup") - expect(response.body).not_to include("optional-but-recommended upgrade") - expect(response.body).not_to include("Correct, just not live") + expect(response.body).to include("## Writing rules") + expect(response.body).to include("Do not write metadata in the body.") + expect(response.body).to include("the type wins") + expect(response.body).to include("Write like a runbook, not a keynote.") end it "walks agents through folder, type, and template before creating" do get agent_instructions_path - expect(response.body).to include("**Pick the folder.**") - expect(response.body).to include("**Pick the type.**") - expect(response.body).to include("template_content") + expect(response.body).to include("/api/v1/library") expect(response.body).to include("/api/v1/plan_types") + expect(response.body).to include("template_content") expect(response.body).to include('"folder_path"') - expect(response.body).to include("fallback of last resort") end it "uses a real configured type (not General) in the create example" do @@ -146,25 +94,34 @@ expect(response.body).to include(%q(Bob'\''s)) end - it "marks which plan types carry a template" do - create(:plan_type, name: "RFC", template_content: "# RFC") - create(:plan_type, name: "Bare", template_content: nil) + it "uses request authentication for the first token mint when the host provides it" do + allow(CoPlan.configuration).to receive(:api_authenticate).and_return(->(_request) { nil }) get agent_instructions_path - expect(response.body).to match(/`RFC`.*yes — fetch and follow it/) - expect(response.body).to match(/`Bare`.*\| —/) + mint_example = response.body[/^SESSION=\$\(.*?&& TOKEN=\$SESSION$/m] + expect(response.body).to include("you do not need a token from Settings") + expect(mint_example).to include("curl -s -X POST") + expect(mint_example).not_to include("Authorization: Bearer") + expect(response.body).to include("run the mint command again") + expect(response.body).not_to include("load the long-lived token") end - it "distinguishes citations, internal section links, and structured references" do + it "uses the host's authenticated mint command when configured" do + allow(CoPlan.configuration).to receive(:api_authenticate).and_return(->(_request) { nil }) + allow(CoPlan.configuration).to receive(:agent_mint_curl_prefix).and_return("sq curl -s") + get agent_instructions_path + expect(response.body).to include("SESSION=$(sq curl -s -X POST") - expect(response.body).to include("Citations and internal cross-references") - expect(response.body).to include("[§3.1](#section-3-1)") - expect(response.body).to include("structured, document-level inventory") - expect(response.body).to include("one **References** section") - expect(response.body).to include("source title, type, and domain") - expect(response.body).to include("click jumps to it") + get agent_instructions_guide_path(guide: "live/bridge") + expect(response.body).to include("COPLAN_TOKEN=$(sq curl -s -X POST \\\n -H \"Content-Type: application/json\"") + expect(response.body).not_to include("credentials.json") + end + + it "includes the host's auth instructions" do + get agent_instructions_path + expect(response.body).to include(CoPlan.configuration.agent_auth_instructions.lines.first.strip) end it "builds example URLs from the request base (root mount)" do @@ -179,6 +136,135 @@ # 404 on their first read. get agent_instructions_path, env: { "SCRIPT_NAME" => "/coplan" } expect(response.body).to include("http://www.example.com/coplan/api/v1/plans") + expect(response.body).to include("http://www.example.com/coplan/agent-instructions/editing") + end + end + + describe "GET /agent-instructions/:guide" do + CoPlan::AgentInstructionsController::GUIDES.each_key do |guide| + it "serves the #{guide} guide as markdown" do + get agent_instructions_guide_path(guide: guide) + + expect(response).to have_http_status(:success) + expect(response.content_type).to include("text/markdown") + expect(response.body).to start_with("# ") + expect(response.body).not_to include("http://www.example.com//") + end + end + + it "404s an unknown guide with a pointer back to the primer" do + get agent_instructions_guide_path(guide: "nope") + + expect(response).to have_http_status(:not_found) + expect(response.body).to include("http://www.example.com/agent-instructions") + end + + it "keeps the organizing guide at its published path" do + get agent_instructions_organizing_path + + expect(response).to have_http_status(:success) + expect(response.body).to include("# Organizing a Library") + expect(response.body).to include("up to #{CoPlan::Folder::MAX_DEPTH} levels deep") + end + + describe "comments" do + it "uses the open/resolved lifecycle" do + get agent_instructions_guide_path(guide: "comments") + body = response.body + + expect(body).to include("A thread is `open` until someone resolves it.") + expect(body).not_to match(/pending|todo|discard|dismiss/) + end + end + + describe "editing" do + it "explains the edit lock that blocks every write" do + get agent_instructions_guide_path(guide: "editing") + expect(response.body).to include(%("code": "edit_locked")) + expect(response.body).to include("including content replacement and operations") + end + end + + describe "creating" do + it "lists plan types with template markers" do + create(:plan_type, name: "RFC", description: "For proposals", template_content: "# RFC") + create(:plan_type, name: "Bare", template_content: nil) + + get agent_instructions_guide_path(guide: "creating") + body = response.body + + expect(body).to include("## Plan types") + expect(body).to match(/`RFC` \| For proposals \| yes — fetch and follow it/) + expect(body).to match(/`Bare` .*\| — \|/) + expect(body.index("`Bare`")).to be < body.index("`RFC`") + end + + it "advertises the configured folder depth" do + get agent_instructions_guide_path(guide: "creating") + expect(response.body).to include("up to #{CoPlan::Folder::MAX_DEPTH} levels deep") + end + + it "says so when no plan types are configured" do + get agent_instructions_guide_path(guide: "creating") + expect(response.body).to include("No plan types are currently configured") + end + end + + describe "markdown" do + it "covers diagrams, tables, and the three kinds of links" do + get agent_instructions_guide_path(guide: "markdown") + body = response.body + + expect(body).to include("```mermaid") + expect(body).to include("## Tables") + expect(body).to include("[§3.1](#section-3-1)") + expect(body).to include("[^queue-depth]") + expect(body).to include("Each selection is a hand edit.") + end + end + + describe "presentations" do + it "documents inline deck regions, not a deck plan type" do + get agent_instructions_guide_path(guide: "presentations") + body = response.body + + expect(body).to include("::: {.presentation #q3-review theme=\"coplan\"}") + expect(body).to include("Outside a region, `---` is a horizontal rule.") + expect(body).not_to include("behavior") + end + end + + describe "live" do + it "gives Amp a wake that works between turns" do + get agent_instructions_guide_path(guide: "live/amp") + expect(response.body).to include("coplan-bridge --adapter amp --session") + expect(response.body).to include("events?wait=0") + end + + it "gives Codex desktop a scheduled follow-up instead of a held connection" do + get agent_instructions_guide_path(guide: "live/codex") + expect(response.body).to include("scheduled follow-up") + expect(response.body).to include("Do not hold a request open between runs.") + end + + it "keeps ACP agents out of attachment" do + get agent_instructions_guide_path(guide: "live/bridge") + expect(response.body).to include("It is not the conversation that wrote the plan") + expect(response.body.index("--adapter claude")).to be < response.body.index("--acp") + end + + it "never sends per-request agent names" do + %w[live live/claude-code live/amp live/codex live/hosted].each do |guide| + get agent_instructions_guide_path(guide: guide) + expect(response.body).not_to include(%("agent_name":)), guide + end + end + + it "documents the authority contract" do + get agent_instructions_guide_path(guide: "live") + expect(response.body).to include(%(authority: "principal")) + expect(response.body).to include(%(authority: "collaborator")) + end end end @@ -192,7 +278,7 @@ expect(response).to have_http_status(:success) expect(response.content_type).to include("text/markdown") - expect(response.body).to include("# CoPlan API") + expect(response.body).to include("# CoPlan for Agents") expect(response.body).not_to include(" "text/markdown" } expect(response.content_type).to include("text/markdown") - expect(response.body).to include("# CoPlan API") + expect(response.body).to include("# CoPlan for Agents") end it "serves byte-identical markdown regardless of non-HTML Accept header" do @@ -216,7 +302,7 @@ end context "browsers (Accept header includes text/html)" do - it "redirects to the human setup page" do + it "redirects the primer to the human setup page" do get agent_instructions_path, headers: { "Accept" => browser_accept } expect(response).to redirect_to(agent_setup_path) @@ -232,7 +318,7 @@ get agent_instructions_path(format: :md), headers: { "Accept" => browser_accept } expect(response.content_type).to include("text/markdown") - expect(response.body).to include("# CoPlan API") + expect(response.body).to include("# CoPlan for Agents") expect(response.body).not_to include(" browser_accept } + + expect(response.content_type).to include("text/html") + expect(response.body).to include("Live Sessions in Claude Code — CoPlan") + expect(response.body).to include("markdown-rendered") + expect(response.body).to include(%(href="#{agent_instructions_reference_path}")) + expect(response.body).to include("/agent-instructions/live/claude-code.md") + expect(response.body).to include(%(data-coplan--clipboard-text-value="http://www.example.com/agent-instructions/live/claude-code")) + end + + it "renders the signed-in nav chrome on guide pages" do + user = create(:coplan_user, name: "Naveen Chrome") + sign_in_as(user) + + get agent_instructions_guide_path(guide: "editing"), headers: { "Accept" => browser_accept } + + expect(response.body).to include("Naveen Chrome") + end end end describe "GET /_/agent/instructions" do - it "shows the current instructions as a separate human-readable page" do + it "shows the primer as a separate human-readable page" do get agent_instructions_reference_path expect(response).to have_http_status(:ok) expect(response.content_type).to include("text/html") expect(response.body).to include("Agent instructions") - expect(response.body).to include("CoPlan API") + expect(response.body).to include("CoPlan for Agents") expect(response.body).to include(%(href="#{agent_setup_path}")) end diff --git a/spec/requests/agent_setup_spec.rb b/spec/requests/agent_setup_spec.rb index f97b50bb..09a69842 100644 --- a/spec/requests/agent_setup_spec.rb +++ b/spec/requests/agent_setup_spec.rb @@ -64,7 +64,7 @@ get link["href"], headers: { "Accept" => browser_accept } expect(response).to have_http_status(:ok) - expect(response.body).to include("CoPlan API") + expect(response.body).to include("CoPlan for Agents") end it "serves Markdown at .md even to browsers" do diff --git a/spec/system/agent_setup_spec.rb b/spec/system/agent_setup_spec.rb index 30f40da0..66992ef0 100644 --- a/spec/system/agent_setup_spec.rb +++ b/spec/system/agent_setup_spec.rb @@ -12,6 +12,6 @@ click_link "View the agent instructions" expect(page).to have_current_path(agent_instructions_reference_path) - expect(page).to have_content("CoPlan API") + expect(page).to have_content("CoPlan for Agents") end end