Move the API to /api/v1, answer /api/v3, and stop capping open threads

eb8ea8fe5161 · AtlantisPleb · · parent 183d69e53235

Move the API to /api/v1, answer /api/v3, and stop capping open threads

Three things: an in-flight rename that had nowhere to land, the reason a
session kept being told its thread was revoked, and a data-rights gap the
traces route opened on its way in.

## The rename

The API lived at `/api/v3` because it began by aiming at parity with GitHub's
v3 REST API — the number was GitHub's, not ours. It is `/api/v1` now (#212).

The retired prefix is answered rather than removed, by
`OpenAgentsWeb.Plugs.ApiV3Rewrite`, which rewrites the two leading path
segments before the router sees them. That matters more than the rename does:
`/api/v3` is in every installed CLI, in every script written against it, and in
the `openagents` binary people already have. A rename without the alias breaks
all of them at the moment of a deploy, with nothing to roll forward to. One
rewrite rather than a second copy of twenty scopes, because a duplicated scope
list drifts — the next route added under `/api/v1` would silently not exist
under `/api/v3`. #216 removes it once the fleet is on 0.4.0.

`POST /traces` landed at `/api/v3` while this was in flight and moves with
everything else; the alias answers the prefix it was written against.

## The revoked thread

"This thread was revoked. Start a new session to open another." A session
revoked its own thread when it exited, which had two consequences nobody asked
for: `--resume` was impossible by construction, because every past session's
thread was already terminal; and anything that touched a thread again — a
delegated child still running, a second window, a resumed session — was told it
had been revoked. The client half is in the CLI's own commit.

The cap goes with it. `maximum_open_threads_per_account` was 8, sized against
sessions that cleaned up after themselves. With threads durable, a count of
open threads is a count of every session the account has ever run, so the ninth
would have been refused for eight finished threads that were only still open
because nothing had said so. `nil` is unbounded, and what bounds an account is
its credit — the same answer as the grant ceilings and the grant clock.

## The export gap

`POST /api/v1/traces` accepts an ATIF trace and no route reads one back, not by
id and not for the owner. That is a record a person authored, held on their
behalf, with no way to take it with them, and it was published as an API family
the export ledger did not classify — so the export question never reached it.

It is recorded blocked against #217, which owns that surface. Two contracts
then asked for a decision rather than letting it through: a blocked family must
be probed rather than asserted from prose, and the gap it puts on the anonymous
`/status` republishes three key paths STATUS-001 had deliberately left
undeclared while the list was empty. Both are now declared. Publishing the gap
is the point of the disclosure — a gap nobody outside can see is one nobody
outside can hold us to.

4367 of 4369 pass. The two failures, `ExitRehearsalRunbookTest` and
`KeyRotationTest`, fail on a clean tree at this tip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012TRDRrfL1khQhQtNr3SRrA
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 336 · 2026-08-25T03:39:29.151231Z

Changed files

  • modified .agents/skills/openagents-work-management/SKILL.md
  • modified AGENTS.md
  • modified INVARIANTS.md
  • modified README.md
  • modified config/config.exs
  • modified docs/api-authentication.md
  • modified docs/architecture.md
  • modified docs/capacity-and-matching-api.md
  • modified docs/deployment-control-plane.md
  • modified docs/forge-operator-independence.md
  • modified docs/forum-port.md
  • modified docs/github-api-issues-projects-assessment.md
  • modified docs/issues-projects-ui-roadmap.md
  • modified docs/issues-projects-work-plan.md
  • modified docs/openagents-cli/api.md
  • modified docs/openagents-cli/command-reference.md
  • modified docs/openagents-cli/delete-repository.md
  • modified docs/operations/production-deploy-runbook.md
  • modified docs/product-promises-registry.md
  • added docs/projects/agent-continuity-and-memory/ISSUES.md
  • added docs/projects/agent-continuity-and-memory/PROJECT.md
  • modified docs/pull-requests.md
  • modified docs/taxonomy.md
  • modified lib/openagents/chat/backends.ex
  • modified lib/openagents/data_rights/account_export.ex
  • modified lib/openagents/data_rights/export_inventory.ex
  • modified lib/openagents/forge/git_http.ex
  • modified lib/openagents/inference/models.ex
  • modified lib/openagents/issues.ex
  • modified lib/openagents/issues/capture.ex
  • modified lib/openagents/projects.ex
  • modified lib/openagents/threads.ex
  • modified lib/openagents/tools/issue_capture.ex
  • modified lib/openagents_web/api_error.ex
  • modified lib/openagents_web/api_route_authority.ex
  • modified lib/openagents_web/contribution_contract.ex
  • modified lib/openagents_web/controllers/api_extension_controller.ex
  • modified lib/openagents_web/controllers/chat_turn_controller.ex
  • modified lib/openagents_web/controllers/fleet_target_controller.ex
  • modified lib/openagents_web/controllers/forum_api_controller.ex
  • modified lib/openagents_web/controllers/forum_api_json.ex
  • modified lib/openagents_web/controllers/issue_controller.ex
  • modified lib/openagents_web/controllers/issue_json.ex
  • modified lib/openagents_web/controllers/label_json.ex
  • modified lib/openagents_web/controllers/milestone_json.ex
  • modified lib/openagents_web/controllers/model_catalog_controller.ex
  • modified lib/openagents_web/controllers/project_json.ex
  • modified lib/openagents_web/controllers/pull_request_json.ex
  • modified lib/openagents_web/controllers/stack_json.ex
  • modified lib/openagents_web/controllers/thread_controller.ex
  • modified lib/openagents_web/docs_catalog.ex
  • modified lib/openagents_web/endpoint.ex
  • modified lib/openagents_web/live/gym_live.ex
  • modified lib/openagents_web/live/home_live.ex
  • modified lib/openagents_web/live/memory_live.ex
  • modified lib/openagents_web/live/thread_index_live.ex
  • added lib/openagents_web/plugs/api_v3_rewrite.ex
  • modified lib/openagents_web/route_authority.ex
  • modified lib/openagents_web/router.ex
  • modified ops/ci/allowed-sarah-references.txt
  • modified priv/api-contracts/repositories-v1.json
  • modified priv/docs/api-tokens.md
  • modified priv/docs/claim-legacy-identity.md
  • modified priv/docs/cli-api.md
  • modified priv/docs/cli-command-reference.md
  • modified priv/docs/delete-repository.md
  • modified priv/docs/forum.md
  • modified priv/docs/issues.md
  • modified priv/docs/merging-stacks.md
  • modified priv/docs/pull-requests.md
  • modified priv/docs/rest-api.md
  • modified priv/docs/stack-actions.md
  • modified priv/docs/stacked-pull-requests.md
  • modified priv/docs/stacks-api.md
  • modified test/openagents/chat/backends_test.exs
  • modified test/openagents/data_rights/export_inventory_test.exs
  • modified test/openagents/dependency_boundary_test.exs
  • modified test/openagents/forge/assignment_credential_reach_test.exs
  • modified test/openagents/forge/independence_test.exs
  • modified test/openagents/network_status_test.exs
  • modified test/openagents/projects_test.exs
  • modified test/openagents/scv/open_code_executor_test.exs
  • modified test/openagents/threads/grant_token_reach_test.exs
  • modified test/openagents_web/api_error_test.exs
  • modified test/openagents_web/api_route_authority_test.exs
  • modified test/openagents_web/contribution_contract_test.exs
  • modified test/openagents_web/controllers/agent_controller_test.exs
  • modified test/openagents_web/controllers/api_error_contract_test.exs
  • modified test/openagents_web/controllers/api_extension_controller_test.exs
  • modified test/openagents_web/controllers/api_extension_governance_test.exs
  • modified test/openagents_web/controllers/api_token_controller_test.exs
  • modified test/openagents_web/controllers/assignee_controller_test.exs
  • modified test/openagents_web/controllers/box_controller_test.exs
  • modified test/openagents_web/controllers/box_fanout_controller_test.exs
  • modified test/openagents_web/controllers/box_run_controller_test.exs
  • modified test/openagents_web/controllers/capacity_controller_test.exs
  • modified test/openagents_web/controllers/chat_turn_controller_test.exs
  • modified test/openagents_web/controllers/comment_controller_test.exs
  • modified test/openagents_web/controllers/computer_control_api_test.exs
  • modified test/openagents_web/controllers/delegations_controller_test.exs
  • modified test/openagents_web/controllers/deployment_controller_test.exs
  • modified test/openagents_web/controllers/device_authorization_controller_test.exs
  • modified test/openagents_web/controllers/fleet_target_controller_test.exs
  • modified test/openagents_web/controllers/forge_user_controller_test.exs
  • modified test/openagents_web/controllers/forum_api_controller_test.exs
  • modified test/openagents_web/controllers/forum_tips_api_controller_test.exs
  • modified test/openagents_web/controllers/gym_run_controller_test.exs
  • modified test/openagents_web/controllers/issue_assignee_controller_test.exs
  • modified test/openagents_web/controllers/issue_capture_controller_test.exs
  • modified test/openagents_web/controllers/issue_completion_claim_controller_test.exs
  • modified test/openagents_web/controllers/issue_controller_test.exs
  • modified test/openagents_web/controllers/issue_dependency_controller_test.exs
  • modified test/openagents_web/controllers/issue_label_controller_test.exs
  • modified test/openagents_web/controllers/label_controller_test.exs
  • modified test/openagents_web/controllers/milestone_controller_test.exs
  • modified test/openagents_web/controllers/model_catalog_controller_test.exs
  • modified test/openagents_web/controllers/private_issue_metadata_test.exs
  • modified test/openagents_web/controllers/project_controller_test.exs
  • modified test/openagents_web/controllers/pull_request_controller_test.exs
  • modified test/openagents_web/controllers/push_receipt_controller_test.exs
  • modified test/openagents_web/controllers/repository_controller_test.exs
  • modified test/openagents_web/controllers/repository_import_controller_test.exs
  • modified test/openagents_web/controllers/repository_isolation_controller_test.exs
  • modified test/openagents_web/controllers/reputation_controller_test.exs
  • modified test/openagents_web/controllers/reputation_subject_claim_controller_test.exs
  • modified test/openagents_web/controllers/stack_controller_test.exs
  • modified test/openagents_web/controllers/thread_controller_test.exs
  • modified test/openagents_web/live/project_show_live_test.exs
  • modified test/openagents_web/operator_surface_test.exs
  • added test/openagents_web/plugs/api_v3_rewrite_test.exs
  • modified test/openagents_web/route_authority_test.exs
  • modified test/openagents_web/thread_visibility_test.exs

Diff

The diff is larger than the display bound; the tail is cut.

127 files changed, +1931 -1426

.agents/skills/openagents-work-management/SKILL.md modified +3 -3

@@ -7,7 +7,7 @@ allowed-tools:

7 7
  - grep
8 8
---
9 9
10
Use this skill when the user asks you to manage OpenAgents issues, projects, comments, labels, assignees, milestones, or Projects V2 boards from the command line. Do not use the `pro-work-management` skill or the Pro Linear/MCP surface. This skill targets the OpenAgents `/api/v3` surface through the `@openagentsinc/cli` package.
10
Use this skill when the user asks you to manage OpenAgents issues, projects, comments, labels, assignees, milestones, or Projects V2 boards from the command line. Do not use the `pro-work-management` skill or the Pro Linear/MCP surface. This skill targets the OpenAgents `/api/v1` surface through the `@openagentsinc/cli` package.
11 11
12 12
The CLI has named commands for most of this work: `openagents issue`
13 13
(`list`, `view`, `create`, `close`, `reopen`, `comment`, `label`, `assign`,

@@ -40,7 +40,7 @@ position, and the examples below show them.

40 40
41 41
## Route addressing
42 42
43
A relative path for `openagents api` resolves under `/api/v3/`. For example, `repos/OWNER/REPO/issues` is the same as `/api/v3/repos/OWNER/REPO/issues`.
43
A relative path for `openagents api` resolves under `/api/v1/`. For example, `repos/OWNER/REPO/issues` is the same as `/api/v1/repos/OWNER/REPO/issues`.
44 44
45 45
- Method: `-X GET|POST|PATCH|PUT|DELETE`.
46 46
- String fields: repeatable `-f KEY=VALUE`. Every value is sent as a JSON string.

@@ -133,7 +133,7 @@ openagents api "repos/OWNER/REPO/issues?progress=in_progress"

133 133
Ask the API what OpenAgents adds to the GitHub shape instead of reading prose:
134 134
135 135
```sh
136
openagents api "/api/v3"
136
openagents api "/api/v1"
137 137
```
138 138
139 139
That root document enumerates every `openagents` field with its type, enum
AGENTS.md modified +11 -11

@@ -514,15 +514,15 @@ We are driving the GitHub-compatible Issues and Projects API through tests. This

514 514
515 515
Build the endpoints in this order:
516 516
517
1. `GET /api/v3/repos/{owner}/{repo}/issues` — list
518
2. `GET /api/v3/repos/{owner}/{repo}/issues/{issue_number}` — get
519
3. `POST /api/v3/repos/{owner}/{repo}/issues` — create
520
4. `PATCH /api/v3/repos/{owner}/{repo}/issues/{issue_number}` — update
521
5. `GET/POST /api/v3/repos/{owner}/{repo}/issues/{issue_number}/comments` — comments
522
6. `GET/POST/DELETE /api/v3/repos/{owner}/{repo}/issues/{issue_number}/assignees` — assignees
523
7. `GET/POST/DELETE /api/v3/repos/{owner}/{repo}/issues/{issue_number}/labels` — labels
524
8. `GET/POST/DELETE /api/v3/repos/{owner}/{repo}/milestones` — milestones
525
9. `/api/v3/users/{username}/projectsV2/*` — project read and write
517
1. `GET /api/v1/repos/{owner}/{repo}/issues` — list
518
2. `GET /api/v1/repos/{owner}/{repo}/issues/{issue_number}` — get
519
3. `POST /api/v1/repos/{owner}/{repo}/issues` — create
520
4. `PATCH /api/v1/repos/{owner}/{repo}/issues/{issue_number}` — update
521
5. `GET/POST /api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments` — comments
522
6. `GET/POST/DELETE /api/v1/repos/{owner}/{repo}/issues/{issue_number}/assignees` — assignees
523
7. `GET/POST/DELETE /api/v1/repos/{owner}/{repo}/issues/{issue_number}/labels` — labels
524
8. `GET/POST/DELETE /api/v1/repos/{owner}/{repo}/milestones` — milestones
525
9. `/api/v1/users/{username}/projectsV2/*` — project read and write
526 526
527 527
### Test anatomy
528 528

@@ -531,8 +531,8 @@ Each endpoint test file follows this shape:

531 531
    defmodule OpenAgentsWeb.IssueControllerTest do
532 532
      use OpenAgentsWeb.ConnCase
533 533
534
      test "GET /api/v3/repos/:owner/:repo/issues lists open issues", %{conn: conn} do
535
        conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
534
      test "GET /api/v1/repos/:owner/:repo/issues lists open issues", %{conn: conn} do
535
        conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
536 536
        assert json_response(conn, 200)["issues"] != nil
537 537
      end
538 538
    end
INVARIANTS.md modified +35 -32

@@ -1205,14 +1205,14 @@ Status: Current

1205 1205
A turn is never answered by a different model than the one requested without
1206 1206
the caller being told. `OpenAgents.Inference.Models` is the one config-driven
1207 1207
catalog (`config :openagents, :model_catalog`) of the models this deployment
1208
serves, `GET /api/v3/models` publishes it — id, provider lane, context and
1208
serves, `GET /api/v1/models` publishes it — id, provider lane, context and
1209 1209
output ceilings, availability — and every admission checks the same list, so
1210 1210
the offered set and the refused-against set cannot drift.
1211 1211
1212 1212
Concretely:
1213 1213
1214 1214
- A model outside the catalog is refused with a typed error naming the served
1215
  set — a field-level `422` at `POST /api/v3/threads`, `model_not_served` at
1215
  set — a field-level `422` at `POST /api/v1/threads`, `model_not_served` at
1216 1216
  the inference proxy — never replaced by a default.
1217 1217
- A model in the catalog whose provider credential is not configured is
1218 1218
  **listed as unavailable rather than omitted**, and selecting it is refused

@@ -1228,8 +1228,8 @@ Concretely:

1228 1228
  mismatch is refused, requested and effective agree on every `200`.
1229 1229
1230 1230
The chat lane keeps the same law through `OpenAgents.Chat.Backends`: an
1231
unsupported `model` on `POST /api/v3/chat/turns` is a typed `422`, and
1232
`GET /api/v3` publishes the supported enum from the same list.
1231
unsupported `model` on `POST /api/v1/chat/turns` is a typed `422`, and
1232
`GET /api/v1` publishes the supported enum from the same list.
1233 1233
1234 1234
Evidence: `OpenAgents.Inference.Models`, `OpenAgentsWeb.ModelCatalogController`,
1235 1235
`OpenAgentsWeb.InferenceProxyController`, `OpenAgentsWeb.ThreadController`,

@@ -2169,7 +2169,7 @@ conversation, and a thread is not one.

2169 2169
2170 2170
- **Authority is capped at admission.** `OpenAgents.Threads.open/3` refuses an
2171 2171
  account already holding `maximum_open_threads_per_account` open threads with
2172
  `:thread_quota_reached`, and `POST /api/v3/threads` renders that as a `429`
2172
  `:thread_quota_reached`, and `POST /api/v1/threads` renders that as a `429`
2173 2173
  naming the limit. The cap is taken before any row is written, so a refused
2174 2174
  caller leaves nothing behind. Because a thread has at most one live grant,
2175 2175
  capping open threads caps the account's concurrent thread-scoped authority by

@@ -2192,7 +2192,7 @@ conversation, and a thread is not one.

2192 2192
  `OpenAgents.Inference.Credit.remaining/1` says the account has left, so
2193 2193
  opening a second thread hands out no second allowance and an account with
2194 2194
  nothing left is refused `:credit_exhausted` rather than minted a grant it
2195
  cannot spend. `GET /api/v3` publishes both allowances, because a client that
2195
  cannot spend. `GET /api/v1` publishes both allowances, because a client that
2196 2196
  read a fixed per-thread cost cap would be reading a budget nobody is given.
2197 2197
2198 2198
  Amended 2026-08-24 (issue #195): the remainder is metered spend, and mints

@@ -2303,7 +2303,7 @@ until the account that opened it says so.

2303 2303
  admits `dark` and `ledger` and nothing else, by check constraint and by
2304 2304
  `Thread.open_changeset/3`. `pulse` would need a metadata-only projection of a
2305 2305
  transcript and `glass` a capability beyond reading one; neither exists, so
2306
  neither is stored. `POST /api/v3/threads` refuses any other value —
2306
  neither is stored. `POST /api/v1/threads` refuses any other value —
2307 2307
  a tier of the vocabulary this surface cannot enforce, or a word that is not a
2308 2308
  tier — with the stable code `thread_visibility_unsupported`, because a client
2309 2309
  that meant to publish and did not must learn it from the code it branches on

@@ -2321,7 +2321,7 @@ until the account that opened it says so.

2321 2321
  a column somebody can find changed with no account of when or why.
2322 2322
- **A tier widens reading and nothing else.** `fetch_readable/2` is the only
2323 2323
  lookup a tier reaches, and it serves exactly two surfaces: `GET
2324
  /api/v3/threads/{thread_id}` with its `/events`, and `/threads/:id`. Every
2324
  /api/v1/threads/{thread_id}` with its `/events`, and `/threads/:id`. Every
2325 2325
  write — the transcript append, the cancel, the re-mint — resolves through the
2326 2326
  owner-scoped `get_for_user/2`, so a published transcript is never a thread a
2327 2327
  stranger may append to, end, or spend.

@@ -2489,7 +2489,7 @@ target, and the same key with different bytes is refused. An optional

2489 2489
expected-current-target ID is a compare-and-set precondition, so two
2490 2490
concurrent operators cannot unknowingly supersede each other.
2491 2491
2492
The `/admin/forge` **Promote** button and `POST /api/v3/admin/forge/targets`
2492
The `/admin/forge` **Promote** button and `POST /api/v1/admin/forge/targets`
2493 2493
are one authority path, not two implementations of one policy. Both call
2494 2494
`OpenAgents.Forge.Promotion`, which calls `OpenAgents.Forge.Targets.promote/4`,
2495 2495
so both write the same append-only `forge_fleet_targets` receipt carrying the

@@ -2505,7 +2505,7 @@ and a digest of the idempotency key. Neither the plaintext credential nor the

2505 2505
plaintext key is ever stored, and a status response discloses no node
2506 2506
identity, filesystem path, or unrestricted failure detail.
2507 2507
2508
Refusals carry the one `/api/v3` envelope, `OpenAgentsWeb.ApiError`, with a
2508
Refusals carry the one `/api/v1` envelope, `OpenAgentsWeb.ApiError`, with a
2509 2509
stable code per refusal reason so a release client can tell "you may not do
2510 2510
this" from "someone promoted first" from "those bytes are not in the forge".
2511 2511

@@ -2870,7 +2870,7 @@ holds is enumerated below, and the proof asserts the enumeration rather than the

2870 2870
sentence:
2871 2871
2872 2872
- Promoting an already-pushed commit as the fleet deploy target, from
2873
  `/admin/forge` and from `POST /api/v3/admin/forge/targets`
2873
  `/admin/forge` and from `POST /api/v1/admin/forge/targets`
2874 2874
  (`OpenAgents.Forge.Promotion` into `OpenAgents.Forge.Targets`), receipted with
2875 2875
  the promoting operator's identity in the append-only `forge_fleet_targets`
2876 2876
  ledger. Only SHAs present in the WAL-backed repository are promotable, so the

@@ -2882,12 +2882,12 @@ sentence:

2882 2882
  `OpenAgents.SCV.Deployments`).
2883 2883
- Forum moderation: closing, reopening, and pinning a topic and hiding or
2884 2884
  deleting a post, from `OpenAgentsWeb.ForumTopicLive` and from
2885
  `PATCH /api/v3/forum/topics/:id` and `PATCH /api/v3/forum/posts/:id`; and
2885
  `PATCH /api/v1/forum/topics/:id` and `PATCH /api/v1/forum/posts/:id`; and
2886 2886
  approving or rejecting an identity claim from `/admin/forum/claims` and
2887
  `PATCH /api/v3/forum/claims/:id`.
2887
  `PATCH /api/v1/forum/claims/:id`.
2888 2888
- Approving or rejecting a reputation subject claim under
2889
  `GET /api/v3/reputation/subject-claims/pending` and
2890
  `PATCH /api/v3/reputation/subject-claims/:id`
2889
  `GET /api/v1/reputation/subject-claims/pending` and
2890
  `PATCH /api/v1/reputation/subject-claims/:id`
2891 2891
  (`OpenAgentsWeb.ReputationController`). The decision binds an attestation
2892 2892
  subject to an account under `EXIT-001`; it never issues, revokes, or alters
2893 2893
  an attestation, and `REPUTATION-001`'s verification does not read it.

@@ -3546,11 +3546,14 @@ either: a failed read is absent, never an error string.

3546 3546
3547 3547
A list-element key path is published only while that list has an element, so
3548 3548
the enumeration follows the data in both directions. `independence.export.gaps`
3549
is empty today because `EXIT-001` records no `partial` or `blocked` family, and
3550
its `family`, `status`, and `issue` paths are therefore not declared. A family
3551
that becomes a gap republishes all three, and the enumeration fails until this
3552
contract readmits them — which is the decision being asked for, not an
3553
accident.
3549
has one: `EXIT-001` records the `trace` family blocked, because
3550
`POST /api/v1/traces` accepts an ATIF upload and no route reads one back. Its
3551
`family`, `status`, and `issue` paths are therefore declared, and an anonymous
3552
caller reads that this deployment holds a kind of record it cannot yet hand
3553
back, and which issue tracks that. Publishing the gap is the point of the
3554
disclosure: a gap nobody outside can see is one nobody outside can hold us to.
3555
Should the list empty again, the three paths come back out — the enumeration
3556
follows the data, and it fails in both directions until someone decides.
3554 3557
3555 3558
The independence disclosure also carries the anchor's own address
3556 3559
(`/.well-known/openagents-forge-anchor.json`), which is a fixed path and not

@@ -3896,7 +3899,7 @@ Amended 2026-08-22 (explicit issue and project repository scope): production

3896 3899
contexts accept a repository or a resource that already carries its repository
3897 3900
identity. No context function selects a default repository or grants membership
3898 3901
as a side effect. The Projects V2 API uses
3899
`/api/v3/repos/:owner/:repo/projectsV2`; every project, item, and field query
3902
`/api/v1/repos/:owner/:repo/projectsV2`; every project, item, and field query
3900 3903
includes that repository. Optional bearer authentication lets a member read a
3901 3904
private repository's issues and projects, while anonymous readers and
3902 3905
nonmembers receive `404 Not Found`. Every write still requires `forge:write`

@@ -3919,12 +3922,12 @@ Evidence: `OpenAgents.Repositories`, `OpenAgents.Repositories.Provisioner`,

3919 3922
3920 3923
Status: Current
3921 3924
3922
The GitHub-shaped API under `/api/v3` grows OpenAgents-specific fields in one
3925
The GitHub-shaped API under `/api/v1` grows OpenAgents-specific fields in one
3923 3926
namespaced object per resource. GitHub-shaped keys keep their exact shape, so a
3924 3927
GitHub client sees an additional `openagents` object and nothing else, and
3925 3928
every OpenAgents field lives inside it.
3926 3929
3927
Discovery is mechanical, not tribal. `GET /api/v3` enumerates each extension
3930
Discovery is mechanical, not tribal. `GET /api/v1` enumerates each extension
3928 3931
field with its type, its enum values, its owning version, and the endpoints it
3929 3932
belongs to. A response carrying an extension names the namespace in the
3930 3933
`x-openagents-extensions` header. A filter the root document lists is refused

@@ -3961,7 +3964,7 @@ Every deployment publishes one participation contract in two representations:

3961 3964
`/agents.md` for a reader and `/agents.json` for a client. Both are rendered
3962 3965
from `OpenAgentsWeb.ContributionContract`, so they carry the same contract
3963 3966
identifier, version, revision, and digest and cannot drift apart by editing
3964
one of them. `GET /api/v3` points at both and republishes that digest, so an
3967
one of them. `GET /api/v1` points at both and republishes that digest, so an
3965 3968
agent that starts at the API description finds the contract without guessing a
3966 3969
path, and an agent receipt can record which instructions it followed.
3967 3970

@@ -3974,7 +3977,7 @@ unchanged identifier means the wording or a derived value moved.

3974 3977
3975 3978
The document is derived rather than written beside the application. Each
3976 3979
published request carries the classification of the authority that owns its
3977
surface, and only that one: `/api/v3` requests carry the principal, family, and
3980
surface, and only that one: `/api/v1` requests carry the principal, family, and
3978 3981
error contract from `OpenAgentsWeb.ApiRouteAuthority`, which is proven against
3979 3982
what the enforcing pipeline does to an anonymous request, and every other
3980 3983
request carries the class, principal, and scope from

@@ -4109,9 +4112,9 @@ maintained beside it.

4109 4112
4110 4113
Coverage is derived, not curated. Every resource family
4111 4114
`OpenAgentsWeb.ApiRouteAuthority.families/0` publishes must appear, in both
4112
directions, so a family reaching `/api/v3` without someone deciding whether a
4115
directions, so a family reaching `/api/v1` without someone deciding whether a
4113 4116
user can export it fails the build, and a family the API drops leaves no stale
4114
claim behind. Families that leave through routes outside `/api/v3` — Git
4117
claim behind. Families that leave through routes outside `/api/v1` — Git
4115 4118
transport for repository content, the `DATA-004` exports for conversations and
4116 4119
memory, and `GET /data/export/account` for the forge-owned and forum-owned
4117 4120
records an account authors — are listed alongside them.

@@ -4145,7 +4148,7 @@ under a principal that is not the requesting account. `push_receipt` is probed

4145 4148
there rather than against the route inventory: what the account gets back is
4146 4149
its own `forge_pushes` rows, matched exactly on the `user:<account-id>`
4147 4150
principal a person's push records. A repository-scoped read is published beside
4148
it — `GET /api/v3/repos/{owner}/{repo}/pushes` (#167), which serves the WAL's
4151
it — `GET /api/v1/repos/{owner}/{repo}/pushes` (#167), which serves the WAL's
4149 4152
own entries and the `EXIT-005` chain link `git push` printed to the pusher —
4150 4153
and it is proven by its own test rather than by this probe, because the
4151 4154
question here is what an *account* gets back.

@@ -4440,7 +4443,7 @@ asserts both halves: clean with no anchor, reported with one.

4440 4443
The link leaves the forge at acknowledgment. `OpenAgents.Forge.GitHTTP` appends
4441 4444
one side-band band-2 message to the `receive-pack` response, so an ordinary
4442 4445
`git push` prints `remote: openagents wal-receipt seq=<n> link=<sha256>` and the
4443
pusher can keep it; `GET /api/v3/repos/{owner}/{repo}/pushes` serves the same
4446
pusher can keep it; `GET /api/v1/repos/{owner}/{repo}/pushes` serves the same
4444 4447
values from the WAL afterwards. A pusher who keeps that line holds a value the
4445 4448
operator cannot retroactively change, and `verify/2` with it as `:anchor`
4446 4449
reports `anchor_mismatch` against a log rewritten at or before that sequence.

@@ -5087,7 +5090,7 @@ and `test/openagents_web/live/notifications_live_test.exs`.

5087 5090
5088 5091
Status: Current
5089 5092
5090
Every refusal from an issue-family `/api/v3` route carries one envelope:
5093
Every refusal from an issue-family `/api/v1` route carries one envelope:
5091 5094
`message`, `code`, `status`, `documentation_url`, `request_id`, and `errors`.
5092 5095
`OpenAgentsWeb.ApiError` owns it, and a code determines its status there, so no
5093 5096
controller chooses a status for a failure the API has already named. Two

@@ -5101,7 +5104,7 @@ table distinguishes them.

5101 5104
Amended 2026-08-23 (issue #186): that ambiguity is load-bearing, so nothing
5102 5105
else may hide behind it. A `rescue Ecto.NoResultsError` wrapped around a whole
5103 5106
controller action caught every bang lookup the action reached, not only the
5104
repository one it was written for, so `POST /api/v3/repos/{owner}/{repo}/issues`
5107
repository one it was written for, so `POST /api/v1/repos/{owner}/{repo}/issues`
5105 5108
answered `404` for a label the request body named that the repository does not
5106 5109
have — the same `404` a repository the caller cannot see answers with, which
5107 5110
made the honest error unreachable and sent one reporter to the router and the

@@ -5118,7 +5121,7 @@ to a private repository the caller is not a member of still answers

5118 5121
`not_found`, with `errors` empty and a body that names neither the repository
5119 5122
nor the label.
5120 5123
5121
The published route inventory at `GET /api/v3` is derived from
5124
The published route inventory at `GET /api/v1` is derived from
5122 5125
`OpenAgentsWeb.Router.__routes__/0` through `OpenAgentsWeb.ApiRouteAuthority`,
5123 5126
never maintained beside it. Each route carries three mandatory classifications
5124 5127
— principal, resource family, and error contract — so a route added to the
README.md modified +1 -1

@@ -124,7 +124,7 @@ an independently pushed GitHub branch as production authority.

124 124
125 125
An agent starts at the front door instead. Every deployment serves the same
126 126
participation contract in two representations, `/agents.md` for a reader and
127
`/agents.json` for a client, and `GET /api/v3` points at both with the digest
127
`/agents.json` for a client, and `GET /api/v1` points at both with the digest
128 128
it currently carries. The contract is derived from the application rather than
129 129
written beside it: `OpenAgentsWeb.ContributionContract` builds it and
130 130
`OpenAgentsWeb.ContributionContractTest` refuses any claim in it the
config/config.exs modified +6 -7

@@ -351,13 +351,12 @@ config :openagents,

351 351
  # calls and a longer life, and a lower token and cost ceiling, because
352 352
  # nothing on the server bounded the work first.
353 353
  #
354
  # `maximum_open_threads_per_account` is the admission cap. Eight open threads
355
  # is a person working several checkouts at once, and THREAD-001 admits at
356
  # most one live grant per open thread, so the account's concurrent
357
  # thread-scoped authority is bounded by eight of the ceilings below.
358
  # `OPENAGENTS_MAX_OPEN_THREADS` overrides the number at boot (runtime.exs)
359
  # without changing the cap mechanism.
360
  maximum_open_threads_per_account: 8,
354
  # No cap. A coder session no longer revokes its thread when it exits — a
355
  # thread is durable and `--resume` is the whole point of that — so a count of
356
  # open threads is a count of every session the account has ever run, and
357
  # refusing the ninth would refuse the work rather than bound it. What bounds
358
  # an account is its credit.
359
  maximum_open_threads_per_account: nil,
361 360
  # Unbounded, all three. A coding session reached 256 calls, a million tokens,
362 361
  # and two dollars in an afternoon, and was told to start a new session — the
363 362
  # same interruption the clock below used to cause, by another route. A
docs/api-authentication.md modified +21 -21

@@ -4,8 +4,8 @@ Date: 2026-08-20

4 4
5 5
## Forge API clients
6 6
7
`GET` routes under `/api/v3` are public projections of published forge data.
8
Write routes under `/api/v3` require a scoped OpenAgents bearer credential.
7
`GET` routes under `/api/v1` are public projections of published forge data.
8
Write routes under `/api/v1` require a scoped OpenAgents bearer credential.
9 9
Human forge writes use a personal API token with exact `forge:write` scope.
10 10
Agent participation writes use an `oa_agent_…` credential with exact
11 11
`agent:participate` scope.

@@ -20,7 +20,7 @@ curl \

20 20
  --header "Authorization: Bearer $OPENAGENTS_API_TOKEN" \
21 21
  --header "Content-Type: application/json" \
22 22
  --data '{"title":"Example"}' \
23
  https://staging.openagents.com/api/v3/repos/OpenAgentsInc/openagents.com/issues
23
  https://staging.openagents.com/api/v1/repos/OpenAgentsInc/openagents.com/issues
24 24
```
25 25
26 26
Do not put the token in a URL, command history, checked-in environment file, or

@@ -96,11 +96,11 @@ used by fan-out admission.

96 96
97 97
The `computer:control` scope gives a human account token access to the
98 98
connected Computer API. It is independent from `box:control`: neither scope
99
confers the other. The scope reaches `GET /api/v3/computers`,
100
`POST /api/v3/computers/:computer_id/probe`,
101
`POST /api/v3/computers/:computer_id/agent-jobs`,
102
`GET /api/v3/computer-agent-jobs/:id`, and
103
`DELETE /api/v3/computer-agent-jobs/:id`:
99
confers the other. The scope reaches `GET /api/v1/computers`,
100
`POST /api/v1/computers/:computer_id/probe`,
101
`POST /api/v1/computers/:computer_id/agent-jobs`,
102
`GET /api/v1/computer-agent-jobs/:id`, and
103
`DELETE /api/v1/computer-agent-jobs/:id`:
104 104
105 105
```sh
106 106
openagents api -X GET computers

@@ -112,8 +112,8 @@ openagents api -X DELETE computer-agent-jobs/JOB_ID

112 112
```
113 113
114 114
The delegated grant uses the same agent grant mechanism as Box control. The
115
grant routes are `POST /api/v3/agents/:handle/computer-control` and
116
`DELETE /api/v3/agents/:handle/computer-control`:
115
grant routes are `POST /api/v1/agents/:handle/computer-control` and
116
`DELETE /api/v1/agents/:handle/computer-control`:
117 117
118 118
```sh
119 119
openagents api -X POST agents/AGENT_HANDLE/computer-control

@@ -258,13 +258,13 @@ its process disappears without an exit sentinel.

258 258
### Agent participation credentials
259 259
260 260
An agent can register without GitHub by sending its handle and display name to
261
`POST /api/v3/agents/register`:
261
`POST /api/v1/agents/register`:
262 262
263 263
```sh
264 264
curl -sS -X POST \
265 265
  -H 'Content-Type: application/json' \
266 266
  -d '{"handle":"release-bot","display_name":"Release bot"}' \
267
  https://openagents.com/api/v3/agents/register
267
  https://openagents.com/api/v1/agents/register
268 268
```
269 269
270 270
The `201 Created` response contains the agent profile and an `oa_agent_…`

@@ -280,7 +280,7 @@ Send the credential as a bearer token:

280 280
```sh
281 281
curl -sS \
282 282
  -H "Authorization: Bearer $OPENAGENTS_AGENT_TOKEN" \
283
  https://openagents.com/api/v3/agent
283
  https://openagents.com/api/v1/agent
284 284
```
285 285
286 286
Registration rejects unavailable, reserved, malformed, confusable, and

@@ -297,7 +297,7 @@ curl -sS -X POST \

297 297
  -H "Authorization: Bearer $OPENAGENTS_AGENT_TOKEN" \
298 298
  -H 'Content-Type: application/json' \
299 299
  -d '{"name":"rotated credential"}' \
300
  https://openagents.com/api/v3/agent/credentials
300
  https://openagents.com/api/v1/agent/credentials
301 301
```
302 302
303 303
The response returns a new one-time `oa_agent_…` credential. The presenting

@@ -308,7 +308,7 @@ An agent that is not allowed to participate in a repository receives

308 308
`{"error":{"code":"agent_participation_forbidden"}}`.
309 309
310 310
An agent may request an optional human link with
311
`POST /api/v3/agent/links` and a `user_id`. The human reviews pending requests
311
`POST /api/v1/agent/links` and a `user_id`. The human reviews pending requests
312 312
with a `forge:write` credential:
313 313
314 314
```sh

@@ -326,8 +326,8 @@ uses `rejected`, and a later request reuses either record as `pending`.

326 326
327 327
### Account chat events
328 328
329
Use `POST /api/v3/chat/turns` to submit an account chat message and
330
`GET /api/v3/chat/events` to list its durable event journal. These routes use
329
Use `POST /api/v1/chat/turns` to submit an account chat message and
330
`GET /api/v1/chat/events` to list its durable event journal. These routes use
331 331
the same account-scoped application service and ordered projection as `/chat`.
332 332
Both routes require a personal API token with the `chat:account` scope. A token
333 333
with only `forge:write` cannot read or submit account chat data.

@@ -339,7 +339,7 @@ curl \

339 339
  --header "Authorization: Bearer $OPENAGENTS_API_TOKEN" \
340 340
  --header "Content-Type: application/json" \
341 341
  --data '{"message":"Summarize my repository README.","reasoning":"high"}' \
342
  https://staging.openagents.com/api/v3/chat/turns
342
  https://staging.openagents.com/api/v1/chat/turns
343 343
```
344 344
345 345
The server returns `202 Accepted` after it durably creates the run and its

@@ -352,7 +352,7 @@ List the account's event journal with this request:

352 352
```sh
353 353
curl \
354 354
  --header "Authorization: Bearer $OPENAGENTS_API_TOKEN" \
355
  https://staging.openagents.com/api/v3/chat/events
355
  https://staging.openagents.com/api/v1/chat/events
356 356
```
357 357
358 358
Each event contains an event ID, run ID, run-local sequence number, type,

@@ -393,7 +393,7 @@ Two conditions authorize each request, and holding one is never enough:

393 393
- `OpenAgents.Accounts.admin?/1` is true for the token's owner at request
394 394
  time. Removing an account from the operator allowlist refuses its next
395 395
  request with `403 not_operator`, without waiting for the token to expire.
396
  Both refusals use the shared `/api/v3` error envelope.
396
  Both refusals use the shared `/api/v1` error envelope.
397 397
398 398
Issuance is gated the same way. Only a current operator can be issued the
399 399
scope, the credential expires in at most 7 days rather than 90, and creation,

@@ -407,7 +407,7 @@ bootstrap release tooling without minting the credential from a settings page:

407 407
curl --request POST \
408 408
  --header "Content-Type: application/json" \
409 409
  --data '{"scope": "deployments:promote"}' \
410
  https://openagents.com/api/v3/device/authorizations
410
  https://openagents.com/api/v1/device/authorizations
411 411
```
412 412
413 413
The approval page at `/device` names every requested scope and marks a
docs/architecture.md modified +2 -2

@@ -123,9 +123,9 @@ turn, work item, or voice session.

123 123
## Durable account chat
124 124
125 125
`OpenAgents.Chat.AccountTurns` is the application entry point for account chat
126
submissions from both `/chat` and `POST /api/v3/chat/turns`. It creates the run
126
submissions from both `/chat` and `POST /api/v1/chat/turns`. It creates the run
127 127
and first event in one transaction before it starts provider work. The browser
128
and `GET /api/v3/chat/events` then project the same account-scoped journal.
128
and `GET /api/v1/chat/events` then project the same account-scoped journal.
129 129
LiveView messages and PubSub notifications remain replaceable projections of
130 130
that durable state.
131 131
docs/capacity-and-matching-api.md modified +3 -3

@@ -21,8 +21,8 @@ this module never admits, reserves, or leases anything.

21 21
| Endpoint | Principal | Purpose |
22 22
| --- | --- | --- |
23 23
| `GET /api/capacity` | Signed-in account session | The projection the web surface reads |
24
| `GET /api/v3/capacity` | `chat:account` bearer token | The same projection for agents |
25
| `POST /api/v3/capacity/matches` | `chat:account` bearer token | Ranked candidates or a typed refusal for one job requirement |
24
| `GET /api/v1/capacity` | `chat:account` bearer token | The same projection for agents |
25
| `POST /api/v1/capacity/matches` | `chat:account` bearer token | Ranked candidates or a typed refusal for one job requirement |
26 26
27 27
Both capacity projections come from one function, so a web reader and an API
28 28
reader with the same authority see the same numbers.

@@ -126,7 +126,7 @@ code while quantities stay visible.

126 126
127 127
## Matching
128 128
129
`POST /api/v3/capacity/matches` takes one typed requirement:
129
`POST /api/v1/capacity/matches` takes one typed requirement:
130 130
131 131
```json
132 132
{
docs/deployment-control-plane.md modified +2 -2

@@ -6,7 +6,7 @@ This plane serves tenants: a repository deploys its own code to its own

6 6
environments. It is not the forge fleet-promotion surface. Promoting the
7 7
OpenAgents release itself remains operator-only behind `deployments:promote`,
8 8
and nothing described here can reach it. That surface is
9
`OpenAgents.Forge.Promotion` behind `POST /api/v3/admin/forge/targets`; it
9
`OpenAgents.Forge.Promotion` behind `POST /api/v1/admin/forge/targets`; it
10 10
requires the privileged scope *and* live operator standing, and it is
11 11
documented in [API authentication](api-authentication.md) and the
12 12
[production deploy runbook](operations/production-deploy-runbook.md).

@@ -121,7 +121,7 @@ host that cannot execute runs still records and evaluates them.

121 121
122 122
## API
123 123
124
The routes live under `/api/v3/repos/:owner/:repo` and require
124
The routes live under `/api/v1/repos/:owner/:repo` and require
125 125
`deployments:write` or a workflow grant. `forge:write` is not deployment
126 126
authority.
127 127
docs/forge-operator-independence.md modified +1 -1

@@ -104,7 +104,7 @@ uneven enough that it deserves a ledger rather than a sentence.

104 104
105 105
`OpenAgents.DataRights.ExportInventory` is that ledger. It classifies every
106 106
resource family the API publishes, plus the families that leave through routes
107
outside `/api/v3`, into four statuses:
107
outside `/api/v1`, into four statuses:
108 108
109 109
| Status | Meaning |
110 110
| --- | --- |
docs/forum-port.md modified +2 -2

@@ -67,7 +67,7 @@ Posts keep the identity they were written under: an `actor_ref` such as

67 67
68 68
A legacy identity attaches to an account through `forum_actor_links`
69 69
(`OpenAgents.Forum.ActorLink`). An account starts a claim at `/forum/claim`
70
(or `POST /api/v3/forum/claims`), which creates a `pending` link; an operator
70
(or `POST /api/v1/forum/claims`), which creates a `pending` link; an operator
71 71
approves or rejects it at `/admin/forum/claims`. Only links in status
72 72
`linked` resolve a post to an account (`OpenAgents.Forum.actor_user/1`), so
73 73
unclaimed and rejected identities stay attributed to their legacy display

@@ -89,7 +89,7 @@ Browser reads are public. An anonymous visitor reaches the board list, a

89 89
board, and a topic, and the sidebar row points every reader at the forum
90 90
rather than only signed-in accounts. Posting, claiming a legacy identity at
91 91
`/forum/claim`, and setting a tip destination at `/forum/tips` still need an
92
account. The `/api/v3/forum` reads are public, and writes require a
92
account. The `/api/v1/forum` reads are public, and writes require a
93 93
`forge:write` token.
94 94
95 95
No runtime dependency on `khala_sync_prod` remains. The only code that names
docs/github-api-issues-projects-assessment.md modified +10 -10

@@ -6,7 +6,7 @@ Status: Repository-scoped subset implemented; bounded compatibility gaps remain

6 6
7 7
## Intent
8 8
9
OpenAgents exposes a bounded `/api/v3` subset so familiar GitHub-shaped clients
9
OpenAgents exposes a bounded `/api/v1` subset so familiar GitHub-shaped clients
10 10
can interact with issues and project boards. The shape is a compatibility aid,
11 11
not a claim that the application implements the complete GitHub API.
12 12

@@ -44,7 +44,7 @@ them, and an advertised URL that answers `404` is worse than an absent one.

44 44
45 45
GitHub has no parameter that lists one kind without the other, so the
46 46
OpenAgents `type` filter (`issue`, `pull_request`, or `all`, defaulting to
47
`all`) is published under `issue.openagents` at `GET /api/v3` rather than
47
`all`) is published under `issue.openagents` at `GET /api/v1` rather than
48 48
presented as a GitHub parameter.
49 49
50 50
## Implemented Projects V2 subset

@@ -223,7 +223,7 @@ reading one refusal taught you nothing about the next.

223 223
  "message": "Validation Failed",
224 224
  "code": "validation_failed",
225 225
  "status": 422,
226
  "documentation_url": "https://openagents.com/api/v3",
226
  "documentation_url": "https://openagents.com/api/v1",
227 227
  "request_id": "GM5_fLaSSJDluDMAACUh",
228 228
  "errors": { "state": ["must be one of: open, closed, all"] }
229 229
}

@@ -234,7 +234,7 @@ reading one refusal taught you nothing about the next.

234 234
| `message` | The human sentence. A GitHub-shaped client reads this key, and a missing resource still reads exactly `Not Found`. |
235 235
| `code` | A stable identifier from the table below. Branch on this, not on `message`. |
236 236
| `status` | The HTTP status, repeated so a logged body is self-describing. |
237
| `documentation_url` | This deployment's `GET /api/v3`, which publishes the codes and the routes. |
237
| `documentation_url` | This deployment's `GET /api/v1`, which publishes the codes and the routes. |
238 238
| `request_id` | The value of the response's `x-request-id`. Name it when you report a failure. |
239 239
| `errors` | Field name to an array of messages. Always present, `{}` when the failure is not field-level. |
240 240

@@ -259,7 +259,7 @@ refusals on issue and comment creation, and the `401` from the bearer-token

259 259
pipelines. New clients read `code`. No key that a client already read has been
260 260
renamed or removed.
261 261
262
`GET /api/v3` publishes which routes answer with this envelope, under each
262
`GET /api/v1` publishes which routes answer with this envelope, under each
263 263
route's `errors` field. Routes marked `legacy` there still answer with their own
264 264
shape; the repository, deployment, box, forum, and agent families have not been
265 265
converged yet, and `priv/api-contracts/repositories-v1.json` remains the

@@ -268,13 +268,13 @@ superset of.

268 268
269 269
## Route inventory
270 270
271
`GET /api/v3` is the root document. It is derived from
271
`GET /api/v1` is the root document. It is derived from
272 272
`OpenAgentsWeb.Router.__routes__/0` through `OpenAgentsWeb.ApiRouteAuthority`,
273 273
never maintained beside it, so it cannot claim a route the router does not
274 274
serve or omit one it does.
275 275
276 276
```sh
277
openagents api /api/v3
277
openagents api /api/v1
278 278
```
279 279
280 280
It carries:

@@ -288,7 +288,7 @@ It carries:

288 288
```json
289 289
{
290 290
  "method": "GET",
291
  "path": "/api/v3/repos/{owner}/{repo}/issues/{issue_number}",
291
  "path": "/api/v1/repos/{owner}/{repo}/issues/{issue_number}",
292 292
  "authority": "optional_bearer",
293 293
  "family": "issue",
294 294
  "errors": "envelope",

@@ -334,7 +334,7 @@ bodies, response envelopes, Issues recipes, and Projects recipes.

334 334
335 335
These are current measured behaviors:
336 336
337
- `/api/v3` anonymous and optional-bearer reads and authenticated writes use
337
- `/api/v1` anonymous and optional-bearer reads and authenticated writes use
338 338
  separate pipelines.
339 339
  Writes require an expiring digest-only `oa_pat_…` bearer with exact
340 340
  `forge:write` scope. An authenticated person creates and revokes credentials

@@ -394,7 +394,7 @@ These are compatibility limits, not authorization fallbacks:

394 394
  objects; this one is a field-to-messages map, which is what the local
395 395
  clients already read.
396 396
- Families outside the issue family still answer with their own error shapes.
397
  `GET /api/v3` names which, so a client can tell without probing.
397
  `GET /api/v1` names which, so a client can tell without probing.
398 398
399 399
Closed on 2026-08-21, each pinned by tests: label rename through `new_name`,
400 400
create-on-add for missing labels at the issue-labels endpoint, 404 for
docs/issues-projects-ui-roadmap.md modified +1 -1

@@ -23,7 +23,7 @@ The Phoenix LiveView application currently ships these pages:

23 23
| Project list | `/:owner/:repo/projects` | Writable members |
24 24
| Project board | `/:owner/:repo/projects/:number` | Writable members |
25 25
26
The matching `/api/v3` issue, comment, label, assignee, milestone, and Projects
26
The matching `/api/v1` issue, comment, label, assignee, milestone, and Projects
27 27
V2 subset is implemented and covered. The dated
28 28
[coverage audit](2026-08-20-test-coverage-audit.md) records the original gaps
29 29
and the coverage added to close them. The
docs/issues-projects-work-plan.md modified +1 -1

@@ -26,7 +26,7 @@ the paired LiveViews are covered separately. The dated

26 26
The implementation tasks are no longer the readiness bottleneck. The resource
27 27
model must now be hardened rather than expanded from this checklist:
28 28
29
- `/api/v3` now has a deliberate CLI model: public reads are separate and every
29
- `/api/v1` now has a deliberate CLI model: public reads are separate and every
30 30
  write requires an expiring first-party bearer with `forge:write` scope.
31 31
- Owner/repository URL parameters do not yet map to a canonical repository row
32 32
  enforced throughout PostgreSQL.
docs/openagents-cli/api.md modified +10 -10

@@ -25,7 +25,7 @@ authenticated CLI session before it sends a request.

25 25
26 26
## Address a route
27 27
28
A relative path resolves under `/api/v3/`:
28
A relative path resolves under `/api/v1/`:
29 29
30 30
```sh
31 31
openagents api repos/OWNER/REPOSITORY/issues

@@ -35,7 +35,7 @@ These paths name the same route:

35 35
36 36
```text
37 37
repos/OWNER/REPOSITORY/issues
38
/api/v3/repos/OWNER/REPOSITORY/issues
38
/api/v1/repos/OWNER/REPOSITORY/issues
39 39
```
40 40
41 41
An absolute path must start with `/api/`. A complete URL must use the exact API

@@ -89,7 +89,7 @@ unique, lowercase handle:

89 89
curl -sS -X POST \
90 90
  -H 'Content-Type: application/json' \
91 91
  -d '{"handle":"release-bot","display_name":"Release bot"}' \
92
  https://openagents.com/api/v3/agents/register
92
  https://openagents.com/api/v1/agents/register
93 93
```
94 94
95 95
The response contains an `oa_agent_…` credential exactly once. Store it

@@ -99,7 +99,7 @@ securely and send it as a bearer token for the agent's participation routes:

99 99
export OPENAGENTS_AGENT_TOKEN='oa_agent_ID.SECRET'
100 100
curl -sS \
101 101
  -H "Authorization: Bearer $OPENAGENTS_AGENT_TOKEN" \
102
  https://openagents.com/api/v3/agent
102
  https://openagents.com/api/v1/agent
103 103
```
104 104
105 105
The credential carries only `agent:participate`. It can create a forum topic

@@ -129,7 +129,7 @@ curl -sS -X POST \

129 129
  -H "Authorization: Bearer $OPENAGENTS_AGENT_TOKEN" \
130 130
  -H 'Content-Type: application/json' \
131 131
  -d '{"user_id":"USER_UUID"}' \
132
  https://openagents.com/api/v3/agent/links
132
  https://openagents.com/api/v1/agent/links
133 133
134 134
openagents api agents/links
135 135
openagents api -X POST agents/links/LINK_ID/accept

@@ -341,7 +341,7 @@ refusal with the same envelope, so one script branch handles all of them:

341 341
  "message": "Validation Failed",
342 342
  "code": "validation_failed",
343 343
  "status": 422,
344
  "documentation_url": "https://openagents.com/api/v3",
344
  "documentation_url": "https://openagents.com/api/v1",
345 345
  "request_id": "GM5_fLaSSJDluDMAACUh",
346 346
  "errors": { "state": ["must be one of: open, closed, all"] }
347 347
}

@@ -362,10 +362,10 @@ the private resource exists.

362 362
363 363
## Discover the API
364 364
365
`GET /api/v3` describes the deployment you are talking to:
365
`GET /api/v1` describes the deployment you are talking to:
366 366
367 367
```sh
368
openagents api /api/v3
368
openagents api /api/v1
369 369
```
370 370
371 371
It publishes the OpenAgents extension fields, the error envelope and its codes,

@@ -376,7 +376,7 @@ the server does not serve.

376 376
377 377
## Forum endpoints
378 378
379
The forum surface lives under `/api/v3/forum`. Reads are public; writes need
379
The forum surface lives under `/api/v1/forum`. Reads are public; writes need
380 380
a `forge:write` API token and attribute posts to the token's account.
381 381
382 382
| Method | Path | Description |

@@ -465,7 +465,7 @@ with `"replayed": true` and the original target; replaying it with a different

465 465
body answers `409 idempotency_conflict`. A stale
466 466
`expected_current_target_id` answers `409 precondition_failed`.
467 467
468
Refusals use the shared `/api/v3` error envelope, so a script branches on
468
Refusals use the shared `/api/v1` error envelope, so a script branches on
469 469
`code`:
470 470
471 471
| Code | Status | Meaning |
docs/openagents-cli/command-reference.md modified +2 -2

@@ -219,9 +219,9 @@ openagents forum claims # check review status

219 219
openagents api [flags] <path>
220 220
```
221 221
222
A path without a leading slash resolves under `/api/v3/`. For example,
222
A path without a leading slash resolves under `/api/v1/`. For example,
223 223
`repos/OWNER/REPOSITORY/issues` and
224
`/api/v3/repos/OWNER/REPOSITORY/issues` name the same route. An absolute path
224
`/api/v1/repos/OWNER/REPOSITORY/issues` name the same route. An absolute path
225 225
must start with `/api/` and stay on the selected API origin.
226 226
227 227
| Flag | Description |
docs/openagents-cli/delete-repository.md modified +1 -1

@@ -30,7 +30,7 @@ npx --yes @openagentsinc/cli@latest repo delete OWNER/REPOSITORY --yes

30 30
```sh
31 31
curl --request DELETE \
32 32
  --header "Authorization: Bearer $OPENAGENTS_TOKEN" \
33
  https://openagents.com/api/v3/repos/OWNER/REPOSITORY
33
  https://openagents.com/api/v1/repos/OWNER/REPOSITORY
34 34
```
35 35
36 36
Success returns `204 No Content`.
docs/operations/production-deploy-runbook.md modified +4 -4

@@ -219,7 +219,7 @@ the scope by name:

219 219
curl --request POST \
220 220
  --header "Content-Type: application/json" \
221 221
  --data '{"scope": "deployments:promote"}' \
222
  https://openagents.com/api/v3/device/authorizations
222
  https://openagents.com/api/v1/device/authorizations
223 223
```
224 224
225 225
Open `/device` in a signed-in operator browser, confirm the code, and read the

@@ -229,7 +229,7 @@ requested scope shown on the approval page. Then exchange the device code:

229 229
curl --request POST \
230 230
  --header "Content-Type: application/json" \
231 231
  --data '{"device_code": "'"$DEVICE_CODE"'"}' \
232
  https://openagents.com/api/v3/device/authorizations/token
232
  https://openagents.com/api/v1/device/authorizations/token
233 233
```
234 234
235 235
Approval is refused unless the approving account is a current operator. A

@@ -249,7 +249,7 @@ curl --request POST \

249 249
    "idempotency_key": "release-2026-08-23-0001",
250 250
    "expected_current_target_id": "'"$CURRENT_TARGET_ID"'"
251 251
  }' \
252
  https://openagents.com/api/v3/admin/forge/targets
252
  https://openagents.com/api/v1/admin/forge/targets
253 253
```
254 254
255 255
The response is `202 Accepted` with the immutable target ID, the exact SHA,

@@ -262,7 +262,7 @@ openagents api admin/forge/targets/$TARGET_ID

262 262
openagents api admin/forge/targets
263 263
```
264 264
265
Refusals carry the shared `/api/v3` error envelope. Three codes matter during
265
Refusals carry the shared `/api/v1` error envelope. Three codes matter during
266 266
an incident:
267 267
268 268
- `idempotency_conflict` (409) — that key already names different bytes. Pick a
docs/product-promises-registry.md modified +3 -3

@@ -61,19 +61,19 @@ within a project. Every promise item has one canonical issue.

61 61
List promises by state:
62 62
63 63
```text
64
GET /api/v3/repos/{owner}/{repo}/projectsV2/{project_number}/items?promise_state=LIVE
64
GET /api/v1/repos/{owner}/{repo}/projectsV2/{project_number}/items?promise_state=LIVE
65 65
```
66 66
67 67
List bounty candidates:
68 68
69 69
```text
70
GET /api/v3/repos/{owner}/{repo}/projectsV2/{project_number}/items?bounty_candidate=true
70
GET /api/v1/repos/{owner}/{repo}/projectsV2/{project_number}/items?bounty_candidate=true
71 71
```
72 72
73 73
Read the append-only history for an item:
74 74
75 75
```text
76
GET /api/v3/repos/{owner}/{repo}/projectsV2/{project_number}/items/{item_id}/events?page=1
76
GET /api/v1/repos/{owner}/{repo}/projectsV2/{project_number}/items/{item_id}/events?page=1
77 77
```
78 78
79 79
Promise data appears under `openagents.promise`. Evidence that points to a
docs/projects/agent-continuity-and-memory/ISSUES.md added +133

@@ -0,0 +1,133 @@

1
# Issues: Persistent Agent Continuity & Epistemic Memory System
2
3
### Issue #1: Formal Engram Schema (NIP-AE Companion Profile) & Redaction Invariants
4
**Target:** `openagents` (`packages/agent-experience-memory`), `openagents.com`
5
**Type:** Specification & Core Invariants
6
**Labels:** `agent-ready`, `memory`, `security`
7
8
**Description:**
9
Establish the formal data structures and security guarantees for durable episodic memory events (engrams).
10
1. Define the OpenAgents companion profile for NIP-AE `kind:30174` addressable events.
11
2. Implement strict zero-credential / zero-secret redaction filters (tokens, private SSH/Nostr keys, private IPs, environment variables) that run strictly *prior* to engram signing.
12
3. Define the explicit supersession model: memories are immutable once signed; corrections append new engrams that explicitly reference and supersede previous event IDs.
13
14
**Acceptance Criteria:**
15
- Redaction test suite passing with 100% catch rate on synthetic tokens/secrets.
16
- Engram signature and supersession chain verification unit tests.
17
18
---
19
20
### Issue #2: Engram Client & Relay Sync Adapter in Agent SDK
21
**Target:** `openagents` (`packages/agent-experience-memory`)
22
**Type:** Feature
23
**Labels:** `agent-ready`, `sdk`, `nostr`
24
25
**Description:**
26
Implement the engram sync provider within `@openagentsinc/agent-experience-memory`.
27
1. Provide signing, symmetric encryption (scoped to owner conversation keys), and transport over Nostr relays.
28
2. Implement optimistic caching and background dispatch so engram persistence is completely non-blocking to interactive user turns.
29
3. Add degraded-mode resilience: turns proceed gracefully if the memory relay is unreachable.
30
31
**Acceptance Criteria:**
32
- SDK passes end-to-end integration test publishing encrypted engrams to a local relay.
33
- Synthetic network outage test confirms zero latency penalty or failure on agent turns.
34
35
---
36
37
### Issue #3: Deterministic Graph & Embedding Projection Worker
38
**Target:** `openagents.com` / `openagents`
39
**Type:** Feature
40
**Labels:** `agent-ready`, `indexer`, `cognee`
41
42
**Description:**
43
Build the projection layer that converts the append-only cryptographic engram log into a queryable semantic graph and vector index.
44
1. Ingest NIP-AE engrams from relay/storage and derive relational entities, attributes, and vector embeddings.
45
2. Ensure the projection layer is strictly ephemeral/derived: build an idempotent replay tool capable of cold-starting and regenerating the complete index from raw engram history.
46
47
**Acceptance Criteria:**
48
- Replay test takes raw engram stream and reconstructs an identical graph index.
49
- Conflicting/superseded engrams correctly invalidate stale nodes in the projection.
50
51
---
52
53
### Issue #4: Background Autonomous Consolidation ("Dreaming Loop")
54
**Target:** `openagents.com` (`OpenAgents.Memory.Consolidation`)
55
**Type:** Feature
56
**Labels:** `agent-ready`, `ai-research`, `background-jobs`
57
58
**Description:**
59
Build an asynchronous background worker that periodically reviews episodic engrams to synthesize high-level heuristics and lessons learned.
60
1. Identify fragmented episodic experiences across different sessions and cluster them by concept/topic.
61
2. Detect contradictions or outdated assumptions, proposing superseding synthesis engrams.
62
3. Compress multi-turn workflows into persistent heuristics (e.g., "when working on Phoenix routes in this repo, ensure reloader state is checked").
63
64
**Acceptance Criteria:**
65
- Background worker processes synthetic cluster of 20 episodic engrams into a single concise heuristic engram.
66
- Provenance links point back to source episodic engrams.
67
68
---
69
70
### Issue #5: Memory Evolution & Consent LiveView Dashboard
71
**Target:** `openagents.com` (`OpenAgentsWeb.MemoryLive`)
72
**Type:** UI / UX & Consent
73
**Labels:** `agent-ready`, `liveview`, `ui`
74
75
**Description:**
76
Provide an operator interface at `/memory` for auditing, inspecting, and managing persistent agent memory.
77
1. Visualize active memory graph, recent engrams, and derived heuristics.
78
2. Show full audit trail of supersessions and corrections.
79
3. Provide one-click retraction/forgetting tools that append cryptographically signed deletion/tombstone receipts.
80
81
**Acceptance Criteria:**
82
- LiveView renders memory timeline and graph nodes.
83
- Operator can view superseded history and trigger explicit memory redaction/tombstones.
84
85
---
86
87
### Issue #6: Scoped Memory Inheritance for Subagent Delegation
88
**Target:** `openagents` (`packages/openagents-cli/src/delegate`)
89
**Type:** Feature
90
**Labels:** `agent-ready`, `coder`, `delegate`
91
92
**Description:**
93
Enable child agents spawned via `delegate` (Devin, ox-alpha, gemini, etc.) to inherit relevant parent memory without leaking global context or credentials.
94
1. When `delegate` is invoked, query the parent memory graph for task-relevant heuristics based on the child's prompt.
95
2. Inject a sanitized "Epistemic Context" block into the child agent bootstrap prompt.
96
3. Ensure no private keys, conversation-private engrams, or unrelated session state leak to the subagent harness.
97
98
**Acceptance Criteria:**
99
- Child prompt includes relevant repository heuristics derived from previous sessions.
100
- Zero credential leakage confirmed by automated scanner.
101
102
---
103
104
### Issue #7: Child Engram Harvest & Parent Ledger Re-integration
105
**Target:** `openagents` (`packages/openagents-cli/src/delegate`)
106
**Type:** Feature
107
**Labels:** `agent-ready`, `coder`, `delegate`
108
109
**Description:**
110
Close the delegation loop by automatically harvesting discoveries, changes, and verification receipts from child agents and committing them to the parent agent's memory ledger.
111
1. Parse child execution artifacts, logs, and receipts upon task completion.
112
2. Construct and sign new episodic engrams representing the child's verified findings and diffs.
113
3. Publish engrams to the parent agent's stream so subsequent turns and sessions benefit from the subagent's work.
114
115
**Acceptance Criteria:**
116
- Parent memory graph reflects subagent's findings immediately after child completes.
117
- Trace receipts link child process run to newly created engrams.
118
119
---
120
121
### Issue #8: Unified Multimodal Activity Stream & Context Continuity
122
**Target:** `openagents.com` (`OpenAgents.Voice`, `OpenAgents.Chat`)
123
**Type:** Feature
124
**Labels:** `agent-ready`, `multimodal`, `voice`
125
126
**Description:**
127
Unify memory and activity context across modalities (Voice, CLI terminal sessions, Web LiveView chat).
128
1. Merge voice tool execution steps, CLI coder events, and LiveView text turns into a unified timeline schema.
129
2. Ensure switching between speaking on voice, coding in terminal, and reviewing on web shares the exact same active memory state and context window.
130
131
**Acceptance Criteria:**
132
- Conversation begun on voice can be continued seamlessly in CLI coder with full context recall.
133
- Ordered tool steps from all modalities display in a unified transcript view.
docs/projects/agent-continuity-and-memory/PROJECT.md added +48

@@ -0,0 +1,48 @@

1
# Project: Persistent Agent Continuity & Epistemic Memory System
2
3
## Overview
4
A comprehensive architecture and implementation plan for continuous agent memory, self-consolidation ("dreaming/consolidation" loop), epistemic heritage for delegated subagents, and cryptographic engram storage.
5
6
This project synthesizes the core principles of the Sarah architecture (Nostr NIP-AE signed engrams + rebuildable Cognee graph projection) with proactive memory consolidation and cross-agent shared heritage.
7
8
## Target Repositories & Modules
9
- `openagents.com`: LiveView memory management, Phoenix backend engram/projection sync, voice/text unified timeline.
10
- `openagents` (`packages/agent-experience-memory`, `packages/openagents-cli`): Core SDK memory adapters, CLI projection queries, delegate memory inheritance.
11
12
---
13
14
## Work Breakdown Structure & Issues
15
16
### Phase 1: Cryptographic Engram Ledger & Rebuildable Graph
17
- **Issue 1: [Spec & Invariants] Formal Engram Schema (NIP-AE Companion Profile) and Redaction Invariants**
18
  - Define schema for signed, encrypted kind:30174 events.
19
  - Implement zero-credential redaction pipeline before engram generation.
20
  - Specify supersession rules (corrections supersede rather than overwrite).
21
- **Issue 2: [SDK] `@openagentsinc/agent-experience-memory` Engram Client & Sync Provider**
22
  - Implement signed engram serialization, encryption, and sync to Nostr/Khala relay.
23
  - Ensure failure resilience (memory offline does not block the active conversation turn).
24
- **Issue 3: [Projection] Asynchronous Graph Indexer (Cognee / Vector + Relational)**
25
  - Implement deterministic graph projection worker replaying engram stream.
26
  - Support cold-start full rebuilds from raw engram history.
27
28
### Phase 2: Autonomous Memory Consolidation ("Dreaming Loop")
29
- **Issue 4: [Background Worker] Asynchronous Memory Synthesizer & Heuristic Compressor**
30
  - Periodic / idle background job evaluating recent episodic engrams.
31
  - Detects contradictions, clusters related episodic memories, and derives high-level behavioral heuristics.
32
  - Emits consolidated memory engrams linked back to parent sources.
33
- **Issue 5: [Consent & Audit] Memory Evolution Dashboard & supersession History**
34
  - LiveView UI in `openagents.com/memory` showing active graph, engram ledger, and derived heuristics.
35
  - Manual verification, correction, and consent retraction controls for the operator.
36
37
### Phase 3: Epistemic Lineage & Delegated Agent Memory
38
- **Issue 6: [Harness] Scoped Parent Memory Projection for `delegate` Tool**
39
  - When spawning child harnesses (`devin`, `ox-alpha`, `gemini`), package relevant memory heuristics into the child's bootstrap context.
40
  - Filter out parent secrets and irrelevant global states.
41
- **Issue 7: [Receipts] Child Engram Harvest & Parent Ledger Re-integration**
42
  - Collect structured findings and receipts from completed child agents.
43
  - Automatically sign and commit child discoveries as new engrams on the parent agent's ledger.
44
45
### Phase 4: Multimodal & Cross-Harness Continuity
46
- **Issue 8: [Unified Stream] Cross-Modality Timeline (Voice, CLI, Web, Agent-to-Agent)**
47
  - Unify voice tool step logs, CLI coder events, and LiveView chat messages into a single timeline stream.
48
  - Seamless context carry-over across UI, terminal, and speech interactions.
docs/pull-requests.md modified +5 -5

@@ -9,7 +9,7 @@ New repositories allow pull requests by default. `OpenAgentsInc/openagents.com`

9 9
A repository owner can change this setting in the repository's **Pull requests** panel or with the repository API:
10 10
11 11
```http
12
PATCH /api/v3/repos/{owner}/{repo}
12
PATCH /api/v1/repos/{owner}/{repo}
13 13
Authorization: Bearer {forge-write-token}
14 14
Content-Type: application/json
15 15

@@ -22,10 +22,10 @@ Maintainers, contributors, viewers, and unauthenticated callers cannot change th

22 22
23 23
Use these endpoints to work with pull requests:
24 24
25
- `GET /api/v3/repos/{owner}/{repo}/pulls`
26
- `GET /api/v3/repos/{owner}/{repo}/pulls/{pull_number}`
27
- `POST /api/v3/repos/{owner}/{repo}/pulls`
28
- `PATCH /api/v3/repos/{owner}/{repo}/pulls/{pull_number}`
25
- `GET /api/v1/repos/{owner}/{repo}/pulls`
26
- `GET /api/v1/repos/{owner}/{repo}/pulls/{pull_number}`
27
- `POST /api/v1/repos/{owner}/{repo}/pulls`
28
- `PATCH /api/v1/repos/{owner}/{repo}/pulls/{pull_number}`
29 29
30 30
Create a pull request with a title, optional body, source repository, source ref, and optional base ref:
31 31
docs/taxonomy.md modified +14 -14

@@ -85,7 +85,7 @@ forge behind a mirror it does not know about.

85 85
86 86
### GitHub-shaped API
87 87
88
**`/api/v3`** — a bounded, GitHub-shaped REST subset served by this
88
**`/api/v1`** — a bounded, GitHub-shaped REST subset served by this
89 89
application: issues, comments, labels, assignees, milestones, and Projects V2
90 90
(`projectsV2`). It mimics GitHub REST shapes as a compatibility aid and
91 91
implements only a subset of the real API. Authorization here comes from API

@@ -329,9 +329,9 @@ report, with an append-only transcript in `thread_events`. Model authority

329 329
binds to it: an inference grant names a thread or a conversation, never both
330 330
and never neither (THREAD-001). The context is `OpenAgents.Threads`, the record
331 331
is `OpenAgents.Threads.Thread`, and the transcript entry is
332
`OpenAgents.Threads.Event`. A caller opens one with `POST /api/v3/threads`,
333
reads what it has spent with `GET /api/v3/threads/{thread_id}`, and revokes it
334
with `DELETE /api/v3/threads/{thread_id}`, all behind the `chat:account` scope
332
`OpenAgents.Threads.Event`. A caller opens one with `POST /api/v1/threads`,
333
reads what it has spent with `GET /api/v1/threads/{thread_id}`, and revokes it
334
with `DELETE /api/v1/threads/{thread_id}`, all behind the `chat:account` scope
335 335
and served by `OpenAgentsWeb.ThreadController`. The CLI that stops writing to
336 336
the conversation is still to come; see the audit in
337 337
`docs/2026-08-23-thread-primitive-audit.md`.

@@ -420,7 +420,7 @@ model.

420 420
**Deployment control plane** — the tenant-facing plane where a repository
421 421
deploys its own commits to its own environments: environments, requests, runs,
422 422
checks, approvals, workflow grants, and append-only deployment events, backed by
423
`OpenAgents.Deployments` and served at `/api/v3` under `deployments:write`. It
423
`OpenAgents.Deployments` and served at `/api/v1` under `deployments:write`. It
424 424
is not the forge deployment plane. Promoting the OpenAgents release itself stays
425 425
operator-only behind `deployments:promote`, and no tenant route reaches it.
426 426
`docs/deployment-control-plane.md` describes the contract.

@@ -461,7 +461,7 @@ status means a worker said it would try; only `done` means it ran (EFFECT-002).

461 461
462 462
**Delegation** — one unit of work this application hands to a substrate it
463 463
does not run inline. `OpenAgents.Delegations` serves all of them at
464
`/api/v3/conversations/{conversation_id}/delegations`: one request shape, one
464
`/api/v1/conversations/{conversation_id}/delegations`: one request shape, one
465 465
status read, one cancel, across every target kind. The facade stores no
466 466
delegation state. The Box run ledger and the `work_jobs` ledger stay
467 467
authoritative, and every projection is derived from them.

@@ -471,8 +471,8 @@ identifier and is never inferred: a target is `box:{uuid}` or

471 471
`computer:{uuid}`, and a delegation is `box-run:{uuid}` or
472 472
`computer-job:{uuid}`. Authority is scoped per kind — the `box:control` and
473 473
`computer:control` token scopes for a signed-in owner, and a per-kind grant to
474
a named agent handle at `/api/v3/agents/{handle}/box-control` and
475
`/api/v3/agents/{handle}/computer-control`.
474
a named agent handle at `/api/v1/agents/{handle}/box-control` and
475
`/api/v1/agents/{handle}/computer-control`.
476 476
477 477
**Box** — a sandbox virtual machine this application rents from the Box Public
478 478
API v1 at `ascii.dev` and bootstraps with the OpenCode harness. A Box is

@@ -564,7 +564,7 @@ its own contracts:

564 564
  tool steps still replay against them, and the `machines` array key of
565 565
  `computer_list.v1`.
566 566
- The response keys `machine_id` and `machine_name` on
567
  `GET /api/v3/computer-agent-jobs/{id}`, the assignment projection, and
567
  `GET /api/v1/computer-agent-jobs/{id}`, the assignment projection, and
568 568
  `GET /controller/pairings/{id}`; and `counts.machines_connected` in
569 569
  `GET /api/status`, which `STATUS-001` pins as an exact published key.
570 570
- The `work_jobs` JSONB keys `delegation->>'machine_id'`,

@@ -601,7 +601,7 @@ its own contracts:

601 601
  `:machine_token_ttl_seconds`, which live in staging and production secrets.
602 602
603 603
**`machine` also means machine-readable** — and that sense is correct, not
604
residue. `GET /api/v3` publishes `contribution.machine`, and `/agents.json`
604
residue. `GET /api/v1` publishes `contribution.machine`, and `/agents.json`
605 605
publishes `representations.machine`, each paired with `human` and each naming a
606 606
format rather than a device. Do not sweep these into a computer rename; they
607 607
are not about computers at all.

@@ -614,7 +614,7 @@ string is on before you rename it.

614 614
615 615
**Issues work system** — first-party issues, labels, milestones, assignees,
616 616
and projects shaped like GitHub's Projects V2, backed by `OpenAgents.Issues`
617
and sibling contexts, served at `/api/v3`. Tests are the contract; the
617
and sibling contexts, served at `/api/v1`. Tests are the contract; the
618 618
assessment document `docs/github-api-issues-projects-assessment.md` is the
619 619
source of truth for paths and JSON shape.
620 620

@@ -665,7 +665,7 @@ the plan; it is not a routine outcome and not a request you can make.

665 665
**Forum** — the first-party discussion surface at `/forum`: boards, topics,
666 666
and posts backed by `OpenAgents.Forum`, ported from the legacy Effect forum
667 667
by a one-time import (`mix openagents.forum.import`). Browser reads are public,
668
as are the `/api/v3/forum` reads; writing a topic or a post, and claiming a
668
as are the `/api/v1/forum` reads; writing a topic or a post, and claiming a
669 669
legacy identity, need an account. `docs/forum-port.md` describes the port;
670 670
`docs/evidence/forum-port-migration.md` records the import.
671 671

@@ -704,7 +704,7 @@ limits and the date of measurement; provider quotas move without notice.

704 704
705 705
**Backend** — one model and the adapter that reaches it, named by a stable id
706 706
a client sends and a label a person reads. `OpenAgents.Chat.Backends` is the
707
single list; `GET /api/v3` publishes the ids so a client discovers the set
707
single list; `GET /api/v1` publishes the ids so a client discovers the set
708 708
instead of hardcoding it. Say backend for the choice and model for the thing
709 709
chosen: Ox Alpha and Gemini 3.7 Flash are models, and each is reached through
710 710
one backend. A backend is not a provider — OpenRouter and Google are

@@ -756,7 +756,7 @@ is exactly one component system; adding a second is forbidden.

756 756
   repo" where both could apply.
757 757
2. **A push is not a deploy.** Promotion requires an operator-authorized
758 758
   target; no Git event promotes anything.
759
3. **GitHub-shaped is not GitHub.** Say "the `/api/v3` surface" or "GitHub"
759
3. **GitHub-shaped is not GitHub.** Say "the `/api/v1` surface" or "GitHub"
760 760
   depending on which server answers.
761 761
4. **Computer, not machine.** Product copy and new code both say `computer`.
762 762
   A `machine` name stays only where something outside this release can observe
lib/openagents/chat/backends.ex modified +1 -1

@@ -5,7 +5,7 @@ defmodule OpenAgents.Chat.Backends do

5 5
  A backend is a model and the adapter that reaches it, named by a stable id a
6 6
  client sends and a label a person reads. Everything that needs to know the
7 7
  set reads it from here: the turn runtime picks the adapter, the chat API
8
  refuses an id that is not in the list, and `GET /api/v3` publishes the ids so
8
  refuses an id that is not in the list, and `GET /api/v1` publishes the ids so
9 9
  a client discovers the choice instead of hardcoding it. Adding a backend is
10 10
  one entry in `@backends`, and every one of those surfaces follows.
11 11
lib/openagents/data_rights/account_export.ex modified +1 -1

@@ -988,7 +988,7 @@ defmodule OpenAgents.DataRights.AccountExport do

988 988
        "reason" =>
989 989
          "Posts written under a legacy actor_ref this account has not claimed, or whose claim " <>
990 990
            "is still pending, are not exported. Only a linked claim resolves a legacy identity.",
991
        "mechanism" => "POST /api/v3/forum/claims",
991
        "mechanism" => "POST /api/v1/forum/claims",
992 992
        "issue" => nil
993 993
      }
994 994
    ]
lib/openagents/data_rights/export_inventory.ex modified +36 -13

@@ -14,7 +14,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

14 14
  `OpenAgentsWeb.ApiRouteAuthority.families/0` publishes must appear here, so a
15 15
  new resource family cannot reach the API without someone deciding whether a
16 16
  user can export it. The rest of the ledger names the families that leave
17
  through routes outside `/api/v3` — Git transport for repository content, the
17
  through routes outside `/api/v1` — Git transport for repository content, the
18 18
  data-rights exports for conversations and memory, and `GET
19 19
  /data/export/account` for the forge-owned and forum-owned records an account
20 20
  authors.

@@ -62,7 +62,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

62 62
      family: :issue,
63 63
      api?: true,
64 64
      status: :portable,
65
      mechanism: "GET /api/v3/repos/{owner}/{repo}/issues",
65
      mechanism: "GET /api/v1/repos/{owner}/{repo}/issues",
66 66
      proof: :inventory,
67 67
      issue: nil,
68 68
      note:

@@ -74,7 +74,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

74 74
      family: :project,
75 75
      api?: true,
76 76
      status: :portable,
77
      mechanism: "GET /api/v3/repos/{owner}/{repo}/projectsV2",
77
      mechanism: "GET /api/v1/repos/{owner}/{repo}/projectsV2",
78 78
      proof: :inventory,
79 79
      issue: nil,
80 80
      note: "Widens for a member the same way issues do; returns the full set unpaged."

@@ -83,7 +83,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

83 83
      family: :repository,
84 84
      api?: true,
85 85
      status: :portable,
86
      mechanism: "GET /api/v3/user/repos",
86
      mechanism: "GET /api/v1/user/repos",
87 87
      proof: :inventory,
88 88
      issue: nil,
89 89
      note: "The one account-wide list the API publishes. Cursor paged, so it enumerates."

@@ -92,7 +92,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

92 92
      family: :comment,
93 93
      api?: true,
94 94
      status: :portable,
95
      mechanism: "GET /api/v3/repos/{owner}/{repo}/issues/{issue_number}/comments",
95
      mechanism: "GET /api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments",
96 96
      proof: :inventory,
97 97
      issue: nil,
98 98
      note:

@@ -104,7 +104,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

104 104
      family: :label,
105 105
      api?: true,
106 106
      status: :portable,
107
      mechanism: "GET /api/v3/repos/{owner}/{repo}/labels",
107
      mechanism: "GET /api/v1/repos/{owner}/{repo}/labels",
108 108
      proof: :inventory,
109 109
      issue: nil,
110 110
      note: "Widens for a member the same way issues do; returns the full set unpaged."

@@ -113,7 +113,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

113 113
      family: :milestone,
114 114
      api?: true,
115 115
      status: :portable,
116
      mechanism: "GET /api/v3/repos/{owner}/{repo}/milestones",
116
      mechanism: "GET /api/v1/repos/{owner}/{repo}/milestones",
117 117
      proof: :inventory,
118 118
      issue: nil,
119 119
      note: "Widens for a member, with the open and closed issue counts each milestone carries."

@@ -122,7 +122,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

122 122
      family: :assignee,
123 123
      api?: true,
124 124
      status: :portable,
125
      mechanism: "GET /api/v3/repos/{owner}/{repo}/assignees",
125
      mechanism: "GET /api/v1/repos/{owner}/{repo}/assignees",
126 126
      proof: :inventory,
127 127
      issue: nil,
128 128
      note:

@@ -133,7 +133,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

133 133
      family: :issue_label,
134 134
      api?: true,
135 135
      status: :portable,
136
      mechanism: "GET /api/v3/repos/{owner}/{repo}/issues/{issue_number}/labels",
136
      mechanism: "GET /api/v1/repos/{owner}/{repo}/issues/{issue_number}/labels",
137 137
      proof: :inventory,
138 138
      issue: nil,
139 139
      note: "Widens for a member, one issue at a time."

@@ -142,7 +142,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

142 142
      family: :issue_assignee,
143 143
      api?: true,
144 144
      status: :portable,
145
      mechanism: "GET /api/v3/repos/{owner}/{repo}/issues/{issue_number}/assignees",
145
      mechanism: "GET /api/v1/repos/{owner}/{repo}/issues/{issue_number}/assignees",
146 146
      proof: :inventory,
147 147
      issue: nil,
148 148
      note: "Widens for a member, one issue at a time."

@@ -158,7 +158,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

158 158
        "The durable records behind the chat family are conversation messages, " <>
159 159
          "turns, tool steps, and the account chat backend's own runs and event " <>
160 160
          "stream, all of which leave through the DATA-004 export rather than " <>
161
          "through /api/v3."
161
          "through /api/v1."
162 162
    },
163 163
    %{
164 164
      family: :repository_content,

@@ -201,7 +201,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

201 201
        "forge_pushes.principal is user:<account-id> for a person's push, so " <>
202 202
          "the account export returns exactly the account's own rows with the " <>
203 203
          "WAL sequence and ref map a clone does not carry. A repository-scoped " <>
204
          "read is published too, GET /api/v3/repos/{owner}/{repo}/pushes, " <>
204
          "read is published too, GET /api/v1/repos/{owner}/{repo}/pushes, " <>
205 205
          "proven by " <>
206 206
          "test/openagents_web/controllers/push_receipt_controller_test.exs. It " <>
207 207
          "serves the WAL's own entries rather than the derived rows, so it " <>

@@ -340,6 +340,29 @@ defmodule OpenAgents.DataRights.ExportInventory do

340 340
          "test OpenAgentsWeb.ReputationController applies."
341 341
    },
342 342
343
    # ── a record a user authors that nothing gives back ───────────────────
344
    %{
345
      family: :trace,
346
      api?: true,
347
      status: :blocked,
348
      # There is none. `POST /api/v1/traces` is the whole surface: a person
349
      # uploads their own agent transcript and no route reads one back, not
350
      # their own and not by id. That is a record they authored, held on their
351
      # behalf, with no way to take it with them.
352
      mechanism: nil,
353
      proof: :inventory,
354
      # #217 owns this surface — it is the issue the upload route was built
355
      # under — and the missing read is part of it. Named rather than opening a
356
      # second issue for half of one surface.
357
      issue: 217,
358
      note:
359
        "Write-only. `POST /api/v1/traces` accepts an ATIF trace and nothing " <>
360
          "reads one back, not by id and not for the owner, so an uploaded " <>
361
          "trace cannot be exported. Classified when the route landed rather " <>
362
          "than left undeclared, because an unclassified family is one the " <>
363
          "export question never reaches."
364
    },
365
343 366
    # ── not a record a user authors and takes with them ───────────────────
344 367
    %{
345 368
      family: :meta,

@@ -438,7 +461,7 @@ defmodule OpenAgents.DataRights.ExportInventory do

438 461
  @spec with_status(status()) :: [entry()]
439 462
  def with_status(status), do: Enum.filter(@entries, &(&1.status == status))
440 463
441
  @doc "The families this ledger claims cover the published `/api/v3` surface."
464
  @doc "The families this ledger claims cover the published `/api/v1` surface."
442 465
  @spec api_families() :: [atom()]
443 466
  def api_families do
444 467
    @entries |> Enum.filter(& &1.api?) |> Enum.map(& &1.family) |> Enum.sort()
lib/openagents/forge/git_http.ex modified +1 -1

@@ -474,7 +474,7 @@ defmodule OpenAgents.Forge.GitHTTP do

474 474
    append_side_band(
475 475
      output,
476 476
      "openagents wal-receipt seq=#{seq} link=#{link}" <>
477
        " (GET /api/v3/repos/#{repository.owner}/#{repository.name}/pushes/#{seq})"
477
        " (GET /api/v1/repos/#{repository.owner}/#{repository.name}/pushes/#{seq})"
478 478
    )
479 479
  rescue
480 480
    error ->
lib/openagents/inference/models.ex modified +2 -2

@@ -8,7 +8,7 @@ defmodule OpenAgents.Inference.Models do

8 8
  set from one config-driven list, `config :openagents, :model_catalog`:
9 9
  `OpenAgents.Threads` admits a thread's model against it, `OpenAgents.Inference.mint/1`
10 10
  refuses a grant naming anything else, `OpenAgentsWeb.InferenceProxyController`
11
  asks it which adapter to call, and `GET /api/v3/models` publishes it so a
11
  asks it which adapter to call, and `GET /api/v1/models` publishes it so a
12 12
  client selects from what is actually served instead of guessing.
13 13
14 14
  Two names appear per model and they are not the same name. The `id` is what

@@ -108,7 +108,7 @@ defmodule OpenAgents.Inference.Models do

108 108
  end
109 109
110 110
  @doc """
111
  The public projection of the catalog, for `GET /api/v3/models`.
111
  The public projection of the catalog, for `GET /api/v1/models`.
112 112
113 113
  No adapter module and no credential state beyond the availability word: a
114 114
  client learns what it can select and what each selection can carry, nothing
lib/openagents/issues.ex modified +1 -1

@@ -7,7 +7,7 @@ defmodule OpenAgents.Issues do

7 7
  therefore takes a `:type` option, and it defaults to `"issue"`: a list this
8 8
  module returns is issues unless the caller asks for more, so a count labelled
9 9
  "Issues" counts issues. `OpenAgentsWeb.IssueController` passes `"all"` to keep
10
  `GET /api/v3/repos/:owner/:repo/issues` shaped like GitHub's, which returns
10
  `GET /api/v1/repos/:owner/:repo/issues` shaped like GitHub's, which returns
11 11
  both and marks the pull requests with a `pull_request` object.
12 12
  """
13 13
lib/openagents/issues/capture.ex modified +1 -1

@@ -4,7 +4,7 @@ defmodule OpenAgents.Issues.Capture do

4 4
5 5
  This module is the whole of the behavior. The chat tool
6 6
  (`OpenAgents.Tools.IssueCapture`) and the authenticated API operation
7
  (`POST /api/v3/repos/:owner/:repo/issues/capture`) are two transports over
7
  (`POST /api/v1/repos/:owner/:repo/issues/capture`) are two transports over
8 8
  it, so the two cannot drift: a refusal the tool gives is the refusal the API
9 9
  gives, and a draft the API writes is the draft the tool writes.
10 10
lib/openagents/projects.ex modified +1 -1

@@ -992,7 +992,7 @@ defmodule OpenAgents.Projects do

992 992
  same way every other project write does. A card's `Status` is a stored field
993 993
  value, so a board that did not hear about this write would keep rendering the
994 994
  card in the column it left — the write path, not the caller, owns the
995
  announcement, so a change made over `/api/v3` and a change made from the
995
  announcement, so a change made over `/api/v1` and a change made from the
996 996
  board produce the same event.
997 997
  """
998 998
  def update_project_item(%ProjectItem{} = item, attrs, actor \\ nil) do
lib/openagents/threads.ex modified +12 -6

@@ -171,7 +171,13 @@ defmodule OpenAgents.Threads do

171 171
      # nine open threads behind an eight-thread promise (issue #195).
172 172
      _serialized = lock_owner(repo, visitor_id)
173 173
174
      if open_count(visitor_id) >= maximum_open_per_account() do
174
      ceiling = maximum_open_per_account()
175
176
      # `nil` is unbounded. A session no longer destroys its thread on the way
177
      # out, so a count of open threads is a count of every session the account
178
      # has ever run — and refusing the ninth would refuse the work rather than
179
      # bound it.
180
      if ceiling != nil and open_count(visitor_id) >= ceiling do
175 181
        {:error, :thread_quota_reached}
176 182
      else
177 183
        {:ok, :admitted}

@@ -248,7 +254,7 @@ defmodule OpenAgents.Threads do

248 254
  reader has the fact without asking again.
249 255
250 256
  The audience of a wide tier is *any signed-in account holding the thread's
251
  id*: both surfaces that call this — `GET /api/v3/threads/{thread_id}` and
257
  id*: both surfaces that call this — `GET /api/v1/threads/{thread_id}` and
252 258
  `/threads/:id` — require an authenticated principal, and this function does
253 259
  not invent an anonymous one (THREAD-002).
254 260
  """

@@ -540,7 +546,7 @@ defmodule OpenAgents.Threads do

540 546
541 547
  Read from `config/config.exs` and stated separately from the delegation
542 548
  ceilings in `OpenAgents.Inference`, because a thread's budget is not a
543
  delegation's budget. `GET /api/v3` publishes this map, so a client reads the
549
  delegation's budget. `GET /api/v1` publishes this map, so a client reads the
544 550
  budget it was given rather than discovering it by exhausting it.
545 551
546 552
  The cost figure here is the configured per-thread cap. What a particular

@@ -589,9 +595,9 @@ defmodule OpenAgents.Threads do

589 595
    end
590 596
  end
591 597
592
  @doc "How many threads one account may hold open at once."
593
  @spec maximum_open_per_account() :: pos_integer()
594
  def maximum_open_per_account, do: setting(:maximum_open_threads_per_account, 8)
598
  @doc "How many threads one account may hold open at once, or `nil` for no limit."
599
  @spec maximum_open_per_account() :: pos_integer() | nil
600
  def maximum_open_per_account, do: setting(:maximum_open_threads_per_account, nil)
595 601
596 602
  @doc "How many threads this account currently holds open."
597 603
  @spec open_count(User.t() | Visitor.t() | String.t()) :: non_neg_integer()
lib/openagents/tools/issue_capture.ex modified +1 -1

@@ -3,7 +3,7 @@ defmodule OpenAgents.Tools.IssueCapture do

3 3
  Files one unmet request from a conversation as a scoped forge issue.
4 4
5 5
  The behavior lives in `OpenAgents.Issues.Capture`, which
6
  `POST /api/v3/repos/:owner/:repo/issues/capture` also calls, so the tool and
6
  `POST /api/v1/repos/:owner/:repo/issues/capture` also calls, so the tool and
7 7
  the API operation cannot drift apart.
8 8
9 9
  ## Why this one is `:external_effect`
lib/openagents_web/api_error.ex modified +4 -4

@@ -1,6 +1,6 @@

1 1
defmodule OpenAgentsWeb.ApiError do
2 2
  @moduledoc """
3
  One error envelope for the issue-family `/api/v3` routes.
3
  One error envelope for the issue-family `/api/v1` routes.
4 4
5 5
  Before this module a caller met six incompatible bodies for the same class of
6 6
  failure: `message` for a missing resource, `errors` for a rejected field,

@@ -17,7 +17,7 @@ defmodule OpenAgentsWeb.ApiError do

17 17
      to branch. This is what gives a CLI distinct exit results.
18 18
    * `status` — the HTTP status, repeated in the body so a logged envelope is
19 19
      self-describing.
20
    * `documentation_url` — the published contract at `GET /api/v3`, which
20
    * `documentation_url` — the published contract at `GET /api/v1`, which
21 21
      enumerates every code and the routes that use this envelope.
22 22
    * `request_id` — the endpoint's `x-request-id`, so a report names one
23 23
      request.

@@ -103,7 +103,7 @@ defmodule OpenAgentsWeb.ApiError do

103 103
  @doc """
104 104
  Every stable error code and the one status it maps to.
105 105
106
  `OpenAgentsWeb.ApiExtensionController` publishes this table at `GET /api/v3`,
106
  `OpenAgentsWeb.ApiExtensionController` publishes this table at `GET /api/v1`,
107 107
  so a client reads the codes it must handle rather than collecting them from
108 108
  runtime examples.
109 109
  """

@@ -226,7 +226,7 @@ defmodule OpenAgentsWeb.ApiError do

226 226
  end
227 227
228 228
  defp documentation_url do
229
    OpenAgentsWeb.Endpoint.url() |> String.trim_trailing("/") |> Kernel.<>("/api/v3")
229
    OpenAgentsWeb.Endpoint.url() |> String.trim_trailing("/") |> Kernel.<>("/api/v1")
230 230
  end
231 231
232 232
  defp translate({message, options}) do
lib/openagents_web/api_route_authority.ex modified +194 -192

@@ -1,10 +1,10 @@

1 1
defmodule OpenAgentsWeb.ApiRouteAuthority do
2 2
  @moduledoc """
3
  The single authority inventory for every `/api/v3` route.
3
  The single authority inventory for every `/api/v1` route.
4 4
5 5
  Each entry names one route and the principal that reaches it. The
6 6
  `OpenAgentsWeb.ApiRouteAuthorityTest` proves two ways that this inventory and
7
  the router agree: every live `/api/v3` route appears here exactly once, so a
7
  the router agree: every live `/api/v1` route appears here exactly once, so a
8 8
  new unclassified route fails CI, and each classification matches what the
9 9
  enforcing pipeline actually does to an anonymous request.
10 10

@@ -23,7 +23,7 @@ defmodule OpenAgentsWeb.ApiRouteAuthority do

23 23
24 24
  Families name the resource the route serves. They group routes that must
25 25
  agree with one another, and `OpenAgentsWeb.ApiExtensionController` publishes
26
  them at `GET /api/v3` so a client can find every route of a family without
26
  them at `GET /api/v1` so a client can find every route of a family without
27 27
  guessing at paths.
28 28
29 29
  Error contracts say which refusal shape the route answers with:

@@ -41,7 +41,7 @@ defmodule OpenAgentsWeb.ApiRouteAuthority do

41 41
  @type error_contract :: :envelope | :legacy
42 42
  @type entry :: {principal(), atom(), error_contract()}
43 43
44
  @doc "The authority classification for one `METHOD /path` API v3 route."
44
  @doc "The authority classification for one `METHOD /path` API v1 route."
45 45
  @spec authority(String.t(), String.t()) :: principal() | nil
46 46
  def authority(verb, path) do
47 47
    case Map.get(inventory(), "#{verb} #{path}") do

@@ -50,7 +50,7 @@ defmodule OpenAgentsWeb.ApiRouteAuthority do

50 50
    end
51 51
  end
52 52
53
  @doc "The resource family one `METHOD /path` API v3 route belongs to."
53
  @doc "The resource family one `METHOD /path` API v1 route belongs to."
54 54
  @spec family(String.t(), String.t()) :: atom() | nil
55 55
  def family(verb, path) do
56 56
    case Map.get(inventory(), "#{verb} #{path}") do

@@ -59,7 +59,7 @@ defmodule OpenAgentsWeb.ApiRouteAuthority do

59 59
    end
60 60
  end
61 61
62
  @doc "The error contract one `METHOD /path` API v3 route answers with."
62
  @doc "The error contract one `METHOD /path` API v1 route answers with."
63 63
  @spec error_contract(String.t(), String.t()) :: error_contract() | nil
64 64
  def error_contract(verb, path) do
65 65
    case Map.get(inventory(), "#{verb} #{path}") do

@@ -79,7 +79,7 @@ defmodule OpenAgentsWeb.ApiRouteAuthority do

79 79
  @doc """
80 80
  Every classified route as a map, sorted by path then method.
81 81
82
  This is what `GET /api/v3` publishes. Paths are rewritten from the router's
82
  This is what `GET /api/v1` publishes. Paths are rewritten from the router's
83 83
  `:owner` form into the `{owner}` form the rest of the API description uses,
84 84
  so a client reads one spelling everywhere.
85 85
  """

@@ -117,299 +117,301 @@ defmodule OpenAgentsWeb.ApiRouteAuthority do

117 117
  defp inventory do
118 118
    %{
119 119
      # Anonymous by design: the extension index is a public API description.
120
      "get /api/v3" => {:anonymous, :meta, :legacy},
120
      "get /api/v1" => {:anonymous, :meta, :legacy},
121 121
      # Anonymous by design: device authorization bootstraps credentials.
122
      "post /api/v3/device/authorizations" => {:anonymous, :device, :legacy},
123
      "post /api/v3/device/authorizations/token" => {:anonymous, :device, :legacy},
124
      "post /api/v3/agents/register" => {:anonymous, :agent, :legacy},
125
      "get /api/v3/agents/:handle" => {:anonymous, :agent, :legacy},
122
      "post /api/v1/device/authorizations" => {:anonymous, :device, :legacy},
123
      "post /api/v1/device/authorizations/token" => {:anonymous, :device, :legacy},
124
      "post /api/v1/agents/register" => {:anonymous, :agent, :legacy},
125
      "get /api/v1/agents/:handle" => {:anonymous, :agent, :legacy},
126 126
      # pipe_through :optional_forge_api — public reads, bearer-widened.
127 127
      # The ancillary issue metadata reads. Anonymous callers still read public
128 128
      # repositories; a bearer token widens them to the private repositories
129 129
      # its account may read, so an owner can read their own issue's comments.
130
      "get /api/v3/repos/:owner/:repo/issues/:issue_number/comments" =>
130
      "get /api/v1/repos/:owner/:repo/issues/:issue_number/comments" =>
131 131
        {:optional_bearer, :comment, :envelope},
132
      "get /api/v3/repos/:owner/:repo/issues/comments/:id" =>
132
      "get /api/v1/repos/:owner/:repo/issues/comments/:id" =>
133 133
        {:optional_bearer, :comment, :envelope},
134
      "get /api/v3/repos/:owner/:repo/issues/:issue_number/labels" =>
134
      "get /api/v1/repos/:owner/:repo/issues/:issue_number/labels" =>
135 135
        {:optional_bearer, :issue_label, :envelope},
136
      "get /api/v3/repos/:owner/:repo/issues/:issue_number/assignees" =>
136
      "get /api/v1/repos/:owner/:repo/issues/:issue_number/assignees" =>
137 137
        {:optional_bearer, :issue_assignee, :envelope},
138
      "get /api/v3/repos/:owner/:repo/labels" => {:optional_bearer, :label, :envelope},
139
      "get /api/v3/repos/:owner/:repo/labels/:name" => {:optional_bearer, :label, :envelope},
140
      "get /api/v3/repos/:owner/:repo/milestones" => {:optional_bearer, :milestone, :envelope},
141
      "get /api/v3/repos/:owner/:repo/milestones/:milestone_number" =>
138
      "get /api/v1/repos/:owner/:repo/labels" => {:optional_bearer, :label, :envelope},
139
      "get /api/v1/repos/:owner/:repo/labels/:name" => {:optional_bearer, :label, :envelope},
140
      "get /api/v1/repos/:owner/:repo/milestones" => {:optional_bearer, :milestone, :envelope},
141
      "get /api/v1/repos/:owner/:repo/milestones/:milestone_number" =>
142 142
        {:optional_bearer, :milestone, :envelope},
143
      "get /api/v3/repos/:owner/:repo/assignees" => {:optional_bearer, :assignee, :envelope},
144
      "get /api/v3/repos/:owner/:repo/assignees/:assignee" =>
143
      "get /api/v1/repos/:owner/:repo/assignees" => {:optional_bearer, :assignee, :envelope},
144
      "get /api/v1/repos/:owner/:repo/assignees/:assignee" =>
145 145
        {:optional_bearer, :assignee, :envelope},
146
      "get /api/v3/repos/:owner/:repo/pushes" => {:optional_bearer, :push_receipt, :envelope},
147
      "get /api/v3/repos/:owner/:repo/pushes/:wal_seq" =>
146
      "get /api/v1/repos/:owner/:repo/pushes" => {:optional_bearer, :push_receipt, :envelope},
147
      "get /api/v1/repos/:owner/:repo/pushes/:wal_seq" =>
148 148
        {:optional_bearer, :push_receipt, :envelope},
149
      "get /api/v3/forum" => {:optional_bearer, :forum, :legacy},
150
      "get /api/v3/forum/topics" => {:optional_bearer, :forum, :legacy},
151
      "get /api/v3/forum/topics/:id" => {:optional_bearer, :forum, :legacy},
152
      "get /api/v3/repos/:owner/:repo" => {:optional_bearer, :repository, :legacy},
153
      "get /api/v3/repos/:owner/:repo/issues" => {:optional_bearer, :issue, :envelope},
154
      "get /api/v3/repos/:owner/:repo/issues/:issue_number" =>
149
      "get /api/v1/forum" => {:optional_bearer, :forum, :legacy},
150
      "get /api/v1/forum/topics" => {:optional_bearer, :forum, :legacy},
151
      "get /api/v1/forum/topics/:id" => {:optional_bearer, :forum, :legacy},
152
      "get /api/v1/repos/:owner/:repo" => {:optional_bearer, :repository, :legacy},
153
      "get /api/v1/repos/:owner/:repo/issues" => {:optional_bearer, :issue, :envelope},
154
      "get /api/v1/repos/:owner/:repo/issues/:issue_number" =>
155 155
        {:optional_bearer, :issue, :envelope},
156
      "get /api/v3/repos/:owner/:repo/issues/:issue_number/dependencies" =>
156
      "get /api/v1/repos/:owner/:repo/issues/:issue_number/dependencies" =>
157 157
        {:optional_bearer, :issue_dependency, :envelope},
158
      "get /api/v3/repos/:owner/:repo/pulls" => {:optional_bearer, :pull_request, :legacy},
159
      "get /api/v3/repos/:owner/:repo/pulls/:pull_number" =>
158
      "get /api/v1/repos/:owner/:repo/pulls" => {:optional_bearer, :pull_request, :legacy},
159
      "get /api/v1/repos/:owner/:repo/pulls/:pull_number" =>
160 160
        {:optional_bearer, :pull_request, :legacy},
161
      "get /api/v3/repos/:owner/:repo/pulls/:pull_number/merge-async/:operation_id" =>
161
      "get /api/v1/repos/:owner/:repo/pulls/:pull_number/merge-async/:operation_id" =>
162 162
        {:optional_bearer, :stack, :legacy},
163
      "get /api/v3/repos/:owner/:repo/stacks" => {:optional_bearer, :stack, :legacy},
164
      "get /api/v3/repos/:owner/:repo/stacks/:stack_number" =>
163
      "get /api/v1/repos/:owner/:repo/stacks" => {:optional_bearer, :stack, :legacy},
164
      "get /api/v1/repos/:owner/:repo/stacks/:stack_number" =>
165 165
        {:optional_bearer, :stack, :legacy},
166
      "get /api/v3/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id" =>
166
      "get /api/v1/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id" =>
167 167
        {:optional_bearer, :stack, :legacy},
168
      "get /api/v3/repos/:owner/:repo/projectsV2" => {:optional_bearer, :project, :envelope},
169
      "get /api/v3/repos/:owner/:repo/projectsV2/:project_number" =>
168
      "get /api/v1/repos/:owner/:repo/projectsV2" => {:optional_bearer, :project, :envelope},
169
      "get /api/v1/repos/:owner/:repo/projectsV2/:project_number" =>
170 170
        {:optional_bearer, :project, :envelope},
171
      "get /api/v3/repos/:owner/:repo/projectsV2/:project_number/items" =>
171
      "get /api/v1/repos/:owner/:repo/projectsV2/:project_number/items" =>
172 172
        {:optional_bearer, :project, :envelope},
173
      "get /api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/events" =>
173
      "get /api/v1/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/events" =>
174 174
        {:optional_bearer, :project, :envelope},
175
      "get /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields" =>
175
      "get /api/v1/repos/:owner/:repo/projectsV2/:project_number/fields" =>
176 176
        {:optional_bearer, :project, :envelope},
177
      "get /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes" =>
177
      "get /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes" =>
178 178
        {:optional_bearer, :project, :envelope},
179
      "get /api/v3/reputation/policy" => {:optional_bearer, :reputation, :legacy},
180
      "get /api/v3/reputation/keys" => {:optional_bearer, :reputation, :legacy},
181
      "get /api/v3/repos/:owner/:repo/issues/:issue_number/attestations" =>
179
      "get /api/v1/reputation/policy" => {:optional_bearer, :reputation, :legacy},
180
      "get /api/v1/reputation/keys" => {:optional_bearer, :reputation, :legacy},
181
      "get /api/v1/repos/:owner/:repo/issues/:issue_number/attestations" =>
182 182
        {:optional_bearer, :reputation, :legacy},
183
      "get /api/v3/repos/:owner/:repo/attestations/:id" =>
183
      "get /api/v1/repos/:owner/:repo/attestations/:id" =>
184 184
        {:optional_bearer, :reputation, :legacy},
185
      "get /api/v3/repos/:owner/:repo/attestations/:id/verification" =>
185
      "get /api/v1/repos/:owner/:repo/attestations/:id/verification" =>
186 186
        {:optional_bearer, :reputation, :legacy},
187
      "get /api/v3/repos/:owner/:repo/reputation/subjects/:subject_id" =>
187
      "get /api/v1/repos/:owner/:repo/reputation/subjects/:subject_id" =>
188 188
        {:optional_bearer, :reputation, :legacy},
189 189
      # Scoped bearer pipelines require the route-specific token authority.
190
      "get /api/v3/chat/events" => {:required_bearer, :chat, :legacy},
191
      "post /api/v3/chat/turns" => {:required_bearer, :chat, :legacy},
192
      "get /api/v3/models" => {:required_bearer, :model, :envelope},
193
      "post /api/v3/threads" => {:required_bearer, :thread, :envelope},
194
      "get /api/v3/threads" => {:required_bearer, :thread, :envelope},
195
      "get /api/v3/threads/:thread_id" => {:required_bearer, :thread, :envelope},
196
      "delete /api/v3/threads/:thread_id" => {:required_bearer, :thread, :envelope},
197
      "get /api/v3/threads/:thread_id/events" => {:required_bearer, :thread, :envelope},
198
      "post /api/v3/threads/:thread_id/events" => {:required_bearer, :thread, :envelope},
199
      "post /api/v3/threads/:thread_id/grants" => {:required_bearer, :thread, :envelope},
200
      "get /api/v3/capacity" => {:required_bearer, :capacity, :legacy},
201
      "post /api/v3/capacity/matches" => {:required_bearer, :capacity, :legacy},
202
      "get /api/v3/conversations/:conversation_id/boxes" => {:required_bearer, :box, :legacy},
203
      "post /api/v3/conversations/:conversation_id/boxes" => {:required_bearer, :box, :legacy},
204
      "post /api/v3/conversations/:conversation_id/boxes/fanout" =>
190
      "get /api/v1/chat/events" => {:required_bearer, :chat, :legacy},
191
      "post /api/v1/chat/turns" => {:required_bearer, :chat, :legacy},
192
      "get /api/v1/models" => {:required_bearer, :model, :envelope},
193
      "post /api/v1/threads" => {:required_bearer, :thread, :envelope},
194
      "get /api/v1/threads" => {:required_bearer, :thread, :envelope},
195
      "get /api/v1/threads/:thread_id" => {:required_bearer, :thread, :envelope},
196
      "delete /api/v1/threads/:thread_id" => {:required_bearer, :thread, :envelope},
197
      "get /api/v1/threads/:thread_id/events" => {:required_bearer, :thread, :envelope},
198
      "post /api/v1/threads/:thread_id/events" => {:required_bearer, :thread, :envelope},
199
      "post /api/v1/threads/:thread_id/grants" => {:required_bearer, :thread, :envelope},
200
      "get /api/v1/capacity" => {:required_bearer, :capacity, :legacy},
201
      "post /api/v1/capacity/matches" => {:required_bearer, :capacity, :legacy},
202
      "get /api/v1/conversations/:conversation_id/boxes" => {:required_bearer, :box, :legacy},
203
      "post /api/v1/conversations/:conversation_id/boxes" => {:required_bearer, :box, :legacy},
204
      "post /api/v1/conversations/:conversation_id/boxes/fanout" =>
205 205
        {:required_bearer, :box, :legacy},
206
      "get /api/v3/conversations/:conversation_id/boxes/fanout/:request_id" =>
206
      "get /api/v1/conversations/:conversation_id/boxes/fanout/:request_id" =>
207 207
        {:required_bearer, :box, :legacy},
208
      "get /api/v3/conversations/:conversation_id/boxes/:box_id" =>
208
      "get /api/v1/conversations/:conversation_id/boxes/:box_id" =>
209 209
        {:required_bearer, :box, :legacy},
210
      "post /api/v3/conversations/:conversation_id/boxes/:box_id/commands" =>
210
      "post /api/v1/conversations/:conversation_id/boxes/:box_id/commands" =>
211 211
        {:required_bearer, :box, :legacy},
212
      "post /api/v3/conversations/:conversation_id/boxes/:box_id/stop" =>
212
      "post /api/v1/conversations/:conversation_id/boxes/:box_id/stop" =>
213 213
        {:required_bearer, :box, :legacy},
214
      "post /api/v3/conversations/:conversation_id/boxes/:box_id/assignments" =>
214
      "post /api/v1/conversations/:conversation_id/boxes/:box_id/assignments" =>
215 215
        {:required_bearer, :assignment, :legacy},
216
      "get /api/v3/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id" =>
216
      "get /api/v1/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id" =>
217 217
        {:required_bearer, :assignment, :legacy},
218
      "post /api/v3/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id/cancel" =>
218
      "post /api/v1/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id/cancel" =>
219 219
        {:required_bearer, :assignment, :legacy},
220
      "post /api/v3/conversations/:conversation_id/computers/:computer_id/assignments" =>
220
      "post /api/v1/conversations/:conversation_id/computers/:computer_id/assignments" =>
221 221
        {:required_bearer, :assignment, :legacy},
222
      "get /api/v3/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id" =>
222
      "get /api/v1/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id" =>
223 223
        {:required_bearer, :assignment, :legacy},
224
      "post /api/v3/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id/cancel" =>
224
      "post /api/v1/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id/cancel" =>
225 225
        {:required_bearer, :assignment, :legacy},
226
      "post /api/v3/conversations/:conversation_id/boxes/:box_id/runs" =>
226
      "post /api/v1/conversations/:conversation_id/boxes/:box_id/runs" =>
227 227
        {:required_bearer, :box, :legacy},
228
      "get /api/v3/conversations/:conversation_id/boxes/:box_id/runs" =>
228
      "get /api/v1/conversations/:conversation_id/boxes/:box_id/runs" =>
229 229
        {:required_bearer, :box, :legacy},
230
      "get /api/v3/conversations/:conversation_id/boxes/:box_id/runs/:run_id" =>
230
      "get /api/v1/conversations/:conversation_id/boxes/:box_id/runs/:run_id" =>
231 231
        {:required_bearer, :box, :legacy},
232
      "get /api/v3/conversations/:conversation_id/boxes/:box_id/runs/:run_id/output" =>
232
      "get /api/v1/conversations/:conversation_id/boxes/:box_id/runs/:run_id/output" =>
233 233
        {:required_bearer, :box, :legacy},
234
      "post /api/v3/conversations/:conversation_id/boxes/:box_id/runs/:run_id/cancel" =>
234
      "post /api/v1/conversations/:conversation_id/boxes/:box_id/runs/:run_id/cancel" =>
235 235
        {:required_bearer, :box, :legacy},
236
      "get /api/v3/computers" => {:required_bearer, :computer, :legacy},
237
      "patch /api/v3/computers/:id" => {:required_bearer, :computer, :legacy},
238
      "post /api/v3/computers/:computer_id/probe" => {:required_bearer, :computer, :legacy},
239
      "post /api/v3/computers/:computer_id/agent-jobs" => {:required_bearer, :computer, :legacy},
240
      "get /api/v3/computer-agent-jobs/:id" => {:required_bearer, :computer, :legacy},
241
      "delete /api/v3/computer-agent-jobs/:id" => {:required_bearer, :computer, :legacy},
242
      "get /api/v3/conversations/:conversation_id/delegation-targets" =>
236
      "get /api/v1/computers" => {:required_bearer, :computer, :legacy},
237
      "patch /api/v1/computers/:id" => {:required_bearer, :computer, :legacy},
238
      "post /api/v1/computers/:computer_id/probe" => {:required_bearer, :computer, :legacy},
239
      "post /api/v1/computers/:computer_id/agent-jobs" => {:required_bearer, :computer, :legacy},
240
      "get /api/v1/computer-agent-jobs/:id" => {:required_bearer, :computer, :legacy},
241
      "delete /api/v1/computer-agent-jobs/:id" => {:required_bearer, :computer, :legacy},
242
      "get /api/v1/conversations/:conversation_id/delegation-targets" =>
243 243
        {:required_bearer, :delegation, :legacy},
244
      "post /api/v3/conversations/:conversation_id/delegations" =>
244
      "post /api/v1/conversations/:conversation_id/delegations" =>
245 245
        {:required_bearer, :delegation, :legacy},
246
      "get /api/v3/conversations/:conversation_id/delegations/:id" =>
246
      "get /api/v1/conversations/:conversation_id/delegations/:id" =>
247 247
        {:required_bearer, :delegation, :legacy},
248
      "delete /api/v3/conversations/:conversation_id/delegations/:id" =>
248
      "delete /api/v1/conversations/:conversation_id/delegations/:id" =>
249 249
        {:required_bearer, :delegation, :legacy},
250
      "delete /api/v3/repos/:owner/:repo" => {:required_bearer, :repository, :legacy},
251
      "delete /api/v3/repos/:owner/:repo/issues/:issue_number/assignees" =>
250
      "delete /api/v1/repos/:owner/:repo" => {:required_bearer, :repository, :legacy},
251
      "delete /api/v1/repos/:owner/:repo/issues/:issue_number/assignees" =>
252 252
        {:required_bearer, :issue_assignee, :envelope},
253
      "delete /api/v3/repos/:owner/:repo/issues/:issue_number/labels/:name" =>
253
      "delete /api/v1/repos/:owner/:repo/issues/:issue_number/labels/:name" =>
254 254
        {:required_bearer, :issue_label, :envelope},
255
      "delete /api/v3/repos/:owner/:repo/issues/comments/:id" =>
255
      "delete /api/v1/repos/:owner/:repo/issues/comments/:id" =>
256 256
        {:required_bearer, :comment, :envelope},
257
      "delete /api/v3/repos/:owner/:repo/labels/:name" => {:required_bearer, :label, :envelope},
258
      "delete /api/v3/repos/:owner/:repo/milestones/:milestone_number" =>
257
      "delete /api/v1/repos/:owner/:repo/labels/:name" => {:required_bearer, :label, :envelope},
258
      "delete /api/v1/repos/:owner/:repo/milestones/:milestone_number" =>
259 259
        {:required_bearer, :milestone, :envelope},
260
      "get /api/v3/repository-imports/:id" => {:required_bearer, :repository, :legacy},
261
      "get /api/v3/user" => {:required_bearer, :repository, :legacy},
262
      "get /api/v3/user/repos" => {:required_bearer, :repository, :legacy},
263
      "patch /api/v3/repos/:owner/:repo/issues/:issue_number" =>
260
      "get /api/v1/repository-imports/:id" => {:required_bearer, :repository, :legacy},
261
      "get /api/v1/user" => {:required_bearer, :repository, :legacy},
262
      "get /api/v1/user/repos" => {:required_bearer, :repository, :legacy},
263
      "patch /api/v1/repos/:owner/:repo/issues/:issue_number" =>
264 264
        {:required_bearer, :issue, :envelope},
265
      "patch /api/v3/repos/:owner/:repo" => {:required_bearer, :repository, :legacy},
266
      "patch /api/v3/repos/:owner/:repo/pulls/:pull_number" =>
265
      "patch /api/v1/repos/:owner/:repo" => {:required_bearer, :repository, :legacy},
266
      "patch /api/v1/repos/:owner/:repo/pulls/:pull_number" =>
267 267
        {:required_bearer, :pull_request, :legacy},
268
      "patch /api/v3/repos/:owner/:repo/issues/comments/:id" =>
268
      "patch /api/v1/repos/:owner/:repo/issues/comments/:id" =>
269 269
        {:required_bearer, :comment, :envelope},
270
      "patch /api/v3/repos/:owner/:repo/labels/:name" => {:required_bearer, :label, :envelope},
271
      "patch /api/v3/repos/:owner/:repo/milestones/:milestone_number" =>
270
      "patch /api/v1/repos/:owner/:repo/labels/:name" => {:required_bearer, :label, :envelope},
271
      "patch /api/v1/repos/:owner/:repo/milestones/:milestone_number" =>
272 272
        {:required_bearer, :milestone, :envelope},
273
      "patch /api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id" =>
273
      "patch /api/v1/repos/:owner/:repo/projectsV2/:project_number/items/:item_id" =>
274 274
        {:required_bearer, :project, :envelope},
275
      "post /api/v3/orgs/:org/repos" => {:required_bearer, :repository, :legacy},
276
      "post /api/v3/orgs/:org/repos/imports" => {:required_bearer, :repository, :legacy},
277
      "post /api/v3/repos/:owner/:repo/pulls" => {:required_bearer, :pull_request, :legacy},
278
      "put /api/v3/repos/:owner/:repo/pulls/:pull_number/merge-async" =>
275
      "post /api/v1/orgs/:org/repos" => {:required_bearer, :repository, :legacy},
276
      "post /api/v1/orgs/:org/repos/imports" => {:required_bearer, :repository, :legacy},
277
      "post /api/v1/repos/:owner/:repo/pulls" => {:required_bearer, :pull_request, :legacy},
278
      "put /api/v1/repos/:owner/:repo/pulls/:pull_number/merge-async" =>
279 279
        {:required_bearer, :stack, :legacy},
280
      "post /api/v3/repos/:owner/:repo/stacks" => {:required_bearer, :stack, :legacy},
281
      "post /api/v3/repos/:owner/:repo/stacks/:stack_number/append" =>
280
      "post /api/v1/repos/:owner/:repo/stacks" => {:required_bearer, :stack, :legacy},
281
      "post /api/v1/repos/:owner/:repo/stacks/:stack_number/append" =>
282 282
        {:required_bearer, :stack, :legacy},
283
      "post /api/v3/repos/:owner/:repo/stacks/:stack_number/unstack" =>
283
      "post /api/v1/repos/:owner/:repo/stacks/:stack_number/unstack" =>
284 284
        {:required_bearer, :stack, :legacy},
285
      "post /api/v3/repos/:owner/:repo/stacks/:stack_number/dissolve" =>
285
      "post /api/v1/repos/:owner/:repo/stacks/:stack_number/dissolve" =>
286 286
        {:required_bearer, :stack, :legacy},
287
      "post /api/v3/repos/:owner/:repo/stacks/:stack_number/rebase" =>
287
      "post /api/v1/repos/:owner/:repo/stacks/:stack_number/rebase" =>
288 288
        {:required_bearer, :stack, :legacy},
289
      "post /api/v3/repos/:owner/:repo/stacks/:stack_number/merge" =>
289
      "post /api/v1/repos/:owner/:repo/stacks/:stack_number/merge" =>
290 290
        {:required_bearer, :stack, :legacy},
291
      "post /api/v3/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/continue" =>
291
      "post /api/v1/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/continue" =>
292 292
        {:required_bearer, :stack, :legacy},
293
      "post /api/v3/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/abort" =>
293
      "post /api/v1/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/abort" =>
294 294
        {:required_bearer, :stack, :legacy},
295 295
      # pipe_through :fleet_promotion_api — operator-only fleet promotion. The
296 296
      # bearer must carry `deployments:promote` and its account must be a
297 297
      # current operator, so no tenant credential reaches these three routes.
298
      "post /api/v3/admin/forge/targets" => {:required_bearer, :fleet_target, :envelope},
299
      "get /api/v3/admin/forge/targets" => {:required_bearer, :fleet_target, :envelope},
300
      "get /api/v3/admin/forge/targets/:id" => {:required_bearer, :fleet_target, :envelope},
298
      "post /api/v1/admin/forge/targets" => {:required_bearer, :fleet_target, :envelope},
299
      "get /api/v1/admin/forge/targets" => {:required_bearer, :fleet_target, :envelope},
300
      "get /api/v1/admin/forge/targets/:id" => {:required_bearer, :fleet_target, :envelope},
301 301
      # pipe_through :deployments_api — tenant deployment authority only. No
302 302
      # route here is anonymous, and none of them reaches the operator fleet
303 303
      # promotion surface.
304
      "get /api/v3/repos/:owner/:repo/deployment-environments" =>
304
      "get /api/v1/repos/:owner/:repo/deployment-environments" =>
305 305
        {:required_bearer, :deployment, :legacy},
306
      "put /api/v3/repos/:owner/:repo/deployment-environments/:name" =>
306
      "put /api/v1/repos/:owner/:repo/deployment-environments/:name" =>
307 307
        {:required_bearer, :deployment, :legacy},
308
      "get /api/v3/repos/:owner/:repo/deployment-environments/:name/protection" =>
308
      "get /api/v1/repos/:owner/:repo/deployment-environments/:name/protection" =>
309 309
        {:required_bearer, :deployment, :legacy},
310
      "post /api/v3/repos/:owner/:repo/deployments" => {:required_bearer, :deployment, :legacy},
311
      "get /api/v3/repos/:owner/:repo/deployments" => {:required_bearer, :deployment, :legacy},
312
      "get /api/v3/repos/:owner/:repo/deployments/:id" =>
310
      "post /api/v1/repos/:owner/:repo/deployments" => {:required_bearer, :deployment, :legacy},
311
      "get /api/v1/repos/:owner/:repo/deployments" => {:required_bearer, :deployment, :legacy},
312
      "get /api/v1/repos/:owner/:repo/deployments/:id" =>
313 313
        {:required_bearer, :deployment, :legacy},
314
      "post /api/v3/repos/:owner/:repo/deployments/:id/cancel" =>
314
      "post /api/v1/repos/:owner/:repo/deployments/:id/cancel" =>
315 315
        {:required_bearer, :deployment, :legacy},
316
      "post /api/v3/repos/:owner/:repo/deployments/:id/approvals" =>
316
      "post /api/v1/repos/:owner/:repo/deployments/:id/approvals" =>
317 317
        {:required_bearer, :deployment, :legacy},
318
      "get /api/v3/repos/:owner/:repo/deployments/:id/approvals" =>
318
      "get /api/v1/repos/:owner/:repo/deployments/:id/approvals" =>
319 319
        {:required_bearer, :deployment, :legacy},
320
      "get /api/v3/repos/:owner/:repo/deployments/:id/events" =>
320
      "get /api/v1/repos/:owner/:repo/deployments/:id/events" =>
321 321
        {:required_bearer, :deployment, :legacy},
322
      "post /api/v3/repos/:owner/:repo/deployment-checks" =>
322
      "post /api/v1/repos/:owner/:repo/deployment-checks" =>
323 323
        {:required_bearer, :deployment, :legacy},
324
      "post /api/v3/repos/:owner/:repo/deployment-workflow-grants" =>
324
      "post /api/v1/repos/:owner/:repo/deployment-workflow-grants" =>
325 325
        {:required_bearer, :deployment, :legacy},
326
      "delete /api/v3/repos/:owner/:repo/deployment-workflow-grants/:id" =>
326
      "delete /api/v1/repos/:owner/:repo/deployment-workflow-grants/:id" =>
327 327
        {:required_bearer, :deployment, :legacy},
328 328
      # Dual-principal participation writes accept either a human forge token or
329 329
      # an agent participation credential.
330
      "post /api/v3/forum/topics" => {:required_bearer, :forum, :legacy},
331
      "post /api/v3/forum/topics/:topic_id/posts" => {:required_bearer, :forum, :legacy},
332
      "post /api/v3/repos/:owner/:repo/issues" => {:required_bearer, :issue, :envelope},
333
      "post /api/v3/repos/:owner/:repo/issues/capture" => {:required_bearer, :issue, :envelope},
334
      "post /api/v3/repos/:owner/:repo/issues/:issue_number/comments" =>
330
      "post /api/v1/forum/topics" => {:required_bearer, :forum, :legacy},
331
      "post /api/v1/forum/topics/:topic_id/posts" => {:required_bearer, :forum, :legacy},
332
      "post /api/v1/repos/:owner/:repo/issues" => {:required_bearer, :issue, :envelope},
333
      "post /api/v1/repos/:owner/:repo/issues/capture" => {:required_bearer, :issue, :envelope},
334
      "post /api/v1/repos/:owner/:repo/issues/:issue_number/comments" =>
335 335
        {:required_bearer, :comment, :envelope},
336
      "post /api/v3/repos/:owner/:repo/issues/:issue_number/completion_claim" =>
336
      "post /api/v1/repos/:owner/:repo/issues/:issue_number/completion_claim" =>
337 337
        {:required_bearer, :issue, :envelope},
338
      "get /api/v3/agent" => {:required_bearer, :agent, :legacy},
339
      "post /api/v3/agents/:handle/box-control" => {:required_bearer, :agent, :legacy},
340
      "post /api/v3/gym/runs" => {:required_bearer, :gym, :envelope},
341
      "get /api/v3/gym/runs" => {:required_bearer, :gym, :envelope},
342
      "delete /api/v3/agents/:handle/box-control" => {:required_bearer, :agent, :legacy},
343
      "post /api/v3/agents/:handle/computer-control" => {:required_bearer, :agent, :legacy},
344
      "delete /api/v3/agents/:handle/computer-control" => {:required_bearer, :agent, :legacy},
345
      "post /api/v3/agent/credentials" => {:required_bearer, :agent, :legacy},
346
      "post /api/v3/agent/links" => {:required_bearer, :agent, :legacy},
347
      "get /api/v3/agents/links" => {:required_bearer, :agent, :legacy},
348
      "post /api/v3/agents/links/:id/accept" => {:required_bearer, :agent, :legacy},
349
      "post /api/v3/agents/links/:id/reject" => {:required_bearer, :agent, :legacy},
350
      "delete /api/v3/agents/links/:id" => {:required_bearer, :agent, :legacy},
351
      "post /api/v3/forum/claims" => {:required_bearer, :forum, :legacy},
352
      "get /api/v3/forum/claims" => {:required_bearer, :forum, :legacy},
353
      "post /api/v3/reputation/subject-claims" => {:required_bearer, :reputation, :legacy},
354
      "get /api/v3/reputation/subject-claims" => {:required_bearer, :reputation, :legacy},
355
      "get /api/v3/reputation/subject-claims/pending" => {:required_bearer, :reputation, :legacy},
356
      "patch /api/v3/reputation/subject-claims/:id" => {:required_bearer, :reputation, :legacy},
338
      "get /api/v1/agent" => {:required_bearer, :agent, :legacy},
339
      "post /api/v1/agents/:handle/box-control" => {:required_bearer, :agent, :legacy},
340
      "post /api/v1/gym/runs" => {:required_bearer, :gym, :envelope},
341
      "get /api/v1/gym/runs" => {:required_bearer, :gym, :envelope},
342
      "delete /api/v1/agents/:handle/box-control" => {:required_bearer, :agent, :legacy},
343
      "post /api/v1/agents/:handle/computer-control" => {:required_bearer, :agent, :legacy},
344
      "delete /api/v1/agents/:handle/computer-control" => {:required_bearer, :agent, :legacy},
345
      "post /api/v1/agent/credentials" => {:required_bearer, :agent, :legacy},
346
      "post /api/v1/agent/links" => {:required_bearer, :agent, :legacy},
347
      "get /api/v1/agents/links" => {:required_bearer, :agent, :legacy},
348
      "post /api/v1/agents/links/:id/accept" => {:required_bearer, :agent, :legacy},
349
      "post /api/v1/agents/links/:id/reject" => {:required_bearer, :agent, :legacy},
350
      "delete /api/v1/agents/links/:id" => {:required_bearer, :agent, :legacy},
351
      "post /api/v1/forum/claims" => {:required_bearer, :forum, :legacy},
352
      "get /api/v1/forum/claims" => {:required_bearer, :forum, :legacy},
353
      "post /api/v1/reputation/subject-claims" => {:required_bearer, :reputation, :legacy},
354
      "get /api/v1/reputation/subject-claims" => {:required_bearer, :reputation, :legacy},
355
      "get /api/v1/reputation/subject-claims/pending" => {:required_bearer, :reputation, :legacy},
356
      "patch /api/v1/reputation/subject-claims/:id" => {:required_bearer, :reputation, :legacy},
357 357
      # Moderation and claim review: a bearer the controller then checks for
358 358
      # operator authority.
359
      "patch /api/v3/forum/topics/:id" => {:required_bearer, :forum, :legacy},
360
      "patch /api/v3/forum/posts/:id" => {:required_bearer, :forum, :legacy},
361
      "get /api/v3/forum/claims/pending" => {:required_bearer, :forum, :legacy},
362
      "patch /api/v3/forum/claims/:id" => {:required_bearer, :forum, :legacy},
359
      "patch /api/v1/forum/topics/:id" => {:required_bearer, :forum, :legacy},
360
      "patch /api/v1/forum/posts/:id" => {:required_bearer, :forum, :legacy},
361
      "get /api/v1/forum/claims/pending" => {:required_bearer, :forum, :legacy},
362
      "patch /api/v1/forum/claims/:id" => {:required_bearer, :forum, :legacy},
363 363
      # Tips: a destination, a payment, and a settlement history each belong to
364 364
      # one account, so anonymous callers never reach them.
365
      "post /api/v3/forum/tips/destination" => {:required_bearer, :forum, :legacy},
366
      "patch /api/v3/forum/tips/destination" => {:required_bearer, :forum, :legacy},
367
      "get /api/v3/forum/tips/destination" => {:required_bearer, :forum, :legacy},
368
      "get /api/v3/forum/tips/received" => {:required_bearer, :forum, :legacy},
369
      "post /api/v3/forum/posts/:post_id/tips" => {:required_bearer, :forum, :legacy},
370
      "post /api/v3/repos/:owner/:repo/issues/:issue_number/assignees" =>
365
      "post /api/v1/forum/tips/destination" => {:required_bearer, :forum, :legacy},
366
      "patch /api/v1/forum/tips/destination" => {:required_bearer, :forum, :legacy},
367
      "get /api/v1/forum/tips/destination" => {:required_bearer, :forum, :legacy},
368
      "get /api/v1/forum/tips/received" => {:required_bearer, :forum, :legacy},
369
      "post /api/v1/forum/posts/:post_id/tips" => {:required_bearer, :forum, :legacy},
370
      "post /api/v1/repos/:owner/:repo/issues/:issue_number/assignees" =>
371 371
        {:required_bearer, :issue_assignee, :envelope},
372
      "post /api/v3/repos/:owner/:repo/issues/:issue_number/dependencies" =>
372
      "post /api/v1/repos/:owner/:repo/issues/:issue_number/dependencies" =>
373 373
        {:required_bearer, :issue_dependency, :envelope},
374
      "delete /api/v3/repos/:owner/:repo/issues/:issue_number/dependencies/:blocked_by_number" =>
374
      "delete /api/v1/repos/:owner/:repo/issues/:issue_number/dependencies/:blocked_by_number" =>
375 375
        {:required_bearer, :issue_dependency, :envelope},
376
      "post /api/v3/repos/:owner/:repo/issues/:issue_number/labels" =>
376
      "post /api/v1/repos/:owner/:repo/issues/:issue_number/labels" =>
377 377
        {:required_bearer, :issue_label, :envelope},
378
      "post /api/v3/repos/:owner/:repo/labels" => {:required_bearer, :label, :envelope},
379
      "post /api/v3/repos/:owner/:repo/milestones" => {:required_bearer, :milestone, :envelope},
380
      "post /api/v3/repos/:owner/:repo/projectsV2" => {:required_bearer, :project, :envelope},
381
      "post /api/v3/repos/:owner/:repo/projectsV2/:project_number/items" =>
378
      "post /api/v1/repos/:owner/:repo/labels" => {:required_bearer, :label, :envelope},
379
      "post /api/v1/repos/:owner/:repo/milestones" => {:required_bearer, :milestone, :envelope},
380
      "post /api/v1/repos/:owner/:repo/projectsV2" => {:required_bearer, :project, :envelope},
381
      "post /api/v1/repos/:owner/:repo/projectsV2/:project_number/items" =>
382 382
        {:required_bearer, :project, :envelope},
383
      "post /api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/move" =>
383
      "post /api/v1/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/move" =>
384 384
        {:required_bearer, :project, :envelope},
385
      "delete /api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id" =>
385
      "delete /api/v1/repos/:owner/:repo/projectsV2/:project_number/items/:item_id" =>
386 386
        {:required_bearer, :project, :envelope},
387
      "post /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields" =>
387
      "post /api/v1/repos/:owner/:repo/projectsV2/:project_number/fields" =>
388 388
        {:required_bearer, :project, :envelope},
389
      "patch /api/v3/repos/:owner/:repo/projectsV2/:project_number" =>
389
      "patch /api/v1/repos/:owner/:repo/projectsV2/:project_number" =>
390 390
        {:required_bearer, :project, :envelope},
391
      "post /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes" =>
391
      "post /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes" =>
392 392
        {:required_bearer, :project, :envelope},
393
      "patch /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id" =>
393
      "patch /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id" =>
394 394
        {:required_bearer, :project, :envelope},
395
      "delete /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id" =>
395
      "delete /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id" =>
396 396
        {:required_bearer, :project, :envelope},
397
      "patch /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields/:field_id" =>
397
      "patch /api/v1/repos/:owner/:repo/projectsV2/:project_number/fields/:field_id" =>
398 398
        {:required_bearer, :project, :envelope},
399
      "delete /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields/:field_id" =>
399
      "delete /api/v1/repos/:owner/:repo/projectsV2/:project_number/fields/:field_id" =>
400 400
        {:required_bearer, :project, :envelope},
401
      "delete /api/v3/repos/:owner/:repo/projectsV2/:project_number" =>
401
      "delete /api/v1/repos/:owner/:repo/projectsV2/:project_number" =>
402 402
        {:required_bearer, :project, :envelope},
403
      "post /api/v3/user/repos" => {:required_bearer, :repository, :legacy},
404
      "post /api/v3/user/repos/imports" => {:required_bearer, :repository, :legacy},
405
      "put /api/v3/repos/:owner/:repo/issues/:issue_number" =>
403
      "post /api/v1/user/repos" => {:required_bearer, :repository, :legacy},
404
      "post /api/v1/user/repos/imports" => {:required_bearer, :repository, :legacy},
405
      "put /api/v1/repos/:owner/:repo/issues/:issue_number" =>
406 406
        {:required_bearer, :issue, :envelope},
407
      "put /api/v3/repos/:owner/:repo/issues/comments/:id" =>
407
      "put /api/v1/repos/:owner/:repo/issues/comments/:id" =>
408 408
        {:required_bearer, :comment, :envelope},
409
      "put /api/v3/repos/:owner/:repo/labels/:name" => {:required_bearer, :label, :envelope},
410
      "put /api/v3/repos/:owner/:repo/milestones/:milestone_number" =>
409
      "put /api/v1/repos/:owner/:repo/labels/:name" => {:required_bearer, :label, :envelope},
410
      "put /api/v1/repos/:owner/:repo/milestones/:milestone_number" =>
411 411
        {:required_bearer, :milestone, :envelope},
412
      "post /api/v3/traces" => {:required_bearer, :trace, :envelope}
412
      # Landed at `/api/v3` while the rename was in flight. It moves with
413
      # everything else; the alias answers the prefix it was written against.
414
      "post /api/v1/traces" => {:required_bearer, :trace, :envelope}
413 415
    }
414 416
  end
415 417
end
lib/openagents_web/contribution_contract.ex modified +28 -28

@@ -10,7 +10,7 @@ defmodule OpenAgentsWeb.ContributionContract do

10 10
  file an issue, how to get a change in, and how to show that the change
11 11
  landed.
12 12
13
  It does not restate the API. `GET /api/v3` already publishes the route
13
  It does not restate the API. `GET /api/v1` already publishes the route
14 14
  inventory, the resource families, the extension fields, and the error
15 15
  envelope, all derived from the router. Publishing a second copy here would
16 16
  give an agent two documents that can disagree. This contract points at that

@@ -26,7 +26,7 @@ defmodule OpenAgentsWeb.ContributionContract do

26 26
27 27
  - Each published request carries the class, principal, and scope that
28 28
    `OpenAgentsWeb.RouteAuthority` computes for that exact router route, and
29
    `/api/v3` requests additionally carry the resource family from
29
    `/api/v1` requests additionally carry the resource family from
30 30
    `OpenAgentsWeb.ApiRouteAuthority`.
31 31
  - The credential scopes come from `OpenAgents.ApiTokens.allowed_scopes/0`
32 32
    and the agent participation scope the router enforces.

@@ -233,18 +233,18 @@ defmodule OpenAgentsWeb.ContributionContract do

233 233
          "api-description",
234 234
          "Read the route inventory, families, extensions, and error envelope.",
235 235
          method: "GET",
236
          path: "/api/v3"
236
          path: "/api/v1"
237 237
        ),
238 238
        step("ready-queue", "List open issues with no open prerequisite.",
239 239
          method: "GET",
240
          path: "/api/v3/repos/:owner/:repo/issues",
240
          path: "/api/v1/repos/:owner/:repo/issues",
241 241
          query: "state=open&blocked=false"
242 242
        ),
243 243
        step(
244 244
          "labels",
245 245
          "Read the tracker's labels. `agent-ready` marks issues shaped for an agent.",
246 246
          method: "GET",
247
          path: "/api/v3/repos/:owner/:repo/labels",
247
          path: "/api/v1/repos/:owner/:repo/labels",
248 248
          note:
249 249
            "Labels are tracker data, not part of this contract. Narrow the ready queue " <>
250 250
              "with `labels=agent-ready` only after this endpoint confirms the label exists."

@@ -253,22 +253,22 @@ defmodule OpenAgentsWeb.ContributionContract do

253 253
          "issue",
254 254
          "Read one issue, including its acceptance criteria and its `openagents` fields.",
255 255
          method: "GET",
256
          path: "/api/v3/repos/:owner/:repo/issues/:issue_number"
256
          path: "/api/v1/repos/:owner/:repo/issues/:issue_number"
257 257
        ),
258 258
        step("dependencies", "Read what an issue waits on and what waits on it.",
259 259
          method: "GET",
260
          path: "/api/v3/repos/:owner/:repo/issues/:issue_number/dependencies"
260
          path: "/api/v1/repos/:owner/:repo/issues/:issue_number/dependencies"
261 261
        ),
262 262
        step(
263 263
          "progress",
264 264
          "Filter by how far along an issue is, as the reader's own boards derive it.",
265 265
          method: "GET",
266
          path: "/api/v3/repos/:owner/:repo/issues",
266
          path: "/api/v1/repos/:owner/:repo/issues",
267 267
          query: "state=open&progress=not_started"
268 268
        ),
269 269
        step("project-views", "List the project boards a repository publishes.",
270 270
          method: "GET",
271
          path: "/api/v3/repos/:owner/:repo/projectsV2"
271
          path: "/api/v1/repos/:owner/:repo/projectsV2"
272 272
        )
273 273
      ]
274 274
    }

@@ -303,16 +303,16 @@ defmodule OpenAgentsWeb.ContributionContract do

303 303
        ),
304 304
        step("device-flow", "Authorize a headless client without a browser on the same machine.",
305 305
          method: "POST",
306
          path: "/api/v3/device/authorizations"
306
          path: "/api/v1/device/authorizations"
307 307
        ),
308 308
        step("register-agent", "Register an agent and receive its participation credential.",
309 309
          method: "POST",
310
          path: "/api/v3/agents/register"
310
          path: "/api/v1/agents/register"
311 311
        ),
312 312
        step("call", "Send the credential as a bearer.",
313 313
          command:
314 314
            "curl --header \"Authorization: Bearer $OPENAGENTS_API_TOKEN\" " <>
315
              base <> "/api/v3/repos/{owner}/{repo}/issues"
315
              base <> "/api/v1/repos/{owner}/{repo}/issues"
316 316
        )
317 317
      ]
318 318
    }

@@ -339,15 +339,15 @@ defmodule OpenAgentsWeb.ContributionContract do

339 339
        ),
340 340
        step("file", "File an issue.",
341 341
          method: "POST",
342
          path: "/api/v3/repos/:owner/:repo/issues"
342
          path: "/api/v1/repos/:owner/:repo/issues"
343 343
        ),
344 344
        step("comment", "Add evidence to an issue.",
345 345
          method: "POST",
346
          path: "/api/v3/repos/:owner/:repo/issues/:issue_number/comments"
346
          path: "/api/v1/repos/:owner/:repo/issues/:issue_number/comments"
347 347
        ),
348 348
        step("declare-dependency", "Record that an issue waits on another.",
349 349
          method: "POST",
350
          path: "/api/v3/repos/:owner/:repo/issues/:issue_number/dependencies"
350
          path: "/api/v1/repos/:owner/:repo/issues/:issue_number/dependencies"
351 351
        )
352 352
      ]
353 353
    }

@@ -385,11 +385,11 @@ defmodule OpenAgentsWeb.ContributionContract do

385 385
        step("push", "Push to the forge.", command: "git push openagents HEAD:main"),
386 386
        step("pull-request", "Open a pull request on the forge.",
387 387
          method: "POST",
388
          path: "/api/v3/repos/:owner/:repo/pulls"
388
          path: "/api/v1/repos/:owner/:repo/pulls"
389 389
        ),
390 390
        step("read-pull-requests", "Read pull requests.",
391 391
          method: "GET",
392
          path: "/api/v3/repos/:owner/:repo/pulls"
392
          path: "/api/v1/repos/:owner/:repo/pulls"
393 393
        ),
394 394
        step("clone-anonymous", "Read without a credential.",
395 395
          command: "git clone " <> base <> "/{owner}/{repo}.git"

@@ -419,7 +419,7 @@ defmodule OpenAgentsWeb.ContributionContract do

419 419
          "attempts",
420 420
          "Read the attempts made against one issue, with the commit each ended at.",
421 421
          method: "GET",
422
          path: "/api/v3/repos/:owner/:repo/issues/:issue_number"
422
          path: "/api/v1/repos/:owner/:repo/issues/:issue_number"
423 423
        ),
424 424
        step("changelog", "Read entries with their push, build, and deploy receipt identifiers.",
425 425
          method: "GET",

@@ -431,11 +431,11 @@ defmodule OpenAgentsWeb.ContributionContract do

431 431
        ),
432 432
        step("deployment", "Read one deployment and its provider receipt.",
433 433
          method: "GET",
434
          path: "/api/v3/repos/:owner/:repo/deployments/:id"
434
          path: "/api/v1/repos/:owner/:repo/deployments/:id"
435 435
        ),
436 436
        step("close", "Close the issue, with the evidence in a comment.",
437 437
          method: "PATCH",
438
          path: "/api/v3/repos/:owner/:repo/issues/:issue_number"
438
          path: "/api/v1/repos/:owner/:repo/issues/:issue_number"
439 439
        )
440 440
      ]
441 441
    }

@@ -468,7 +468,7 @@ defmodule OpenAgentsWeb.ContributionContract do

468 468
        ),
469 469
        step("promises", "Filter a project board's items by promise state.",
470 470
          method: "GET",
471
          path: "/api/v3/repos/:owner/:repo/projectsV2/:project_number/items",
471
          path: "/api/v1/repos/:owner/:repo/projectsV2/:project_number/items",
472 472
          query: "promise_state=LIVE",
473 473
          document: "docs/product-promises-registry.md"
474 474
        )

@@ -489,7 +489,7 @@ defmodule OpenAgentsWeb.ContributionContract do

489 489
        "state" =>
490 490
          "Implemented in the domain and not served. No route prices, claims, verifies, " <>
491 491
            "or settles a bounty, and no payment surface is reachable over HTTP.",
492
        "absent_route" => "POST /api/v3/repos/{owner}/{repo}/issues/{issue_number}/bounty",
492
        "absent_route" => "POST /api/v1/repos/{owner}/{repo}/issues/{issue_number}/bounty",
493 493
        "policy_document" => "docs/bounty-settlement.md"
494 494
      },
495 495
      %{

@@ -509,15 +509,15 @@ defmodule OpenAgentsWeb.ContributionContract do

509 509
            "at, and the push, build, and deploy receipts for a commit are read from the " <>
510 510
            "changelog or the commit page. The join from an issue to those receipt " <>
511 511
            "identifiers is still made by the reader.",
512
        "absent_route" => "GET /api/v3/repos/{owner}/{repo}/issues/{issue_number}/receipts"
512
        "absent_route" => "GET /api/v1/repos/{owner}/{repo}/issues/{issue_number}/receipts"
513 513
      },
514 514
      %{
515 515
        "id" => "openapi_document",
516 516
        "summary" => "An OpenAPI description of the API.",
517 517
        "state" =>
518
          "Not published. `GET /api/v3` carries the route inventory, the resource " <>
518
          "Not published. `GET /api/v1` carries the route inventory, the resource " <>
519 519
            "families, and the error envelope instead.",
520
        "absent_route" => "GET /api/v3/openapi.json"
520
        "absent_route" => "GET /api/v1/openapi.json"
521 521
      },
522 522
      %{
523 523
        "id" => "fork",

@@ -525,7 +525,7 @@ defmodule OpenAgentsWeb.ContributionContract do

525 525
        "state" =>
526 526
          "Not implemented. The contribution path from outside a repository's membership " <>
527 527
            "is a detailed issue.",
528
        "absent_route" => "POST /api/v3/repos/{owner}/{repo}/forks"
528
        "absent_route" => "POST /api/v1/repos/{owner}/{repo}/forks"
529 529
      },
530 530
      %{
531 531
        "id" => "automatic_github_mirroring",

@@ -594,13 +594,13 @@ defmodule OpenAgentsWeb.ContributionContract do

594 594
  end
595 595
596 596
  # Each surface has exactly one authority, and this publishes whichever one
597
  # owns the route. `/api/v3` belongs to `OpenAgentsWeb.ApiRouteAuthority`,
597
  # owns the route. `/api/v1` belongs to `OpenAgentsWeb.ApiRouteAuthority`,
598 598
  # whose classifications are proven against what the enforcing pipeline does
599 599
  # to an anonymous request. Everything else belongs to the site-wide
600 600
  # `OpenAgentsWeb.RouteAuthority`. Publishing both for one route would let the
601 601
  # document contradict itself wherever the two are worded differently.
602 602
  defp access(method, path) do
603
    if String.starts_with?(path, "/api/v3") do
603
    if String.starts_with?(path, "/api/v1") do
604 604
      api_access(method, path)
605 605
    else
606 606
      site_access(method, path)
lib/openagents_web/controllers/api_extension_controller.ex modified +27 -27

@@ -231,10 +231,10 @@ defmodule OpenAgentsWeb.ApiExtensionController do

231 231
        "The inference backends an account chat turn may choose between. " <>
232 232
          "Every backend answers with the same events and the same terminal " <>
233 233
          "shape, so the choice changes which model replies and nothing else.",
234
      "endpoints" => ["POST /api/v3/chat/turns", "GET /api/v3/chat/events"],
234
      "endpoints" => ["POST /api/v1/chat/turns", "GET /api/v1/chat/events"],
235 235
      "parameters" => %{
236 236
        "model" => %{
237
          "endpoint" => "POST /api/v3/chat/turns",
237
          "endpoint" => "POST /api/v1/chat/turns",
238 238
          "type" => "string",
239 239
          "enum" => OpenAgents.Chat.Backends.ids(),
240 240
          "default" => OpenAgents.Chat.Backends.default_id(),

@@ -251,8 +251,8 @@ defmodule OpenAgentsWeb.ApiExtensionController do

251 251
      "description" => "Owner-safe quantity-based capacity and matching projections.",
252 252
      "endpoints" => [
253 253
        "GET /api/capacity",
254
        "GET /api/v3/capacity",
255
        "POST /api/v3/capacity/matches"
254
        "GET /api/v1/capacity",
255
        "POST /api/v1/capacity/matches"
256 256
      ],
257 257
      "schemas" => [
258 258
        "openagents.capacity.v1",

@@ -317,12 +317,12 @@ defmodule OpenAgentsWeb.ApiExtensionController do

317 317
      },
318 318
      "filters" => %{
319 319
        "blocked" => %{
320
          "endpoint" => "GET /api/v3/repos/{owner}/{repo}/issues",
320
          "endpoint" => "GET /api/v1/repos/{owner}/{repo}/issues",
321 321
          "type" => "boolean",
322 322
          "description" => "Lists issues that do or do not have an open prerequisite."
323 323
        },
324 324
        "progress" => %{
325
          "endpoint" => "GET /api/v3/repos/{owner}/{repo}/issues",
325
          "endpoint" => "GET /api/v1/repos/{owner}/{repo}/issues",
326 326
          "type" => "string",
327 327
          "enum" => OpenAgents.Issues.progress_values(),
328 328
          "description" =>

@@ -330,7 +330,7 @@ defmodule OpenAgentsWeb.ApiExtensionController do

330 330
              "state, so done needs state=all or state=closed."
331 331
        },
332 332
        "type" => %{
333
          "endpoint" => "GET /api/v3/repos/{owner}/{repo}/issues",
333
          "endpoint" => "GET /api/v1/repos/{owner}/{repo}/issues",
334 334
          "type" => "string",
335 335
          "enum" => OpenAgents.Issues.type_values(),
336 336
          "default" => "all",

@@ -342,9 +342,9 @@ defmodule OpenAgentsWeb.ApiExtensionController do

342 342
        }
343 343
      },
344 344
      "endpoints" => [
345
        "GET /api/v3/repos/{owner}/{repo}/issues/{issue_number}/dependencies",
346
        "POST /api/v3/repos/{owner}/{repo}/issues/{issue_number}/dependencies",
347
        "DELETE /api/v3/repos/{owner}/{repo}/issues/{issue_number}/dependencies/{blocked_by_number}"
345
        "GET /api/v1/repos/{owner}/{repo}/issues/{issue_number}/dependencies",
346
        "POST /api/v1/repos/{owner}/{repo}/issues/{issue_number}/dependencies",
347
        "DELETE /api/v1/repos/{owner}/{repo}/issues/{issue_number}/dependencies/{blocked_by_number}"
348 348
      ]
349 349
    },
350 350
    "project_item.openagents" => %{

@@ -363,24 +363,24 @@ defmodule OpenAgentsWeb.ApiExtensionController do

363 363
      },
364 364
      "filters" => %{
365 365
        "promise_state" => %{
366
          "endpoint" => "GET /api/v3/repos/{owner}/{repo}/projectsV2/{project_number}/items",
366
          "endpoint" => "GET /api/v1/repos/{owner}/{repo}/projectsV2/{project_number}/items",
367 367
          "type" => "string",
368 368
          "enum" => ["LIVE", "GATED", "WITHDRAWN"]
369 369
        },
370 370
        "bounty_candidate" => %{
371
          "endpoint" => "GET /api/v3/repos/{owner}/{repo}/projectsV2/{project_number}/items",
371
          "endpoint" => "GET /api/v1/repos/{owner}/{repo}/projectsV2/{project_number}/items",
372 372
          "type" => "boolean"
373 373
        }
374 374
      },
375 375
      "endpoints" => [
376
        "GET /api/v3/repos/{owner}/{repo}/projectsV2/{project_number}/items/{item_id}/events"
376
        "GET /api/v1/repos/{owner}/{repo}/projectsV2/{project_number}/items/{item_id}/events"
377 377
      ]
378 378
    }
379 379
  }
380 380
381 381
  def show(conn, _params) do
382 382
    json(conn, %{
383
      "api_version" => "v3",
383
      "api_version" => "v1",
384 384
      "version" => @document_version,
385 385
      "extensions" => Map.put(@extensions, "thread.openagents", thread_extension()),
386 386
      "errors" => errors_contract(),

@@ -423,21 +423,21 @@ defmodule OpenAgentsWeb.ApiExtensionController do

423 423
          "conversation, and a grant names one or the other, never both " <>
424 424
          "(THREAD-001).",
425 425
      "endpoints" => [
426
        "GET /api/v3/models",
427
        "POST /api/v3/threads",
428
        "GET /api/v3/threads/{thread_id}",
429
        "DELETE /api/v3/threads/{thread_id}"
426
        "GET /api/v1/models",
427
        "POST /api/v1/threads",
428
        "GET /api/v1/threads/{thread_id}",
429
        "DELETE /api/v1/threads/{thread_id}"
430 430
      ],
431 431
      "parameters" => %{
432 432
        "objective" => %{
433
          "endpoint" => "POST /api/v3/threads",
433
          "endpoint" => "POST /api/v1/threads",
434 434
          "type" => "string",
435 435
          "description" =>
436 436
            "What this body of work is for. Required, non-blank, and capped " <>
437 437
              "at 32 KB."
438 438
        },
439 439
        "repository" => %{
440
          "endpoint" => "POST /api/v3/threads",
440
          "endpoint" => "POST /api/v1/threads",
441 441
          "type" => "string",
442 442
          "description" =>
443 443
            "The repository the work concerns, as the opener names it " <>

@@ -445,12 +445,12 @@ defmodule OpenAgentsWeb.ApiExtensionController do

445 445
              "capped at 200 bytes; not validated against the forge's " <>
446 446
              "repositories, because a thread may concern a repository the " <>
447 447
              "forge does not host. Returned in every thread view, and " <>
448
              "GET /api/v3/threads?repository= filters the listing by exact " <>
448
              "GET /api/v1/threads?repository= filters the listing by exact " <>
449 449
              "match, so a resume picker filters structurally instead of " <>
450 450
              "parsing the objective back."
451 451
        },
452 452
        "visibility" => %{
453
          "endpoint" => "POST /api/v3/threads",
453
          "endpoint" => "POST /api/v1/threads",
454 454
          "type" => "string",
455 455
          "enum" => OpenAgents.Threads.Thread.visibilities(),
456 456
          "default" => OpenAgents.Threads.Thread.default_visibility(),

@@ -459,7 +459,7 @@ defmodule OpenAgentsWeb.ApiExtensionController do

459 459
              "transcript, from the same dark/pulse/ledger/glass vocabulary " <>
460 460
              "the forge uses for disclosure. Optional and owner-only by " <>
461 461
              "default: `dark` keeps the thread to the account that opened " <>
462
              "it, and `ledger` opens GET /api/v3/threads/{thread_id} and its " <>
462
              "it, and `ledger` opens GET /api/v1/threads/{thread_id} and its " <>
463 463
              "events to any signed-in reader holding the thread id. Widening " <>
464 464
              "is recorded in the transcript as `thread.visibility_set`. The " <>
465 465
              "`pulse` and `glass` tiers have no thread read path behind them " <>

@@ -469,13 +469,13 @@ defmodule OpenAgentsWeb.ApiExtensionController do

469 469
              "and a reader admitted by the tier is served `\"grant\": null`."
470 470
        },
471 471
        "model" => %{
472
          "endpoint" => "POST /api/v3/threads",
472
          "endpoint" => "POST /api/v1/threads",
473 473
          "type" => "string",
474 474
          "enum" => OpenAgents.Inference.Models.ids(),
475 475
          "default" => OpenAgents.Inference.Models.default_id(),
476 476
          "description" =>
477 477
            "The model the thread's grant pins, and therefore the model every " <>
478
              "call at the inference proxy reaches. GET /api/v3/models is the " <>
478
              "call at the inference proxy reaches. GET /api/v1/models is the " <>
479 479
              "typed catalog behind this enum, with each model's provider, " <>
480 480
              "ceilings, and availability. A value outside this enum is " <>
481 481
              "refused with a field-level 422 naming `model`; a listed model " <>

@@ -484,7 +484,7 @@ defmodule OpenAgentsWeb.ApiExtensionController do

484 484
              "second thread to run other work on another model."
485 485
        },
486 486
        "reasoning" => %{
487
          "endpoint" => "POST /api/v3/threads",
487
          "endpoint" => "POST /api/v1/threads",
488 488
          "type" => "string",
489 489
          "enum" => OpenAgents.Threads.Thread.reasoning_efforts(),
490 490
          "default" => OpenAgents.Threads.default_reasoning(),

@@ -494,7 +494,7 @@ defmodule OpenAgentsWeb.ApiExtensionController do

494 494
              "`reasoning` rather than replaced by the default."
495 495
        },
496 496
        "permission_profile" => %{
497
          "endpoint" => "POST /api/v3/threads",
497
          "endpoint" => "POST /api/v1/threads",
498 498
          "type" => "string",
499 499
          "enum" => OpenAgents.Threads.Thread.permission_profiles(),
500 500
          "default" => OpenAgents.Threads.default_permission_profile(),
lib/openagents_web/controllers/chat_turn_controller.ex modified +1 -1

@@ -3,7 +3,7 @@ defmodule OpenAgentsWeb.ChatTurnController do

3 3
  Bearer-authenticated access to an account's durable chat turns.
4 4
5 5
  A turn may name the backend that answers it with `model`, whose supported
6
  values `GET /api/v3` publishes. An unsupported value is refused with a
6
  values `GET /api/v1` publishes. An unsupported value is refused with a
7 7
  field-level `422` rather than quietly answered by the default, because a
8 8
  caller that asked for one model and was served another has no way to tell.
9 9
  """
lib/openagents_web/controllers/fleet_target_controller.ex modified +3 -3

@@ -15,7 +15,7 @@ defmodule OpenAgentsWeb.FleetTargetController do

15 15
  classifier, direct-load, relup, and rolling-replacement lanes still own
16 16
  execution, and the caller polls the target to a terminal state.
17 17
18
  Refusals use `OpenAgentsWeb.ApiError`, the one `/api/v3` envelope, so release
18
  Refusals use `OpenAgentsWeb.ApiError`, the one `/api/v1` envelope, so release
19 19
  tooling reads one shape and branches on one stable code.
20 20
  """
21 21

@@ -101,7 +101,7 @@ defmodule OpenAgentsWeb.FleetTargetController do

101 101
  end
102 102
103 103
  defp status_url(conn, %Target{id: id}) do
104
    url(conn, ~p"/api/v3/admin/forge/targets/#{id}")
104
    url(conn, ~p"/api/v1/admin/forge/targets/#{id}")
105 105
  end
106 106
107 107
  defp fetch(id) do

@@ -136,7 +136,7 @@ defmodule OpenAgentsWeb.FleetTargetController do

136 136
  end
137 137
138 138
  # Codes come from `OpenAgentsWeb.ApiError`'s one table, so this surface cannot
139
  # disagree with the rest of `/api/v3` about what a refusal is worth.
139
  # disagree with the rest of `/api/v1` about what a refusal is worth.
140 140
  defp failure(conn, :not_operator), do: ApiError.refuse(conn, "not_operator")
141 141
142 142
  defp failure(conn, :target_not_found), do: ApiError.not_found(conn)
lib/openagents_web/controllers/forum_api_controller.ex modified +1 -1

@@ -1,6 +1,6 @@

1 1
defmodule OpenAgentsWeb.ForumApiController do
2 2
  @moduledoc """
3
  The forum surface of the `/api/v3` JSON API: boards, topics, posts, and
3
  The forum surface of the `/api/v1` JSON API: boards, topics, posts, and
4 4
  legacy identity claims.
5 5
6 6
  Reads are public. Writes require a `forge:write` API token and attribute
lib/openagents_web/controllers/forum_api_json.ex modified +1 -1

@@ -1,5 +1,5 @@

1 1
defmodule OpenAgentsWeb.ForumApiJSON do
2
  @moduledoc "Renders forum JSON for the `/api/v3` surface."
2
  @moduledoc "Renders forum JSON for the `/api/v1` surface."
3 3
4 4
  alias OpenAgents.Forum.ActorLink
5 5
lib/openagents_web/controllers/issue_controller.ex modified +1 -1

@@ -97,7 +97,7 @@ defmodule OpenAgentsWeb.IssueController do

97 97
      # `pull_request` object, so this endpoint asks for both by default even
98 98
      # though `OpenAgents.Issues` defaults its lists to issues alone. A client
99 99
      # that wants one kind without the other sends `?type=`, which GitHub has
100
      # no counterpart for and which `GET /api/v3` therefore publishes as an
100
      # no counterpart for and which `GET /api/v1` therefore publishes as an
101 101
      # `issue.openagents` filter.
102 102
      type: Map.get(params, "type", "all"),
103 103
      state: Map.get(params, "state", "open"),
lib/openagents_web/controllers/issue_json.ex modified +3 -3

@@ -22,7 +22,7 @@ defmodule OpenAgentsWeb.IssueJSON do

22 22
  stays the requested outcome and never becomes a second work record.
23 23
24 24
  Every key this module can put inside the object is enumerated at
25
  `GET /api/v3`, and `OpenAgentsWeb.ApiExtensionGovernanceTest` fails if one
25
  `GET /api/v1`, and `OpenAgentsWeb.ApiExtensionGovernanceTest` fails if one
26 26
  is not.
27 27
  """
28 28

@@ -65,7 +65,7 @@ defmodule OpenAgentsWeb.IssueJSON do

65 65
      updated_at: issue.updated_at,
66 66
      closed_at: issue.closed_at,
67 67
      html_url: "#{url_base}/#{owner}/#{repo}/issues/#{issue.number}",
68
      url: "#{url_base}/api/v3/repos/#{owner}/#{repo}/issues/#{issue.number}"
68
      url: "#{url_base}/api/v1/repos/#{owner}/#{repo}/issues/#{issue.number}"
69 69
    }
70 70
    |> put_pull_request(issue, assigns, owner, repo, url_base)
71 71
    |> put_extension(issue, assigns)

@@ -89,7 +89,7 @@ defmodule OpenAgentsWeb.IssueJSON do

89 89
        json
90 90
        |> Map.put(:draft, marker.draft)
91 91
        |> Map.put(:pull_request, %{
92
          url: "#{url_base}/api/v3/repos/#{owner}/#{repo}/pulls/#{issue.number}",
92
          url: "#{url_base}/api/v1/repos/#{owner}/#{repo}/pulls/#{issue.number}",
93 93
          html_url: "#{url_base}/#{owner}/#{repo}/pulls/#{issue.number}",
94 94
          merged_at: marker.merged_at
95 95
        })
lib/openagents_web/controllers/label_json.ex modified +1 -1

@@ -22,7 +22,7 @@ defmodule OpenAgentsWeb.LabelJSON do

22 22
      description: label.description,
23 23
      default: false,
24 24
      url:
25
        "#{url_base(assigns)}/api/v3/repos/#{owner}/#{repo}/labels/#{URI.encode(label.name, &URI.char_unreserved?/1)}"
25
        "#{url_base(assigns)}/api/v1/repos/#{owner}/#{repo}/labels/#{URI.encode(label.name, &URI.char_unreserved?/1)}"
26 26
    }
27 27
  end
28 28
lib/openagents_web/controllers/milestone_json.ex modified +1 -1

@@ -24,7 +24,7 @@ defmodule OpenAgentsWeb.MilestoneJSON do

24 24
      due_on: milestone.due_on,
25 25
      open_issues: milestone.open_issues,
26 26
      closed_issues: milestone.closed_issues,
27
      url: "#{url_base(assigns)}/api/v3/repos/#{owner}/#{repo}/milestones/#{milestone.number}"
27
      url: "#{url_base(assigns)}/api/v1/repos/#{owner}/#{repo}/milestones/#{milestone.number}"
28 28
    }
29 29
  end
30 30
lib/openagents_web/controllers/model_catalog_controller.ex modified +1 -1

@@ -1,6 +1,6 @@

1 1
defmodule OpenAgentsWeb.ModelCatalogController do
2 2
  @moduledoc """
3
  `GET /api/v3/models`: the typed model catalog this deployment serves.
3
  `GET /api/v1/models`: the typed model catalog this deployment serves.
4 4
5 5
  The CLI renders model selection from this list instead of guessing, so it is
6 6
  the same list every admission checks against — `OpenAgents.Inference.Models`
lib/openagents_web/controllers/project_json.ex modified +1 -1

@@ -96,7 +96,7 @@ defmodule OpenAgentsWeb.ProjectJSON do

96 96
        repo: repository.name,
97 97
        number: issue.number,
98 98
        url:
99
          "#{base_url}/api/v3/repos/#{repository.owner}/#{repository.name}/issues/#{issue.number}",
99
          "#{base_url}/api/v1/repos/#{repository.owner}/#{repository.name}/issues/#{issue.number}",
100 100
        html_url: "#{base_url}/#{repository.owner}/#{repository.name}/issues/#{issue.number}"
101 101
      },
102 102
      values: values
lib/openagents_web/controllers/pull_request_json.ex modified +1 -1

@@ -31,7 +31,7 @@ defmodule OpenAgentsWeb.PullRequestJSON do

31 31
      created_at: pr.inserted_at,
32 32
      updated_at: pr.updated_at,
33 33
      html_url: "#{base_url}/#{owner}/#{repo}/pulls/#{pr.issue.number}",
34
      url: "#{base_url}/api/v3/repos/#{owner}/#{repo}/pulls/#{pr.issue.number}"
34
      url: "#{base_url}/api/v1/repos/#{owner}/#{repo}/pulls/#{pr.issue.number}"
35 35
    }
36 36
  end
37 37
lib/openagents_web/controllers/stack_json.ex modified +3 -3

@@ -47,7 +47,7 @@ defmodule OpenAgentsWeb.StackJSON do

47 47
      created_at: operation.inserted_at,
48 48
      completed_at: operation.completed_at,
49 49
      url:
50
        "#{base_url}/api/v3/repos/#{assigns.owner}/#{assigns.repo}/pulls/#{assigns.pull_number}/merge-async/#{operation.id}"
50
        "#{base_url}/api/v1/repos/#{assigns.owner}/#{assigns.repo}/pulls/#{assigns.pull_number}/merge-async/#{operation.id}"
51 51
    }
52 52
53 53
    case Map.get(assigns, :replay_state) do

@@ -82,7 +82,7 @@ defmodule OpenAgentsWeb.StackJSON do

82 82
      entries: Enum.map(stack.entries, &entry(&1, owner, repo, base_url)),
83 83
      created_at: stack.inserted_at,
84 84
      updated_at: stack.updated_at,
85
      url: "#{base_url}/api/v3/repos/#{owner}/#{repo}/stacks/#{stack.number}"
85
      url: "#{base_url}/api/v1/repos/#{owner}/#{repo}/stacks/#{stack.number}"
86 86
    }
87 87
  end
88 88

@@ -98,7 +98,7 @@ defmodule OpenAgentsWeb.StackJSON do

98 98
        state: pull_request.issue.state,
99 99
        head: %{ref: pull_request.head_ref, sha: pull_request.head_sha},
100 100
        base: %{ref: pull_request.base_ref, sha: pull_request.base_sha},
101
        url: "#{base_url}/api/v3/repos/#{owner}/#{repo}/pulls/#{pull_request.issue.number}"
101
        url: "#{base_url}/api/v1/repos/#{owner}/#{repo}/pulls/#{pull_request.issue.number}"
102 102
      }
103 103
    }
104 104
  end
lib/openagents_web/controllers/thread_controller.ex modified +3 -3

@@ -172,7 +172,7 @@ defmodule OpenAgentsWeb.ThreadController do

172 172
      # the generation, and hands back fresh authority on the same thread, so
173 173
      # a resumed session can never race a zombie of its former self
174 174
      # (THREAD-001). The plaintext token exists exactly once, in this
175
      # response, like the one `POST /api/v3/threads` returns.
175
      # response, like the one `POST /api/v1/threads` returns.
176 176
      case Threads.mint_grant(thread) do
177 177
        {:ok, minted, grant, token} ->
178 178
          conn

@@ -310,7 +310,7 @@ defmodule OpenAgentsWeb.ThreadController do

310 310
311 311
    sentence =
312 312
      "This account holds #{held} open threads and the configured maximum is #{limit}. " <>
313
        "Revoke a thread with DELETE /api/v3/threads/{thread_id} before opening another."
313
        "Revoke a thread with DELETE /api/v1/threads/{thread_id} before opening another."
314 314
315 315
    ApiError.refuse(conn, "thread_quota_reached",
316 316
      message: sentence,

@@ -602,7 +602,7 @@ defmodule OpenAgentsWeb.ThreadController do

602 602
    sentence =
603 603
      "#{inspect(model_id)} is in the catalog but its provider is not configured " <>
604 604
        "on this deployment. Currently available: " <>
605
        "#{Enum.join(Models.available_ids(), ", ")}. See GET /api/v3/models."
605
        "#{Enum.join(Models.available_ids(), ", ")}. See GET /api/v1/models."
606 606
607 607
    ApiError.refuse(conn, "model_unavailable",
608 608
      message: sentence,
lib/openagents_web/docs_catalog.ex modified +4 -4

@@ -94,7 +94,7 @@ defmodule OpenAgentsWeb.DocsCatalog do

94 94
          slug: "cli-api",
95 95
          title: "Call the API with the CLI",
96 96
          icon: "square-code",
97
          route: "/api/v3/repos/:owner/:repo/issues"
97
          route: "/api/v1/repos/:owner/:repo/issues"
98 98
        }
99 99
      ]
100 100
    },

@@ -159,7 +159,7 @@ defmodule OpenAgentsWeb.DocsCatalog do

159 159
          slug: "merging-stacks",
160 160
          title: "Merging stacks",
161 161
          icon: "check-circle",
162
          route: "/api/v3/repos/:owner/:repo/stacks"
162
          route: "/api/v1/repos/:owner/:repo/stacks"
163 163
        }
164 164
      ]
165 165
    },

@@ -223,13 +223,13 @@ defmodule OpenAgentsWeb.DocsCatalog do

223 223
          slug: "rest-api",
224 224
          title: "REST API",
225 225
          icon: "square-code",
226
          route: "/api/v3/repos/:owner/:repo/issues"
226
          route: "/api/v1/repos/:owner/:repo/issues"
227 227
        },
228 228
        %{
229 229
          slug: "stacks-api",
230 230
          title: "Stacks API",
231 231
          icon: "square-code",
232
          route: "/api/v3/repos/:owner/:repo/stacks"
232
          route: "/api/v1/repos/:owner/:repo/stacks"
233 233
        },
234 234
        %{slug: "status-api", title: "Status API", icon: "info", route: "/api/status"}
235 235
      ]
lib/openagents_web/endpoint.ex modified +1

@@ -67,5 +67,6 @@ defmodule OpenAgentsWeb.Endpoint do

67 67
  # Analytics hints only; identity still comes from the session.
68 68
  plug PostHog.Integrations.Plug
69 69
70
  plug OpenAgentsWeb.Plugs.ApiV3Rewrite
70 71
  plug OpenAgentsWeb.Router
71 72
end
lib/openagents_web/live/gym_live.ex modified +2 -2

@@ -3,7 +3,7 @@ defmodule OpenAgentsWeb.GymLive do

3 3
  The Gym: graded benchmark runs of our agents, operator-only.
4 4
5 5
  Read-only over `OpenAgents.Gym` — the harness runs elsewhere and posts
6
  results through `POST /api/v3/gym/runs`; this surface is the scoreboard
6
  results through `POST /api/v1/gym/runs`; this surface is the scoreboard
7 7
  that capability work (models, plugins, harness changes) is read against.
8 8
  Operator-gated the same way `/chat` is: the route sits behind the
9 9
  `:operator` pipeline, the mount re-checks, and every event re-checks,

@@ -101,7 +101,7 @@ defmodule OpenAgentsWeb.GymLive do

101 101
        <div :if={@runs_empty?}>
102 102
          <.empty title="No runs recorded yet">
103 103
            No graded runs have been posted. The harness records one with
104
            <code>POST /api/v3/gym/runs</code>
104
            <code>POST /api/v1/gym/runs</code>
105 105
            — see <code>docs/2026-08-24-harbor-terminal-bench-plan.md</code>.
106 106
          </.empty>
107 107
        </div>
lib/openagents_web/live/home_live.ex modified +1 -1

@@ -541,7 +541,7 @@ defmodule OpenAgentsWeb.HomeLive do

541 541
          </:item>
542 542
          <:item question="What does the API look like?">
543 543
            <p>
544
              It is shaped after GitHub's REST API and served under <code>/api/v3</code>. An existing client usually needs only a base URL
544
              It is shaped after GitHub's REST API and served under <code>/api/v1</code>. An existing client usually needs only a base URL
545 545
              change.
546 546
            </p>
547 547
          </:item>
lib/openagents_web/live/memory_live.ex modified +273 -9

@@ -23,6 +23,9 @@ defmodule OpenAgentsWeb.MemoryLive do

23 23
  alias OpenAgents.Analytics
24 24
  alias OpenAgents.Conversations
25 25
  alias OpenAgents.DataRights
26
  alias OpenAgents.ExperienceMemory
27
  alias OpenAgents.GraphMemory
28
  alias OpenAgents.Memory.Consent
26 29
  alias OpenAgents.ProfileMemory
27 30
  alias OpenAgents.Voice.Recordings
28 31

@@ -48,7 +51,8 @@ defmodule OpenAgentsWeb.MemoryLive do

48 51
     |> assign(:leaderboard_opted_out?, current_user.public_leaderboard_opted_out)
49 52
     |> assign(:recording_config, Recordings.config())
50 53
     |> assign(:privacy_delete_form, to_form(%{"confirmation" => ""}, as: :privacy))
51
     |> reload_memory()}
54
     |> assign(:work_scope, "conversation:#{conversation.id}")
55
     |> load_dashboard()}
52 56
  end
53 57
54 58
  @impl true

@@ -68,6 +72,11 @@ defmodule OpenAgentsWeb.MemoryLive do

68 72
        privacy_delete_form={@privacy_delete_form}
69 73
        recording_config={@recording_config}
70 74
        leaderboard_opted_out?={@leaderboard_opted_out?}
75
        work_scope={@work_scope}
76
        graph_entities={@graph_entities}
77
        engram_timeline={@engram_timeline}
78
        derived_heuristics={@derived_heuristics}
79
        supersession_trail={@supersession_trail}
71 80
      />
72 81
    </Layouts.app>
73 82
    """

@@ -179,29 +188,121 @@ defmodule OpenAgentsWeb.MemoryLive do

179 188
     socket
180 189
     |> assign(:pending_memory_action, nil)
181 190
     |> assign(:memory_status, {status, message})
182
     |> reload_memory()}
191
     |> load_dashboard()}
192
  end
193
194
  def handle_event("retract_engram", %{"record_ref" => ref}, socket) do
195
    owner = socket.assigns.memory_owner
196
197
    context_consent = %{
198
      "kind" => "first_party_ui",
199
      "operation" => "forget",
200
      "claim" => ref
201
    }
202
203
    result =
204
      with {:ok, _consent} <- Consent.forget(nil, "record", ref, context_consent),
205
           record_id <- String.replace_prefix(ref, "experience:", ""),
206
           {:ok, parsed_id} <- Ecto.UUID.cast(record_id),
207
           {:ok, receipt} <- ExperienceMemory.delete(owner, parsed_id, "operator_retraction") do
208
        {:ok, receipt}
209
      else
210
        :error -> {:error, :invalid_engram_ref}
211
        {:error, reason} -> {:error, reason}
212
      end
213
214
    message =
215
      case result do
216
        {:ok, receipt} ->
217
          "Retracted #{ref}. Receipt #{receipt.receipt_digest}."
218
219
        {:error, :invalid_engram_ref} ->
220
          "That engram reference is not valid."
221
222
        {:error, reason} ->
223
          "Could not retract engram: #{format_reason(reason)}"
224
      end
225
226
    status = if match?({:ok, _}, result), do: :ok, else: :error
227
228
    {:noreply,
229
     socket
230
     |> assign(:memory_status, {status, message})
231
     |> load_dashboard()}
232
  end
233
234
  defp format_reason(reason) do
235
    cond do
236
      is_binary(reason) -> reason
237
      is_atom(reason) -> String.replace_leading(Atom.to_string(reason), "Elixir.", "")
238
      true -> inspect(reason)
239
    end
183 240
  end
184 241
185 242
  # Another tab correcting or forgetting a record must be reflected here: the
186 243
  # records are one account's, not one socket's.
187 244
  @impl true
188 245
  def handle_info({:profile_memory_updated, _result}, socket) do
189
    {:noreply, reload_memory(socket)}
246
    {:noreply, load_dashboard(socket)}
190 247
  end
191 248
192 249
  def handle_info(_message, socket), do: {:noreply, socket}
193 250
194
  defp reload_memory(socket) do
195
    case ProfileMemory.export(socket.assigns.memory_owner) do
196
      {:ok, export} -> assign(socket, :memory_records, export["records"])
197
      {:error, _reason} -> assign(socket, :memory_status, {:error, "Memory is unavailable."})
198
    end
251
  defp load_dashboard(socket) do
252
    owner = socket.assigns.memory_owner
253
    work_scope = socket.assigns.work_scope
254
    profile_result = ProfileMemory.export(owner)
255
256
    profile_export =
257
      case profile_result do
258
        {:ok, export} -> export
259
        {:error, _reason} -> %{"records" => []}
260
      end
261
262
    memory_status =
263
      case {socket.assigns.memory_status, profile_result} do
264
        {nil, {:error, _reason}} -> {:error, "Memory is unavailable."}
265
        {status, _} -> status
266
      end
267
268
    graph_entities =
269
      case GraphMemory.export(owner, work_scope) do
270
        {:ok, export} -> export
271
        {:error, _reason} -> nil
272
      end
273
274
    {engram_timeline, derived_heuristics} =
275
      case ExperienceMemory.export_scope(owner, work_scope) do
276
        {:ok, export} -> {export["records"] || [], export["patterns"] || []}
277
        {:error, _reason} -> {[], []}
278
      end
279
280
    records_by_id = Map.new(profile_export["records"] || [], &{&1["id"], &1})
281
282
    supersession_trail =
283
      Enum.reduce(profile_export["records"] || [], [], fn record, acc ->
284
        if record["supersedes_record_id"] do
285
          previous = Map.get(records_by_id, record["supersedes_record_id"])
286
          [%{previous: previous, replacement: record} | acc]
287
        else
288
          acc
289
        end
290
      end)
291
      |> Enum.reverse()
292
293
    socket
294
    |> assign(:memory_records, profile_export["records"] || [])
295
    |> assign(:memory_status, memory_status)
296
    |> assign(:graph_entities, graph_entities)
297
    |> assign(:engram_timeline, engram_timeline)
298
    |> assign(:derived_heuristics, derived_heuristics)
299
    |> assign(:supersession_trail, supersession_trail)
199 300
  end
200 301
201 302
  defp memory_result(socket, :ok, message) do
202 303
    socket
203 304
    |> assign(:memory_status, {:ok, message})
204
    |> reload_memory()
305
    |> load_dashboard()
205 306
  end
206 307
207 308
  defp memory_result(socket, {:error, reason}, _message) do

@@ -271,6 +372,11 @@ defmodule OpenAgentsWeb.MemoryLive do

271 372
  attr :privacy_delete_form, :any, required: true
272 373
  attr :recording_config, :map, required: true
273 374
  attr :leaderboard_opted_out?, :boolean, required: true
375
  attr :work_scope, :string, required: true
376
  attr :graph_entities, :any, default: nil
377
  attr :engram_timeline, :list, default: []
378
  attr :derived_heuristics, :list, default: []
379
  attr :supersession_trail, :list, default: []
274 380
275 381
  defp memory_manager(assigns) do
276 382
    ~H"""

@@ -394,6 +500,14 @@ defmodule OpenAgentsWeb.MemoryLive do

394 500
        <.memory_record :for={record <- @memory_records} record={record} />
395 501
      </div>
396 502
503
      <.memory_dashboard
504
        work_scope={@work_scope}
505
        graph_entities={@graph_entities}
506
        engram_timeline={@engram_timeline}
507
        derived_heuristics={@derived_heuristics}
508
        supersession_trail={@supersession_trail}
509
      />
510
397 511
      <.card id="leaderboard-preference" aria-labelledby="leaderboard-preference-heading">
398 512
        <header>
399 513
          <h2 id="leaderboard-preference-heading">Public leaderboard</h2>

@@ -571,4 +685,154 @@ defmodule OpenAgentsWeb.MemoryLive do

571 685
    </.card>
572 686
    """
573 687
  end
688
689
  attr :work_scope, :string, required: true
690
  attr :graph_entities, :any, default: nil
691
  attr :engram_timeline, :list, default: []
692
  attr :derived_heuristics, :list, default: []
693
  attr :supersession_trail, :list, default: []
694
695
  defp memory_dashboard(assigns) do
696
    ~H"""
697
    <section id="memory-dashboard" class="memory-dashboard" aria-labelledby="memory-dashboard-heading">
698
      <h2 id="memory-dashboard-heading">Memory evolution and consent</h2>
699
700
      <.card id="active-memory-graph" aria-labelledby="active-memory-graph-heading">
701
        <header>
702
          <h3 id="active-memory-graph-heading">Active memory graph</h3>
703
        </header>
704
        <.empty :if={is_nil(@graph_entities)} title="Graph projection unavailable">
705
          Graph memory is disabled or has not been built for this conversation yet.
706
        </.empty>
707
        <div :if={not is_nil(@graph_entities)} class="memory-graph">
708
          <p>
709
            <.badge variant={:info}>{@graph_entities["manifest"]["ref"]}</.badge>
710
          </p>
711
          <.list :if={active_graph_nodes(@graph_entities) != []}>
712
            <:item
713
              :for={node <- active_graph_nodes(@graph_entities)}
714
              title={node["artifact_id"]}
715
            >
716
              <.badge variant={:success}>{node["entity_kind"] || "node"}</.badge>
717
              <span class="memory-graph__label">{graph_node_label(node)}</span>
718
            </:item>
719
          </.list>
720
          <.empty :if={active_graph_nodes(@graph_entities) == []} title="No active graph entities">
721
            The current graph has no active nodes.
722
          </.empty>
723
        </div>
724
      </.card>
725
726
      <.card id="engram-timeline" aria-labelledby="engram-timeline-heading">
727
        <header>
728
          <h3 id="engram-timeline-heading">Recent engrams</h3>
729
        </header>
730
        <.empty :if={@engram_timeline == []} title="No recent engrams">
731
          Experience memory is empty for this conversation.
732
        </.empty>
733
        <.list :if={@engram_timeline != []}>
734
          <:item
735
            :for={engram <- @engram_timeline}
736
            title={engram["objective"]}
737
          >
738
            <div class="memory-engram__meta">
739
              <.badge variant={engram_state_variant(engram["state"])}>
740
                {String.upcase(engram["state"])}
741
              </.badge>
742
              <.badge variant={:default}>{engram["record_ref"]}</.badge>
743
            </div>
744
            <p :if={engram["outcome"]} class="memory-engram__outcome">
745
              {engram["outcome"]}
746
            </p>
747
            <.button
748
              id={"retract-engram-#{engram_id(engram["record_ref"])}"}
749
              variant={:link}
750
              tone={:danger}
751
              size={:sm}
752
              phx-click="retract_engram"
753
              phx-value-record_ref={engram["record_ref"]}
754
            >
755
              <.icon name="trash" /> RETRACT
756
            </.button>
757
          </:item>
758
        </.list>
759
      </.card>
760
761
      <.card id="derived-heuristics" aria-labelledby="derived-heuristics-heading">
762
        <header>
763
          <h3 id="derived-heuristics-heading">Derived heuristics</h3>
764
        </header>
765
        <.empty :if={@derived_heuristics == []} title="No derived heuristics">
766
          No advisory patterns have been synthesized yet.
767
        </.empty>
768
        <.list :if={@derived_heuristics != []}>
769
          <:item
770
            :for={pattern <- @derived_heuristics}
771
            title={pattern["phenomenon"]}
772
          >
773
            <p>{pattern["applicability"]}</p>
774
            <div class="memory-heuristic__support">
775
              <.badge variant={:info}>Effect: {pattern["expected_effect"]}</.badge>
776
              <.badge variant={:success}>Successes: {pattern["support"]["successes"]}</.badge>
777
              <.badge variant={:danger}>Failures: {pattern["support"]["failures"]}</.badge>
778
            </div>
779
          </:item>
780
        </.list>
781
      </.card>
782
783
      <.card id="supersession-audit" aria-labelledby="supersession-audit-heading">
784
        <header>
785
          <h3 id="supersession-audit-heading">Supersession audit trail</h3>
786
        </header>
787
        <.empty :if={@supersession_trail == []} title="No supersessions">
788
          No corrections or supersessions are recorded.
789
        </.empty>
790
        <.list :if={@supersession_trail != []}>
791
          <:item
792
            :for={event <- @supersession_trail}
793
            title={event.replacement["claim"] || "WITHHELD"}
794
          >
795
            <p>
796
              Superseded
797
              <time :if={event.previous["inserted_at"]} datetime={event.previous["inserted_at"]}>
798
                {memory_date(event.previous["inserted_at"])}
799
              </time>
800
              : {event.previous["claim"] || "WITHHELD"}
801
            </p>
802
            <.badge variant={:warning}>Superseded by {event.replacement["id"]}</.badge>
803
          </:item>
804
        </.list>
805
      </.card>
806
    </section>
807
    """
808
  end
809
810
  defp active_graph_nodes(graph) do
811
    (graph["artifacts"] || [])
812
    |> Enum.filter(fn artifact ->
813
      artifact["kind"] == "node" and
814
        (artifact["properties"]["eligible_for_traversal"] == true or
815
           artifact["entity_kind"] == "pattern")
816
    end)
817
  end
818
819
  defp graph_node_label(node) do
820
    properties = node["properties"] || %{}
821
822
    cond do
823
      properties["phenomenon"] -> properties["phenomenon"]
824
      properties["outcome"] -> properties["outcome"]
825
      properties["label"] -> properties["label"]
826
      true -> node["identity_key"] || ""
827
    end
828
  end
829
830
  defp engram_state_variant("succeeded"), do: :success
831
  defp engram_state_variant("failed"), do: :danger
832
  defp engram_state_variant("corrected"), do: :warning
833
  defp engram_state_variant("running"), do: :info
834
  defp engram_state_variant(_state), do: :default
835
836
  defp engram_id(ref) when is_binary(ref), do: String.replace_prefix(ref, "experience:", "")
837
  defp engram_id(_ref), do: ""
574 838
end
lib/openagents_web/live/thread_index_live.ex modified +1 -1

@@ -48,7 +48,7 @@ defmodule OpenAgentsWeb.ThreadIndexLive do

48 48
        <.empty :if={@threads_empty?} id="threads-empty" title="No threads yet">
49 49
          Open one with
50 50
          <.kbd>openagents coder</.kbd>
51
          or <.kbd>POST /api/v3/threads</.kbd>.
51
          or <.kbd>POST /api/v1/threads</.kbd>.
52 52
        </.empty>
53 53
54 54
        <.table :if={!@threads_empty?} id="threads-table" rows={@streams.threads}>
lib/openagents_web/plugs/api_v3_rewrite.ex added +20

@@ -0,0 +1,20 @@

1
defmodule OpenAgentsWeb.Plugs.ApiV3Rewrite do
2
  @moduledoc """
3
  Transparently rewrites `/api/v3/*` requests to `/api/v1/*`.
4
5
  During the `/api/v3` to `/api/v1` migration, legacy clients continue to call
6
  `/api/v3/*`. This plug modifies `conn.path_info` so the router routes the
7
  request to the `/api/v1` scopes without requiring an HTTP redirect.
8
  """
9
  @behaviour Plug
10
11
  @impl true
12
  def init(opts), do: opts
13
14
  @impl true
15
  def call(%Plug.Conn{path_info: ["api", "v3" | rest]} = conn, _opts) do
16
    %{conn | path_info: ["api", "v1" | rest]}
17
  end
18
19
  def call(conn, _opts), do: conn
20
end
lib/openagents_web/route_authority.ex modified +58 -58

@@ -69,26 +69,26 @@ defmodule OpenAgentsWeb.RouteAuthority do

69 69
  # is what the token changes, and every one of these routes resolves the
70 70
  # repository through `Repositories.get_visible_by_path!/3`.
71 71
  @optional_forge_read_paths [
72
    "/api/v3/repos/:owner/:repo/issues",
73
    "/api/v3/repos/:owner/:repo/issues/:issue_number",
74
    "/api/v3/repos/:owner/:repo/issues/:issue_number/dependencies",
75
    "/api/v3/repos/:owner/:repo/issues/:issue_number/comments",
76
    "/api/v3/repos/:owner/:repo/issues/comments/:id",
77
    "/api/v3/repos/:owner/:repo/issues/:issue_number/labels",
78
    "/api/v3/repos/:owner/:repo/issues/:issue_number/assignees",
79
    "/api/v3/repos/:owner/:repo/labels",
80
    "/api/v3/repos/:owner/:repo/labels/:name",
81
    "/api/v3/repos/:owner/:repo/milestones",
82
    "/api/v3/repos/:owner/:repo/milestones/:milestone_number",
83
    "/api/v3/repos/:owner/:repo/assignees",
84
    "/api/v3/repos/:owner/:repo/assignees/:assignee",
85
    "/api/v3/repos/:owner/:repo/pulls",
86
    "/api/v3/repos/:owner/:repo/pulls/:pull_number",
87
    "/api/v3/repos/:owner/:repo/projectsV2",
88
    "/api/v3/repos/:owner/:repo/projectsV2/:project_number",
89
    "/api/v3/repos/:owner/:repo/projectsV2/:project_number/items",
90
    "/api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/events",
91
    "/api/v3/repos/:owner/:repo/projectsV2/:project_number/fields"
72
    "/api/v1/repos/:owner/:repo/issues",
73
    "/api/v1/repos/:owner/:repo/issues/:issue_number",
74
    "/api/v1/repos/:owner/:repo/issues/:issue_number/dependencies",
75
    "/api/v1/repos/:owner/:repo/issues/:issue_number/comments",
76
    "/api/v1/repos/:owner/:repo/issues/comments/:id",
77
    "/api/v1/repos/:owner/:repo/issues/:issue_number/labels",
78
    "/api/v1/repos/:owner/:repo/issues/:issue_number/assignees",
79
    "/api/v1/repos/:owner/:repo/labels",
80
    "/api/v1/repos/:owner/:repo/labels/:name",
81
    "/api/v1/repos/:owner/:repo/milestones",
82
    "/api/v1/repos/:owner/:repo/milestones/:milestone_number",
83
    "/api/v1/repos/:owner/:repo/assignees",
84
    "/api/v1/repos/:owner/:repo/assignees/:assignee",
85
    "/api/v1/repos/:owner/:repo/pulls",
86
    "/api/v1/repos/:owner/:repo/pulls/:pull_number",
87
    "/api/v1/repos/:owner/:repo/projectsV2",
88
    "/api/v1/repos/:owner/:repo/projectsV2/:project_number",
89
    "/api/v1/repos/:owner/:repo/projectsV2/:project_number/items",
90
    "/api/v1/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/events",
91
    "/api/v1/repos/:owner/:repo/projectsV2/:project_number/fields"
92 92
  ]
93 93
94 94
  @spec classes() :: [atom()]

@@ -262,7 +262,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

262 262
              verb in [:get, :head],
263 263
       do: declaration(:public_read, "anonymous", "published:api-contract", false)
264 264
265
  defp policy(%{path: "/api/v3", verb: verb}) when verb in [:get, :head],
265
  defp policy(%{path: "/api/v1", verb: verb}) when verb in [:get, :head],
266 266
    do: declaration(:public_read, "anonymous", "published:api-extensions", false)
267 267
268 268
  defp policy(%{path: "/controller/pairings", verb: :post}),

@@ -292,7 +292,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

292 292
           false
293 293
         )
294 294
295
  defp policy(%{path: "/api/v3/agent/credentials", verb: :post}),
295
  defp policy(%{path: "/api/v1/agent/credentials", verb: :post}),
296 296
    do:
297 297
      declaration(
298 298
        :authenticated_api,

@@ -303,11 +303,11 @@ defmodule OpenAgentsWeb.RouteAuthority do

303 303
304 304
  defp policy(%{path: path, verb: verb})
305 305
       when path in [
306
              "/api/v3/computers",
307
              "/api/v3/computers/:id",
308
              "/api/v3/computers/:computer_id/probe",
309
              "/api/v3/computers/:computer_id/agent-jobs",
310
              "/api/v3/computer-agent-jobs/:id"
306
              "/api/v1/computers",
307
              "/api/v1/computers/:id",
308
              "/api/v1/computers/:computer_id/probe",
309
              "/api/v1/computers/:computer_id/agent-jobs",
310
              "/api/v1/computer-agent-jobs/:id"
311 311
            ] do
312 312
    declaration(
313 313
      :authenticated_api,

@@ -317,7 +317,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

317 317
    )
318 318
  end
319 319
320
  defp policy(%{path: "/api/v3/conversations/:conversation_id/boxes", verb: verb})
320
  defp policy(%{path: "/api/v1/conversations/:conversation_id/boxes", verb: verb})
321 321
       when verb in [:get, :post],
322 322
       do:
323 323
         declaration(

@@ -327,7 +327,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

327 327
           verb == :post
328 328
         )
329 329
330
  defp policy(%{path: "/api/v3/conversations/:conversation_id/boxes/:box_id", verb: :get}),
330
  defp policy(%{path: "/api/v1/conversations/:conversation_id/boxes/:box_id", verb: :get}),
331 331
    do:
332 332
      declaration(
333 333
        :authenticated_api,

@@ -337,7 +337,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

337 337
      )
338 338
339 339
  defp policy(%{
340
         path: "/api/v3/conversations/:conversation_id/boxes/:box_id/commands",
340
         path: "/api/v1/conversations/:conversation_id/boxes/:box_id/commands",
341 341
         verb: :post
342 342
       }),
343 343
       do:

@@ -348,7 +348,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

348 348
           true
349 349
         )
350 350
351
  defp policy(%{path: "/api/v3/conversations/:conversation_id/boxes/:box_id/stop", verb: :post}),
351
  defp policy(%{path: "/api/v1/conversations/:conversation_id/boxes/:box_id/stop", verb: :post}),
352 352
    do:
353 353
      declaration(
354 354
        :authenticated_api,

@@ -358,7 +358,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

358 358
      )
359 359
360 360
  defp policy(%{
361
         path: "/api/v3/conversations/:conversation_id/boxes/:box_id/assignments",
361
         path: "/api/v1/conversations/:conversation_id/boxes/:box_id/assignments",
362 362
         verb: :post
363 363
       }),
364 364
       do:

@@ -370,7 +370,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

370 370
         )
371 371
372 372
  defp policy(%{
373
         path: "/api/v3/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id",
373
         path: "/api/v1/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id",
374 374
         verb: :get
375 375
       }),
376 376
       do:

@@ -383,7 +383,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

383 383
384 384
  defp policy(%{
385 385
         path:
386
           "/api/v3/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id/cancel",
386
           "/api/v1/conversations/:conversation_id/boxes/:box_id/assignments/:assignment_id/cancel",
387 387
         verb: :post
388 388
       }),
389 389
       do:

@@ -395,7 +395,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

395 395
         )
396 396
397 397
  defp policy(%{
398
         path: "/api/v3/conversations/:conversation_id/computers/:computer_id/assignments",
398
         path: "/api/v1/conversations/:conversation_id/computers/:computer_id/assignments",
399 399
         verb: :post
400 400
       }),
401 401
       do:

@@ -408,7 +408,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

408 408
409 409
  defp policy(%{
410 410
         path:
411
           "/api/v3/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id",
411
           "/api/v1/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id",
412 412
         verb: :get
413 413
       }),
414 414
       do:

@@ -421,7 +421,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

421 421
422 422
  defp policy(%{
423 423
         path:
424
           "/api/v3/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id/cancel",
424
           "/api/v1/conversations/:conversation_id/computers/:computer_id/assignments/:assignment_id/cancel",
425 425
         verb: :post
426 426
       }),
427 427
       do:

@@ -433,7 +433,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

433 433
         )
434 434
435 435
  defp policy(%{
436
         path: "/api/v3/conversations/:conversation_id/boxes/:box_id/runs",
436
         path: "/api/v1/conversations/:conversation_id/boxes/:box_id/runs",
437 437
         verb: verb
438 438
       })
439 439
       when verb in [:get, :post],

@@ -446,7 +446,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

446 446
         )
447 447
448 448
  defp policy(%{
449
         path: "/api/v3/conversations/:conversation_id/boxes/:box_id/runs/:run_id",
449
         path: "/api/v1/conversations/:conversation_id/boxes/:box_id/runs/:run_id",
450 450
         verb: :get
451 451
       }),
452 452
       do:

@@ -458,7 +458,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

458 458
         )
459 459
460 460
  defp policy(%{
461
         path: "/api/v3/conversations/:conversation_id/boxes/:box_id/runs/:run_id/output",
461
         path: "/api/v1/conversations/:conversation_id/boxes/:box_id/runs/:run_id/output",
462 462
         verb: :get
463 463
       }),
464 464
       do:

@@ -470,7 +470,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

470 470
         )
471 471
472 472
  defp policy(%{
473
         path: "/api/v3/conversations/:conversation_id/boxes/:box_id/runs/:run_id/cancel",
473
         path: "/api/v1/conversations/:conversation_id/boxes/:box_id/runs/:run_id/cancel",
474 474
         verb: :post
475 475
       }),
476 476
       do:

@@ -508,7 +508,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

508 508
        verb not in [:get, :head]
509 509
      )
510 510
511
  defp policy(%{path: "/api/v3/device/authorizations" <> _path, verb: :post}),
511
  defp policy(%{path: "/api/v1/device/authorizations" <> _path, verb: :post}),
512 512
    do:
513 513
      declaration(
514 514
        :authenticated_api,

@@ -518,11 +518,11 @@ defmodule OpenAgentsWeb.RouteAuthority do

518 518
      )
519 519
520 520
  defp policy(%{path: path, verb: verb})
521
       when path in ["/api/v3/user", "/api/v3/user/repos", "/api/v3/repository-imports/:id"] and
521
       when path in ["/api/v1/user", "/api/v1/user/repos", "/api/v1/repository-imports/:id"] and
522 522
              verb in [:get, :head],
523 523
       do: declaration(:authenticated_api, "first-party bearer token", "forge:read", false)
524 524
525
  defp policy(%{path: "/api/v3/repos/:owner/:repo", verb: verb}) when verb in [:get, :head],
525
  defp policy(%{path: "/api/v1/repos/:owner/:repo", verb: verb}) when verb in [:get, :head],
526 526
    do:
527 527
      declaration(
528 528
        :public_read,

@@ -532,10 +532,10 @@ defmodule OpenAgentsWeb.RouteAuthority do

532 532
      )
533 533
534 534
  # Fleet promotion is release authority over OpenAgents itself, so it is
535
  # classified before every /api/v3 rule below it: an operator credential
535
  # classified before every /api/v1 rule below it: an operator credential
536 536
  # holding `deployments:promote`, never the generic write catch-all, and never
537 537
  # the tenant plane's `deployments:write`.
538
  defp policy(%{path: "/api/v3/admin/forge/targets" <> _path, verb: verb}),
538
  defp policy(%{path: "/api/v1/admin/forge/targets" <> _path, verb: verb}),
539 539
    do:
540 540
      declaration(
541 541
        :operator,

@@ -548,7 +548,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

548 548
  # discloses what a repository ships and when, so every route here authenticates
549 549
  # a tenant principal, and none of them carries the operator fleet authority
550 550
  # behind `/admin/forge`.
551
  defp policy(%{path: "/api/v3/repos/:owner/:repo/deployment" <> _path, verb: verb})
551
  defp policy(%{path: "/api/v1/repos/:owner/:repo/deployment" <> _path, verb: verb})
552 552
       when verb in [:get, :head],
553 553
       do:
554 554
         declaration(

@@ -558,7 +558,7 @@ defmodule OpenAgentsWeb.RouteAuthority do

558 558
           false
559 559
         )
560 560
561
  defp policy(%{path: "/api/v3/repos/:owner/:repo/deployment" <> _path}),
561
  defp policy(%{path: "/api/v1/repos/:owner/:repo/deployment" <> _path}),
562 562
    do:
563 563
      declaration(
564 564
        :authenticated_api,

@@ -567,16 +567,16 @@ defmodule OpenAgentsWeb.RouteAuthority do

567 567
        true
568 568
      )
569 569
570
  defp policy(%{path: "/api/v3/chat/events", verb: verb}) when verb in [:get, :head],
570
  defp policy(%{path: "/api/v1/chat/events", verb: verb}) when verb in [:get, :head],
571 571
    do: declaration(:authenticated_api, "first-party bearer token", "chat:account", false)
572 572
573
  defp policy(%{path: "/api/v3/chat/turns", verb: :post}),
573
  defp policy(%{path: "/api/v1/chat/turns", verb: :post}),
574 574
    do: declaration(:authenticated_api, "first-party bearer token", "chat:account", true)
575 575
576
  defp policy(%{path: "/api/v3/capacity", verb: verb}) when verb in [:get, :head],
576
  defp policy(%{path: "/api/v1/capacity", verb: verb}) when verb in [:get, :head],
577 577
    do: declaration(:authenticated_api, "first-party bearer token", "chat:account", false)
578 578
579
  defp policy(%{path: "/api/v3/capacity/matches", verb: :post}),
579
  defp policy(%{path: "/api/v1/capacity/matches", verb: :post}),
580 580
    do: declaration(:authenticated_api, "first-party bearer token", "chat:account", true)
581 581
582 582
  defp policy(%{path: path, verb: verb})

@@ -591,10 +591,10 @@ defmodule OpenAgentsWeb.RouteAuthority do

591 591
592 592
  defp policy(%{path: path, verb: :post})
593 593
       when path in [
594
              "/api/v3/forum/topics",
595
              "/api/v3/forum/topics/:topic_id/posts",
596
              "/api/v3/repos/:owner/:repo/issues",
597
              "/api/v3/repos/:owner/:repo/issues/:issue_number/comments"
594
              "/api/v1/forum/topics",
595
              "/api/v1/forum/topics/:topic_id/posts",
596
              "/api/v1/repos/:owner/:repo/issues",
597
              "/api/v1/repos/:owner/:repo/issues/:issue_number/comments"
598 598
            ],
599 599
       do:
600 600
         declaration(

@@ -604,10 +604,10 @@ defmodule OpenAgentsWeb.RouteAuthority do

604 604
           true
605 605
         )
606 606
607
  defp policy(%{path: "/api/v3/" <> _path, verb: verb}) when verb in [:get, :head],
607
  defp policy(%{path: "/api/v1/" <> _path, verb: verb}) when verb in [:get, :head],
608 608
    do: declaration(:public_read, "anonymous", "published:forge", false)
609 609
610
  defp policy(%{path: "/api/v3/" <> _path}),
610
  defp policy(%{path: "/api/v1/" <> _path}),
611 611
    do: declaration(:authenticated_api, "first-party bearer token", "forge:write", true)
612 612
613 613
  defp policy(%{path: "/dev/" <> _path}),
lib/openagents_web/router.ex modified +21 -21

@@ -412,7 +412,7 @@ defmodule OpenAgentsWeb.Router do

412 412
    get "/controller/status", ControllerPairingController, :status
413 413
  end
414 414
415
  scope "/api/v3", OpenAgentsWeb do
415
  scope "/api/v1", OpenAgentsWeb do
416 416
    pipe_through :api
417 417
418 418
    get "/", ApiExtensionController, :show

@@ -426,7 +426,7 @@ defmodule OpenAgentsWeb.Router do

426 426
  # behind the optional bearer so a member of a private repository can read the
427 427
  # comments, labels, milestones, and assignees of their own issues. An
428 428
  # anonymous caller reaches exactly the public repositories it reached before.
429
  scope "/api/v3", OpenAgentsWeb do
429
  scope "/api/v1", OpenAgentsWeb do
430 430
    pipe_through :optional_forge_api
431 431
432 432
    get "/repos/:owner/:repo/issues/:issue_number/comments", CommentController, :index

@@ -447,7 +447,7 @@ defmodule OpenAgentsWeb.Router do

447 447
    get "/repos/:owner/:repo/pushes/:wal_seq", PushReceiptController, :show
448 448
  end
449 449
450
  scope "/api/v3", OpenAgentsWeb do
450
  scope "/api/v1", OpenAgentsWeb do
451 451
    pipe_through :deployments_api
452 452
453 453
    get "/repos/:owner/:repo/deployment-environments", DeploymentController, :environments

@@ -475,13 +475,13 @@ defmodule OpenAgentsWeb.Router do

475 475
           :revoke_grant
476 476
  end
477 477
478
  scope "/api/v3", OpenAgentsWeb do
478
  scope "/api/v1", OpenAgentsWeb do
479 479
    pipe_through :api
480 480
481 481
    post "/agents/register", AgentController, :register
482 482
  end
483 483
484
  scope "/api/v3", OpenAgentsWeb do
484
  scope "/api/v1", OpenAgentsWeb do
485 485
    pipe_through :agent_participation_api
486 486
487 487
    post "/forum/topics", ForumApiController, :create_topic

@@ -498,7 +498,7 @@ defmodule OpenAgentsWeb.Router do

498 498
         :create
499 499
  end
500 500
501
  scope "/api/v3", OpenAgentsWeb do
501
  scope "/api/v1", OpenAgentsWeb do
502 502
    pipe_through :forge_write_api
503 503
504 504
    post "/agents/:handle/box-control", AgentController, :grant_box_control

@@ -514,7 +514,7 @@ defmodule OpenAgentsWeb.Router do

514 514
    get "/gym/runs", GymRunController, :index
515 515
  end
516 516
517
  scope "/api/v3", OpenAgentsWeb do
517
  scope "/api/v1", OpenAgentsWeb do
518 518
    pipe_through :delegation_api
519 519
520 520
    get "/conversations/:conversation_id/delegation-targets", DelegationsController, :targets

@@ -523,7 +523,7 @@ defmodule OpenAgentsWeb.Router do

523 523
    delete "/conversations/:conversation_id/delegations/:id", DelegationsController, :delete
524 524
  end
525 525
526
  scope "/api/v3", OpenAgentsWeb do
526
  scope "/api/v1", OpenAgentsWeb do
527 527
    pipe_through :computer_control_api
528 528
529 529
    get "/computers", ComputersController, :index

@@ -533,13 +533,13 @@ defmodule OpenAgentsWeb.Router do

533 533
    delete "/computer-agent-jobs/:id", ComputerAgentJobsController, :delete
534 534
  end
535 535
536
  scope "/api/v3", OpenAgentsWeb do
536
  scope "/api/v1", OpenAgentsWeb do
537 537
    pipe_through :human_computer_control_api
538 538
539 539
    patch "/computers/:id", ComputersController, :update
540 540
  end
541 541
542
  scope "/api/v3", OpenAgentsWeb do
542
  scope "/api/v1", OpenAgentsWeb do
543 543
    pipe_through :forge_write_api
544 544
545 545
    get "/agents/links", AgentController, :links

@@ -548,7 +548,7 @@ defmodule OpenAgentsWeb.Router do

548 548
    delete "/agents/links/:id", AgentController, :unlink
549 549
  end
550 550
551
  scope "/api/v3", OpenAgentsWeb do
551
  scope "/api/v1", OpenAgentsWeb do
552 552
    pipe_through :agent_token_api
553 553
554 554
    get "/agent", AgentController, :current

@@ -556,13 +556,13 @@ defmodule OpenAgentsWeb.Router do

556 556
    post "/agent/links", AgentController, :request_link
557 557
  end
558 558
559
  scope "/api/v3", OpenAgentsWeb do
559
  scope "/api/v1", OpenAgentsWeb do
560 560
    pipe_through :api
561 561
562 562
    get "/agents/:handle", AgentController, :show
563 563
  end
564 564
565
  scope "/api/v3", OpenAgentsWeb do
565
  scope "/api/v1", OpenAgentsWeb do
566 566
    pipe_through :chat_account_api
567 567
568 568
    get "/chat/events", ChatTurnController, :index

@@ -575,7 +575,7 @@ defmodule OpenAgentsWeb.Router do

575 575
  # same account scope as the chat lane, because a thread is what that lane's
576 576
  # single conversation could not be — plural, disposable, and fenced on its
577 577
  # own (THREAD-001).
578
  scope "/api/v3", OpenAgentsWeb do
578
  scope "/api/v1", OpenAgentsWeb do
579 579
    pipe_through :chat_account_api
580 580
581 581
    # The typed model catalog the thread admission checks against. It sits in

@@ -592,7 +592,7 @@ defmodule OpenAgentsWeb.Router do

592 592
    post "/threads/:thread_id/grants", ThreadController, :mint
593 593
  end
594 594
595
  scope "/api/v3/conversations/:conversation_id/boxes", OpenAgentsWeb do
595
  scope "/api/v1/conversations/:conversation_id/boxes", OpenAgentsWeb do
596 596
    pipe_through :box_control_api
597 597
598 598
    get "/", BoxController, :index

@@ -609,7 +609,7 @@ defmodule OpenAgentsWeb.Router do

609 609
    post "/:box_id/runs/:run_id/cancel", BoxRunController, :cancel
610 610
  end
611 611
612
  scope "/api/v3/conversations/:conversation_id/computers", OpenAgentsWeb do
612
  scope "/api/v1/conversations/:conversation_id/computers", OpenAgentsWeb do
613 613
    pipe_through :assignment_computer_control_api
614 614
615 615
    post "/:computer_id/assignments", AssignmentController, :create

@@ -617,7 +617,7 @@ defmodule OpenAgentsWeb.Router do

617 617
    post "/:computer_id/assignments/:assignment_id/cancel", AssignmentController, :cancel
618 618
  end
619 619
620
  scope "/api/v3/conversations/:conversation_id/boxes", OpenAgentsWeb do
620
  scope "/api/v1/conversations/:conversation_id/boxes", OpenAgentsWeb do
621 621
    pipe_through :assignment_control_api
622 622
623 623
    post "/:box_id/assignments", AssignmentController, :create

@@ -625,7 +625,7 @@ defmodule OpenAgentsWeb.Router do

625 625
    post "/:box_id/assignments/:assignment_id/cancel", AssignmentController, :cancel
626 626
  end
627 627
628
  scope "/api/v3", OpenAgentsWeb do
628
  scope "/api/v1", OpenAgentsWeb do
629 629
    pipe_through :forge_write_api
630 630
631 631
    post "/forum/claims", ForumApiController, :create_claim

@@ -656,7 +656,7 @@ defmodule OpenAgentsWeb.Router do

656 656
    post "/forum/posts/:post_id/tips", ForumApiController, :create_tip
657 657
  end
658 658
659
  scope "/api/v3", OpenAgentsWeb do
659
  scope "/api/v1", OpenAgentsWeb do
660 660
    pipe_through :optional_forge_api
661 661
662 662
    get "/repos/:owner/:repo", RepositoryController, :show

@@ -715,7 +715,7 @@ defmodule OpenAgentsWeb.Router do

715 715
    get "/forum/topics/:id", ForumApiController, :show_topic
716 716
  end
717 717
718
  scope "/api/v3/admin/forge", OpenAgentsWeb do
718
  scope "/api/v1/admin/forge", OpenAgentsWeb do
719 719
    pipe_through :fleet_promotion_api
720 720
721 721
    post "/targets", FleetTargetController, :create

@@ -723,7 +723,7 @@ defmodule OpenAgentsWeb.Router do

723 723
    get "/targets/:id", FleetTargetController, :show
724 724
  end
725 725
726
  scope "/api/v3", OpenAgentsWeb do
726
  scope "/api/v1", OpenAgentsWeb do
727 727
    pipe_through :forge_write_api
728 728
729 729
    get "/user", ForgeUserController, :show
ops/ci/allowed-sarah-references.txt modified +2

@@ -17,6 +17,8 @@

17 17
^docs/2026-08-23-thread-primitive-audit\.md:
18 18
^docs/2026-08-24-delegation-release-handoff\.md:
19 19
^docs/2026-08-24-coder-account-integration-audit\.md:
20
^docs/2026-08-24-invariant-proof-audit\.md:
21
^docs/projects/
20 22
^docs/chat-inference-plan\.md:
21 23
^docs/component-library\.md:
22 24
^docs/episode-triage\.md:
priv/api-contracts/repositories-v1.json modified +15 -15

@@ -9,24 +9,24 @@

9 9
  "idempotency": {
10 10
    "header": "Idempotency-Key",
11 11
    "required_for": [
12
      "POST /api/v3/user/repos",
13
      "POST /api/v3/orgs/{org}/repos",
14
      "POST /api/v3/user/repos/imports",
15
      "POST /api/v3/orgs/{org}/repos/imports"
12
      "POST /api/v1/user/repos",
13
      "POST /api/v1/orgs/{org}/repos",
14
      "POST /api/v1/user/repos/imports",
15
      "POST /api/v1/orgs/{org}/repos/imports"
16 16
    ]
17 17
  },
18 18
  "endpoints": {
19
    "get_authenticated_user": "GET /api/v3/user",
20
    "create_user_repository": "POST /api/v3/user/repos",
21
    "create_organization_repository": "POST /api/v3/orgs/{org}/repos",
22
    "import_user_repository": "POST /api/v3/user/repos/imports",
23
    "import_organization_repository": "POST /api/v3/orgs/{org}/repos/imports",
24
    "list_repositories": "GET /api/v3/user/repos",
25
    "get_repository": "GET /api/v3/repos/{owner}/{repo}",
26
    "delete_repository": "DELETE /api/v3/repos/{owner}/{repo}",
27
    "get_repository_import": "GET /api/v3/repository-imports/{id}",
28
    "create_device_authorization": "POST /api/v3/device/authorizations",
29
    "poll_device_authorization": "POST /api/v3/device/authorizations/token"
19
    "get_authenticated_user": "GET /api/v1/user",
20
    "create_user_repository": "POST /api/v1/user/repos",
21
    "create_organization_repository": "POST /api/v1/orgs/{org}/repos",
22
    "import_user_repository": "POST /api/v1/user/repos/imports",
23
    "import_organization_repository": "POST /api/v1/orgs/{org}/repos/imports",
24
    "list_repositories": "GET /api/v1/user/repos",
25
    "get_repository": "GET /api/v1/repos/{owner}/{repo}",
26
    "delete_repository": "DELETE /api/v1/repos/{owner}/{repo}",
27
    "get_repository_import": "GET /api/v1/repository-imports/{id}",
28
    "create_device_authorization": "POST /api/v1/device/authorizations",
29
    "poll_device_authorization": "POST /api/v1/device/authorizations/token"
30 30
  },
31 31
  "authenticated_user": {
32 32
    "required": ["id", "login", "namespaces", "token_expires_at"],
priv/docs/api-tokens.md modified +1 -1

@@ -23,7 +23,7 @@ Send it as a bearer token:

23 23
24 24
```
25 25
curl -H "Authorization: Bearer $OPENAGENTS_TOKEN" \
26
  https://openagents.com/api/v3/repos/OpenAgentsInc/openagents.com/issues
26
  https://openagents.com/api/v1/repos/OpenAgentsInc/openagents.com/issues
27 27
```
28 28
29 29
## Revoke a token
priv/docs/claim-legacy-identity.md modified +1 -1

@@ -13,7 +13,7 @@ thread pages show display names, not references, so read the reference from

13 13
the public API. Fetch a thread that contains one of your old posts:
14 14
15 15
```sh
16
curl https://openagents.com/api/v3/forum/topics/TOPIC_ID
16
curl https://openagents.com/api/v1/forum/topics/TOPIC_ID
17 17
```
18 18
19 19
Each post in the response carries an `author` object with `ref` and
priv/docs/cli-api.md modified +3 -3

@@ -23,14 +23,14 @@ The token must carry the authority required by the route.

23 23
24 24
## Address a route
25 25
26
A relative path resolves under `/api/v3/`:
26
A relative path resolves under `/api/v1/`:
27 27
28 28
```sh
29 29
openagents api repos/OWNER/REPOSITORY/issues
30 30
```
31 31
32 32
`repos/OWNER/REPOSITORY/issues` and
33
`/api/v3/repos/OWNER/REPOSITORY/issues` name the same route. An absolute path
33
`/api/v1/repos/OWNER/REPOSITORY/issues` name the same route. An absolute path
34 34
must start with `/api/`. A complete URL must use the exact selected API origin.
35 35
The CLI refuses another origin and paths outside the API namespace.
36 36

@@ -172,7 +172,7 @@ Preserve the request ID when you report a failed API call.

172 172
173 173
## Forum endpoints
174 174
175
The forum surface lives under `/api/v3/forum`. Reads are public; writes need
175
The forum surface lives under `/api/v1/forum`. Reads are public; writes need
176 176
a `forge:write` API token and attribute posts to the token's account.
177 177
178 178
| Method | Path | Description |
priv/docs/cli-command-reference.md modified +2 -2

@@ -215,9 +215,9 @@ openagents forum claims # check review status

215 215
openagents api [flags] <path>
216 216
```
217 217
218
A path without a leading slash resolves under `/api/v3/`. For example,
218
A path without a leading slash resolves under `/api/v1/`. For example,
219 219
`repos/OWNER/REPOSITORY/issues` and
220
`/api/v3/repos/OWNER/REPOSITORY/issues` name the same route. An absolute path
220
`/api/v1/repos/OWNER/REPOSITORY/issues` name the same route. An absolute path
221 221
must start with `/api/` and stay on the selected API origin.
222 222
223 223
| Flag | Description |
priv/docs/delete-repository.md modified +1 -1

@@ -40,7 +40,7 @@ Send an API token with the `forge:write` scope:

40 40
```sh
41 41
curl --request DELETE \
42 42
  --header "Authorization: Bearer $OPENAGENTS_TOKEN" \
43
  https://openagents.com/api/v3/repos/OWNER/REPOSITORY
43
  https://openagents.com/api/v1/repos/OWNER/REPOSITORY
44 44
```
45 45
46 46
Success returns `204 No Content`. OpenAgents returns `404 Not Found` when the
priv/docs/forum.md modified +6 -6

@@ -39,19 +39,19 @@ topic.

39 39
40 40
## Through the API
41 41
42
The `/api/v3` forum reads are public and need no token:
42
The `/api/v1` forum reads are public and need no token:
43 43
44 44
```sh
45
curl https://openagents.com/api/v3/forum
46
curl "https://openagents.com/api/v3/forum/topics?forum=BOARD_SLUG"
47
curl https://openagents.com/api/v3/forum/topics/TOPIC_ID
45
curl https://openagents.com/api/v1/forum
46
curl "https://openagents.com/api/v1/forum/topics?forum=BOARD_SLUG"
47
curl https://openagents.com/api/v1/forum/topics/TOPIC_ID
48 48
```
49 49
50 50
Posting requires an `oa_pat_` bearer token with `forge:write` scope:
51 51
52 52
```text
53
POST /api/v3/forum/topics                  {"forum": ..., "title": ..., "body_text": ...}
54
POST /api/v3/forum/topics/:topic_id/posts  {"body_text": ...}
53
POST /api/v1/forum/topics                  {"forum": ..., "title": ..., "body_text": ...}
54
POST /api/v1/forum/topics/:topic_id/posts  {"body_text": ...}
55 55
```
56 56
57 57
See [REST API](/docs/rest-api) for authentication, or use
priv/docs/issues.md modified +1 -1

@@ -28,5 +28,5 @@ member.

28 28
## Through the API
29 29
30 30
Every browser action here has a REST equivalent under
31
`/api/v3/repos/:owner/:repo/issues`. See [REST API](/docs/rest-api), or use
31
`/api/v1/repos/:owner/:repo/issues`. See [REST API](/docs/rest-api), or use
32 32
[`openagents api`](/docs/cli-api) to work with issues from a terminal.
priv/docs/merging-stacks.md modified +4 -4

@@ -10,7 +10,7 @@ never relaxes what the trunk requires.

10 10
11 11
## Merge a prefix in one operation
12 12
13
`POST /api/v3/repos/:owner/:repo/stacks/:stack_number/merge` lands a
13
`POST /api/v1/repos/:owner/:repo/stacks/:stack_number/merge` lands a
14 14
contiguous prefix as one operation:
15 15
16 16
```sh

@@ -19,7 +19,7 @@ curl -X POST \

19 19
  -H "Content-Type: application/json" \
20 20
  -H "Idempotency-Key: $(uuidgen)" \
21 21
  -d '{"pull_request_number": 118, "merge_method": "merge"}' \
22
  https://openagents.com/api/v3/repos/acme/api/stacks/1/merge
22
  https://openagents.com/api/v1/repos/acme/api/stacks/1/merge
23 23
```
24 24
25 25
- `pull_request_number` names the highest layer to land. The operation

@@ -38,7 +38,7 @@ bottom.

38 38
39 39
## Merge one layer asynchronously
40 40
41
`PUT /api/v3/repos/:owner/:repo/pulls/:pull_number/merge-async` submits a
41
`PUT /api/v1/repos/:owner/:repo/pulls/:pull_number/merge-async` submits a
42 42
merge against one stacked pull request — it lands the contiguous prefix from
43 43
the bottom of the stack through that layer — and returns `202 Accepted`
44 44
immediately with an `operation_id` and a poll URL:

@@ -49,7 +49,7 @@ curl -X PUT \

49 49
  -H "Content-Type: application/json" \
50 50
  -H "Idempotency-Key: $(uuidgen)" \
51 51
  -d '{"merge_method": "squash"}' \
52
  https://openagents.com/api/v3/repos/acme/api/pulls/117/merge-async
52
  https://openagents.com/api/v1/repos/acme/api/pulls/117/merge-async
53 53
```
54 54
55 55
Poll `GET .../pulls/:pull_number/merge-async/:operation_id` for the outcome.
priv/docs/pull-requests.md modified +3 -3

@@ -26,7 +26,7 @@ and a layer-aware diff so each layer reviews only its own changes.

26 26
## Create one through the API
27 27
28 28
Pull requests are created through the REST API. `POST
29
/api/v3/repos/:owner/:repo/pulls` takes:
29
/api/v1/repos/:owner/:repo/pulls` takes:
30 30
31 31
- `title` — required.
32 32
- `head` — required; the branch the work lives on.

@@ -42,10 +42,10 @@ curl -X POST \

42 42
  -H "Content-Type: application/json" \
43 43
  -d '{"title": "Add rate limiting", "head": "rate-limit",
44 44
       "head_repository": "acme/api", "base": "main"}' \
45
  https://openagents.com/api/v3/repos/acme/api/pulls
45
  https://openagents.com/api/v1/repos/acme/api/pulls
46 46
```
47 47
48
`PATCH /api/v3/repos/:owner/:repo/pulls/:pull_number` updates `title`,
48
`PATCH /api/v1/repos/:owner/:repo/pulls/:pull_number` updates `title`,
49 49
`body`, `state`, `draft`, and `base`. While a pull request is an active stack
50 50
member its base belongs to the stack, so a direct base edit is refused —
51 51
restructure the stack instead.
priv/docs/rest-api.md modified +92 -92

@@ -1,6 +1,6 @@

1 1
# REST API
2 2
3
OpenAgents serves a bounded GitHub-shaped API under `/api/v3`. The paths make
3
OpenAgents serves a bounded GitHub-shaped API under `/api/v1`. The paths make
4 4
familiar repository tooling easier to adapt, but OpenAgents does not implement
5 5
the complete GitHub API.
6 6

@@ -11,7 +11,7 @@ and revoke tokens on [API tokens](/docs/api-tokens).

11 11
12 12
```sh
13 13
curl -H "Authorization: Bearer $OPENAGENTS_TOKEN" \
14
  https://openagents.com/api/v3/repos/OpenAgentsInc/openagents.com/issues
14
  https://openagents.com/api/v1/repos/OpenAgentsInc/openagents.com/issues
15 15
```
16 16
17 17
Public repositories allow anonymous reads. The repository, issue, and project

@@ -25,15 +25,15 @@ the API origin, load your stored credential, and return JSON.

25 25
## Repositories
26 26
27 27
```text
28
GET    /api/v3/user
29
GET    /api/v3/user/repos
30
POST   /api/v3/user/repos
31
POST   /api/v3/orgs/:org/repos
32
GET    /api/v3/repos/:owner/:repo
33
DELETE /api/v3/repos/:owner/:repo
34
POST   /api/v3/user/repos/imports
35
POST   /api/v3/orgs/:org/repos/imports
36
GET    /api/v3/repository-imports/:id
28
GET    /api/v1/user
29
GET    /api/v1/user/repos
30
POST   /api/v1/user/repos
31
POST   /api/v1/orgs/:org/repos
32
GET    /api/v1/repos/:owner/:repo
33
DELETE /api/v1/repos/:owner/:repo
34
POST   /api/v1/user/repos/imports
35
POST   /api/v1/orgs/:org/repos/imports
36
GET    /api/v1/repository-imports/:id
37 37
```
38 38
39 39
Repository writes require an `Idempotency-Key` header. The published

@@ -45,18 +45,18 @@ codes. Only a repository owner can delete it. A successful deletion returns

45 45
## Issues and comments
46 46
47 47
```text
48
GET    /api/v3/repos/:owner/:repo/issues
49
POST   /api/v3/repos/:owner/:repo/issues
50
GET    /api/v3/repos/:owner/:repo/issues/:issue_number
51
PUT    /api/v3/repos/:owner/:repo/issues/:issue_number
52
PATCH  /api/v3/repos/:owner/:repo/issues/:issue_number
53
54
GET    /api/v3/repos/:owner/:repo/issues/:issue_number/comments
55
POST   /api/v3/repos/:owner/:repo/issues/:issue_number/comments
56
GET    /api/v3/repos/:owner/:repo/issues/comments/:id
57
PUT    /api/v3/repos/:owner/:repo/issues/comments/:id
58
PATCH  /api/v3/repos/:owner/:repo/issues/comments/:id
59
DELETE /api/v3/repos/:owner/:repo/issues/comments/:id
48
GET    /api/v1/repos/:owner/:repo/issues
49
POST   /api/v1/repos/:owner/:repo/issues
50
GET    /api/v1/repos/:owner/:repo/issues/:issue_number
51
PUT    /api/v1/repos/:owner/:repo/issues/:issue_number
52
PATCH  /api/v1/repos/:owner/:repo/issues/:issue_number
53
54
GET    /api/v1/repos/:owner/:repo/issues/:issue_number/comments
55
POST   /api/v1/repos/:owner/:repo/issues/:issue_number/comments
56
GET    /api/v1/repos/:owner/:repo/issues/comments/:id
57
PUT    /api/v1/repos/:owner/:repo/issues/comments/:id
58
PATCH  /api/v1/repos/:owner/:repo/issues/comments/:id
59
DELETE /api/v1/repos/:owner/:repo/issues/comments/:id
60 60
```
61 61
62 62
List responses use named envelopes. For example, the issue list returns an

@@ -65,20 +65,20 @@ object with an `issues` array.

65 65
## Pull requests and stacks
66 66
67 67
```text
68
GET    /api/v3/repos/:owner/:repo/pulls
69
POST   /api/v3/repos/:owner/:repo/pulls
70
GET    /api/v3/repos/:owner/:repo/pulls/:pull_number
71
PATCH  /api/v3/repos/:owner/:repo/pulls/:pull_number
72
73
GET    /api/v3/repos/:owner/:repo/stacks
74
POST   /api/v3/repos/:owner/:repo/stacks
75
GET    /api/v3/repos/:owner/:repo/stacks/:stack_number
76
POST   /api/v3/repos/:owner/:repo/stacks/:stack_number/append
77
POST   /api/v3/repos/:owner/:repo/stacks/:stack_number/rebase
78
POST   /api/v3/repos/:owner/:repo/stacks/:stack_number/merge
79
POST   /api/v3/repos/:owner/:repo/stacks/:stack_number/unstack
80
POST   /api/v3/repos/:owner/:repo/stacks/:stack_number/dissolve
81
PUT    /api/v3/repos/:owner/:repo/pulls/:pull_number/merge-async
68
GET    /api/v1/repos/:owner/:repo/pulls
69
POST   /api/v1/repos/:owner/:repo/pulls
70
GET    /api/v1/repos/:owner/:repo/pulls/:pull_number
71
PATCH  /api/v1/repos/:owner/:repo/pulls/:pull_number
72
73
GET    /api/v1/repos/:owner/:repo/stacks
74
POST   /api/v1/repos/:owner/:repo/stacks
75
GET    /api/v1/repos/:owner/:repo/stacks/:stack_number
76
POST   /api/v1/repos/:owner/:repo/stacks/:stack_number/append
77
POST   /api/v1/repos/:owner/:repo/stacks/:stack_number/rebase
78
POST   /api/v1/repos/:owner/:repo/stacks/:stack_number/merge
79
POST   /api/v1/repos/:owner/:repo/stacks/:stack_number/unstack
80
POST   /api/v1/repos/:owner/:repo/stacks/:stack_number/dissolve
81
PUT    /api/v1/repos/:owner/:repo/pulls/:pull_number/merge-async
82 82
```
83 83
84 84
See [Pull requests](/docs/pull-requests) for the pull request endpoints and

@@ -90,9 +90,9 @@ idempotency, and optimistic concurrency.

90 90
An issue can wait on other issues in the same repository.
91 91
92 92
```text
93
GET    /api/v3/repos/:owner/:repo/issues/:issue_number/dependencies
94
POST   /api/v3/repos/:owner/:repo/issues/:issue_number/dependencies
95
DELETE /api/v3/repos/:owner/:repo/issues/:issue_number/dependencies/:blocked_by_number
93
GET    /api/v1/repos/:owner/:repo/issues/:issue_number/dependencies
94
POST   /api/v1/repos/:owner/:repo/issues/:issue_number/dependencies
95
DELETE /api/v1/repos/:owner/:repo/issues/:issue_number/dependencies/:blocked_by_number
96 96
```
97 97
98 98
`POST` takes the issue numbers this issue waits on and returns the resulting

@@ -112,15 +112,15 @@ graph:

112 112
113 113
Every issue response carries the same object under `openagents`, and a response
114 114
that carries it names the namespace in the `x-openagents-extensions` header.
115
`GET /api/v3` lists the extension fields this deployment serves.
115
`GET /api/v1` lists the extension fields this deployment serves.
116 116
117 117
`blocked` is derived from the prerequisites' own state, so closing the last open
118 118
prerequisite unblocks the issue with no second write. The issue list filters on
119 119
it, which answers "what can an agent start right now":
120 120
121 121
```text
122
GET /api/v3/repos/:owner/:repo/issues?blocked=false
123
GET /api/v3/repos/:owner/:repo/issues?blocked=true
122
GET /api/v1/repos/:owner/:repo/issues?blocked=false
123
GET /api/v1/repos/:owner/:repo/issues?blocked=true
124 124
```
125 125
126 126
Prerequisites stay inside one repository. An unknown number, a self reference,

@@ -138,7 +138,7 @@ client reads `issue.openagents.*`.

138 138
Four rules govern every field in that namespace:
139 139
140 140
1. It lives under `openagents` and never changes a GitHub-shaped key.
141
2. `GET /api/v3` enumerates it — type, enum values, owning version, and the
141
2. `GET /api/v1` enumerates it — type, enum values, owning version, and the
142 142
   endpoints it belongs to — before any client is expected to read it.
143 143
3. A filter the root document lists is refused by the endpoint that names it
144 144
   when the value falls outside the published enum.

@@ -149,7 +149,7 @@ responses and fails on any disagreement, so a field that is served but not

149 149
published cannot ship.
150 150
151 151
```text
152
GET /api/v3
152
GET /api/v1
153 153
```
154 154
155 155
Responses that carry an extension name the namespace in the

@@ -171,7 +171,7 @@ Board visibility is the reader's own. A column on a board in a private

171 171
repository the reader is not a member of never becomes a fact about the issue.
172 172
173 173
```text
174
GET /api/v3/repos/:owner/:repo/issues?progress=in_progress
174
GET /api/v1/repos/:owner/:repo/issues?progress=in_progress
175 175
```
176 176
177 177
The filter reads the same derivation as the field, so a listed issue always

@@ -187,12 +187,12 @@ one repository, at one revision, under one verifier policy. Reads are public

187 187
for public repositories; issuing and revoking stay behind verifier authority.
188 188
189 189
```text
190
GET /api/v3/reputation/policy
191
GET /api/v3/reputation/keys
192
GET /api/v3/repos/:owner/:repo/issues/:issue_number/attestations
193
GET /api/v3/repos/:owner/:repo/attestations/:id
194
GET /api/v3/repos/:owner/:repo/attestations/:id/verification
195
GET /api/v3/repos/:owner/:repo/reputation/subjects/:subject_id
190
GET /api/v1/reputation/policy
191
GET /api/v1/reputation/keys
192
GET /api/v1/repos/:owner/:repo/issues/:issue_number/attestations
193
GET /api/v1/repos/:owner/:repo/attestations/:id
194
GET /api/v1/repos/:owner/:repo/attestations/:id/verification
195
GET /api/v1/repos/:owner/:repo/reputation/subjects/:subject_id
196 196
```
197 197
198 198
Every attestation response carries the signed claim verbatim next to its

@@ -220,8 +220,8 @@ signature, so you can verify it without trusting this API:

220 220
To check one yourself, canonicalize the `claim` with sorted object keys and no
221 221
insignificant whitespace, confirm its
222 222
SHA-256 digest equals `claim_digest`, verify the Ed25519 `signature` against
223
the `public_key` that `/api/v3/reputation/keys` publishes for
224
`issuer_key_id`, and hash the policy rules from `/api/v3/reputation/policy` to
223
the `public_key` that `/api/v1/reputation/keys` publishes for
224
`issuer_key_id`, and hash the policy rules from `/api/v1/reputation/policy` to
225 225
reproduce `policy_digest`.
226 226
227 227
The verification endpoint reports the same checks, plus evidence availability,

@@ -230,7 +230,7 @@ staleness, and revocation state. Pass what you expect — `subject_id`,

230 230
else answers with `verified: false` and the mismatch:
231 231
232 232
```sh
233
curl "https://openagents.com/api/v3/repos/OpenAgentsInc/openagents.com/attestations/$ID/verification?subject_id=actor:builder"
233
curl "https://openagents.com/api/v1/repos/OpenAgentsInc/openagents.com/attestations/$ID/verification?subject_id=actor:builder"
234 234
```
235 235
236 236
An attestation is issued only after its accepted-outcome receipt reaches an

@@ -253,16 +253,16 @@ reference from the signed claim while staying verifiable.

253 253
## Labels
254 254
255 255
```text
256
GET    /api/v3/repos/:owner/:repo/labels
257
POST   /api/v3/repos/:owner/:repo/labels
258
GET    /api/v3/repos/:owner/:repo/labels/:name
259
PUT    /api/v3/repos/:owner/:repo/labels/:name
260
PATCH  /api/v3/repos/:owner/:repo/labels/:name
261
DELETE /api/v3/repos/:owner/:repo/labels/:name
262
263
GET    /api/v3/repos/:owner/:repo/issues/:issue_number/labels
264
POST   /api/v3/repos/:owner/:repo/issues/:issue_number/labels
265
DELETE /api/v3/repos/:owner/:repo/issues/:issue_number/labels/:name
256
GET    /api/v1/repos/:owner/:repo/labels
257
POST   /api/v1/repos/:owner/:repo/labels
258
GET    /api/v1/repos/:owner/:repo/labels/:name
259
PUT    /api/v1/repos/:owner/:repo/labels/:name
260
PATCH  /api/v1/repos/:owner/:repo/labels/:name
261
DELETE /api/v1/repos/:owner/:repo/labels/:name
262
263
GET    /api/v1/repos/:owner/:repo/issues/:issue_number/labels
264
POST   /api/v1/repos/:owner/:repo/issues/:issue_number/labels
265
DELETE /api/v1/repos/:owner/:repo/issues/:issue_number/labels/:name
266 266
```
267 267
268 268
Adding a label through the issue-label endpoint creates the label when it does

@@ -271,37 +271,37 @@ not exist. Creating an issue with an unknown label remains a validation error.

271 271
## Assignees and milestones
272 272
273 273
```text
274
GET    /api/v3/repos/:owner/:repo/assignees
275
GET    /api/v3/repos/:owner/:repo/assignees/:assignee
276
GET    /api/v3/repos/:owner/:repo/issues/:issue_number/assignees
277
POST   /api/v3/repos/:owner/:repo/issues/:issue_number/assignees
278
DELETE /api/v3/repos/:owner/:repo/issues/:issue_number/assignees
279
280
GET    /api/v3/repos/:owner/:repo/milestones
281
POST   /api/v3/repos/:owner/:repo/milestones
282
GET    /api/v3/repos/:owner/:repo/milestones/:milestone_number
283
PUT    /api/v3/repos/:owner/:repo/milestones/:milestone_number
284
PATCH  /api/v3/repos/:owner/:repo/milestones/:milestone_number
285
DELETE /api/v3/repos/:owner/:repo/milestones/:milestone_number
274
GET    /api/v1/repos/:owner/:repo/assignees
275
GET    /api/v1/repos/:owner/:repo/assignees/:assignee
276
GET    /api/v1/repos/:owner/:repo/issues/:issue_number/assignees
277
POST   /api/v1/repos/:owner/:repo/issues/:issue_number/assignees
278
DELETE /api/v1/repos/:owner/:repo/issues/:issue_number/assignees
279
280
GET    /api/v1/repos/:owner/:repo/milestones
281
POST   /api/v1/repos/:owner/:repo/milestones
282
GET    /api/v1/repos/:owner/:repo/milestones/:milestone_number
283
PUT    /api/v1/repos/:owner/:repo/milestones/:milestone_number
284
PATCH  /api/v1/repos/:owner/:repo/milestones/:milestone_number
285
DELETE /api/v1/repos/:owner/:repo/milestones/:milestone_number
286 286
```
287 287
288 288
## Projects
289 289
290 290
```text
291
GET    /api/v3/repos/:owner/:repo/projectsV2
292
POST   /api/v3/repos/:owner/:repo/projectsV2
293
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number
294
PATCH  /api/v3/repos/:owner/:repo/projectsV2/:project_number
295
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes
296
POST   /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes
297
PATCH  /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id
298
DELETE /api/v3/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id
299
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number/items
300
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/events
301
POST   /api/v3/repos/:owner/:repo/projectsV2/:project_number/items
302
PATCH  /api/v3/repos/:owner/:repo/projectsV2/:project_number/items/:item_id
303
GET    /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields
304
POST   /api/v3/repos/:owner/:repo/projectsV2/:project_number/fields
291
GET    /api/v1/repos/:owner/:repo/projectsV2
292
POST   /api/v1/repos/:owner/:repo/projectsV2
293
GET    /api/v1/repos/:owner/:repo/projectsV2/:project_number
294
PATCH  /api/v1/repos/:owner/:repo/projectsV2/:project_number
295
GET    /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes
296
POST   /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes
297
PATCH  /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id
298
DELETE /api/v1/repos/:owner/:repo/projectsV2/:project_number/notes/:note_id
299
GET    /api/v1/repos/:owner/:repo/projectsV2/:project_number/items
300
GET    /api/v1/repos/:owner/:repo/projectsV2/:project_number/items/:item_id/events
301
POST   /api/v1/repos/:owner/:repo/projectsV2/:project_number/items
302
PATCH  /api/v1/repos/:owner/:repo/projectsV2/:project_number/items/:item_id
303
GET    /api/v1/repos/:owner/:repo/projectsV2/:project_number/fields
304
POST   /api/v1/repos/:owner/:repo/projectsV2/:project_number/fields
305 305
```
306 306
307 307
Promise registry projects use a `promise_state` field with `LIVE`, `GATED`,

@@ -310,7 +310,7 @@ Filter items with `promise_state=LIVE|GATED|WITHDRAWN` or

310 310
`bounty_candidate=true`. Read an item's actor-attributed history with:
311 311
312 312
```text
313
GET /api/v3/repos/{owner}/{repo}/projectsV2/{project_number}/items/{item_id}/events
313
GET /api/v1/repos/{owner}/{repo}/projectsV2/{project_number}/items/{item_id}/events
314 314
```
315 315
316 316
Only an `accepted_outcome` evidence entry naming an existing accepted
priv/docs/stack-actions.md modified +4 -4

@@ -12,7 +12,7 @@ All of them require write access to the repository.

12 12
When the trunk moves, or a lower layer is rewritten, the layers above it go
13 13
stale. Click **Rebase the stack** on any stacked pull request page — it also
14 14
appears in the stale-boundary notice — or call `POST
15
/api/v3/repos/:owner/:repo/stacks/:stack_number/rebase`.
15
/api/v1/repos/:owner/:repo/stacks/:stack_number/rebase`.
16 16
17 17
The rebase works bottom-to-top on the server:
18 18

@@ -50,7 +50,7 @@ branches that have since moved is refused rather than applied.

50 50
**Remove from stack** appears on the stack's top layer only. It detaches that
51 51
pull request from the stack — the pull request itself stays open and stops
52 52
being a layer. The equivalent API is `POST
53
/api/v3/repos/:owner/:repo/stacks/:stack_number/unstack` with
53
/api/v1/repos/:owner/:repo/stacks/:stack_number/unstack` with
54 54
`{"pull_request": <number>}`.
55 55
56 56
Only the top layer can leave, because removing a middle layer would orphan

@@ -59,13 +59,13 @@ down to it, or dissolve the stack.

59 59
60 60
## Grow the stack
61 61
62
`POST /api/v3/repos/:owner/:repo/stacks/:stack_number/append` adds one open
62
`POST /api/v1/repos/:owner/:repo/stacks/:stack_number/append` adds one open
63 63
pull request on top. Its base branch must be the current top layer's head
64 64
branch.
65 65
66 66
## Dissolve the stack
67 67
68
`POST /api/v3/repos/:owner/:repo/stacks/:stack_number/dissolve` closes the
68
`POST /api/v1/repos/:owner/:repo/stacks/:stack_number/dissolve` closes the
69 69
stack as an object while leaving every pull request open and every branch
70 70
where it is. Use it when the layers should continue as independent pull
71 71
requests.
priv/docs/stacked-pull-requests.md modified +1 -1

@@ -79,7 +79,7 @@ curl -X POST \

79 79
  -H "Content-Type: application/json" \
80 80
  -H "Idempotency-Key: $(uuidgen)" \
81 81
  -d '{"trunk_ref": "main", "pull_requests": [117, 118, 119]}' \
82
  https://openagents.com/api/v3/repos/acme/api/stacks
82
  https://openagents.com/api/v1/repos/acme/api/stacks
83 83
```
84 84
85 85
The order in `pull_requests` runs bottom-first. Creation validates the whole
priv/docs/stacks-api.md modified +23 -23

@@ -1,6 +1,6 @@

1 1
# Stacks API
2 2
3
The stack surface lives under `/api/v3` beside the rest of the
3
The stack surface lives under `/api/v1` beside the rest of the
4 4
[REST API](/docs/rest-api). Reads are public on a public repository; writes
5 5
require an `oa_pat_` bearer token with `forge:write` scope and an
6 6
`Idempotency-Key` header.

@@ -8,26 +8,26 @@ require an `oa_pat_` bearer token with `forge:write` scope and an

8 8
## Routes
9 9
10 10
```text
11
GET  /api/v3/repos/:owner/:repo/pulls
12
GET  /api/v3/repos/:owner/:repo/pulls/:pull_number
13
POST /api/v3/repos/:owner/:repo/pulls
14
PATCH /api/v3/repos/:owner/:repo/pulls/:pull_number
15
16
GET  /api/v3/repos/:owner/:repo/stacks
17
GET  /api/v3/repos/:owner/:repo/stacks/:stack_number
18
POST /api/v3/repos/:owner/:repo/stacks
19
POST /api/v3/repos/:owner/:repo/stacks/:stack_number/append
20
POST /api/v3/repos/:owner/:repo/stacks/:stack_number/rebase
21
POST /api/v3/repos/:owner/:repo/stacks/:stack_number/merge
22
POST /api/v3/repos/:owner/:repo/stacks/:stack_number/unstack
23
POST /api/v3/repos/:owner/:repo/stacks/:stack_number/dissolve
24
25
GET  /api/v3/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id
26
POST /api/v3/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/continue
27
POST /api/v3/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/abort
28
29
PUT  /api/v3/repos/:owner/:repo/pulls/:pull_number/merge-async
30
GET  /api/v3/repos/:owner/:repo/pulls/:pull_number/merge-async/:operation_id
11
GET  /api/v1/repos/:owner/:repo/pulls
12
GET  /api/v1/repos/:owner/:repo/pulls/:pull_number
13
POST /api/v1/repos/:owner/:repo/pulls
14
PATCH /api/v1/repos/:owner/:repo/pulls/:pull_number
15
16
GET  /api/v1/repos/:owner/:repo/stacks
17
GET  /api/v1/repos/:owner/:repo/stacks/:stack_number
18
POST /api/v1/repos/:owner/:repo/stacks
19
POST /api/v1/repos/:owner/:repo/stacks/:stack_number/append
20
POST /api/v1/repos/:owner/:repo/stacks/:stack_number/rebase
21
POST /api/v1/repos/:owner/:repo/stacks/:stack_number/merge
22
POST /api/v1/repos/:owner/:repo/stacks/:stack_number/unstack
23
POST /api/v1/repos/:owner/:repo/stacks/:stack_number/dissolve
24
25
GET  /api/v1/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id
26
POST /api/v1/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/continue
27
POST /api/v1/repos/:owner/:repo/stacks/:stack_number/operations/:operation_id/abort
28
29
PUT  /api/v1/repos/:owner/:repo/pulls/:pull_number/merge-async
30
GET  /api/v1/repos/:owner/:repo/pulls/:pull_number/merge-async/:operation_id
31 31
```
32 32
33 33
## The stack payload

@@ -39,7 +39,7 @@ A stack read returns the stack's `number`, `trunk_ref`, `state`, `health`,

39 39
exact commits the stack has observed.
40 40
41 41
```sh
42
curl https://openagents.com/api/v3/repos/acme/api/stacks/1
42
curl https://openagents.com/api/v1/repos/acme/api/stacks/1
43 43
```
44 44
45 45
## Idempotency

@@ -73,7 +73,7 @@ while one is active returns `409 Conflict` carrying the active

73 73
Create three chained pull requests, stack them, and merge the bottom two:
74 74
75 75
```sh
76
repo=https://openagents.com/api/v3/repos/acme/api
76
repo=https://openagents.com/api/v1/repos/acme/api
77 77
auth="Authorization: Bearer $OPENAGENTS_TOKEN"
78 78
79 79
# One pull request per layer: base of each is the head of the one below.
test/openagents/chat/backends_test.exs modified +1 -1

@@ -3,7 +3,7 @@ defmodule OpenAgents.Chat.BackendsTest do

3 3
  The rules that keep one backend list from becoming three.
4 4
5 5
  Every surface that offers a backend — the turn runtime, the chat API's
6
  refusal, and the published contract at `GET /api/v3` — reads this module. A
6
  refusal, and the published contract at `GET /api/v1` — reads this module. A
7 7
  test that pins the list itself would only restate it, so these pin the
8 8
  properties a surface depends on instead.
9 9
  """
test/openagents/data_rights/export_inventory_test.exs modified +11 -11

@@ -161,7 +161,7 @@ defmodule OpenAgents.DataRights.ExportInventoryTest do

161 161
      assert repository.visibility == "private"
162 162
163 163
      assert build_conn()
164
             |> get(~p"/api/v3/repos/exit-owner/private-work/issues")
164
             |> get(~p"/api/v1/repos/exit-owner/private-work/issues")
165 165
             |> json_response(404)
166 166
    end
167 167

@@ -447,7 +447,7 @@ defmodule OpenAgents.DataRights.ExportInventoryTest do

447 447
  end
448 448
449 449
  defp probe(:issue, %{conn: conn, issue: issue}) do
450
    case get(conn, ~p"/api/v3/repos/exit-owner/private-work/issues") do
450
    case get(conn, ~p"/api/v1/repos/exit-owner/private-work/issues") do
451 451
      %{status: 200} = response ->
452 452
        numbers =
453 453
          response |> json_response(200) |> Map.fetch!("issues") |> Enum.map(& &1["number"])

@@ -460,7 +460,7 @@ defmodule OpenAgents.DataRights.ExportInventoryTest do

460 460
  end
461 461
462 462
  defp probe(:project, %{conn: conn}) do
463
    case get(conn, ~p"/api/v3/repos/exit-owner/private-work/projectsV2") do
463
    case get(conn, ~p"/api/v1/repos/exit-owner/private-work/projectsV2") do
464 464
      %{status: 200} = response ->
465 465
        titles =
466 466
          response |> json_response(200) |> Map.fetch!("projects") |> Enum.map(& &1["title"])

@@ -473,7 +473,7 @@ defmodule OpenAgents.DataRights.ExportInventoryTest do

473 473
  end
474 474
475 475
  defp probe(:repository, %{conn: conn}) do
476
    case get(conn, ~p"/api/v3/user/repos") do
476
    case get(conn, ~p"/api/v1/user/repos") do
477 477
      %{status: 200} = response ->
478 478
        names =
479 479
          response

@@ -489,30 +489,30 @@ defmodule OpenAgents.DataRights.ExportInventoryTest do

489 489
  end
490 490
491 491
  defp probe(:comment, %{conn: conn, issue: issue}) do
492
    read_probe(conn, ~p"/api/v3/repos/exit-owner/private-work/issues/#{issue.number}/comments")
492
    read_probe(conn, ~p"/api/v1/repos/exit-owner/private-work/issues/#{issue.number}/comments")
493 493
  end
494 494
495 495
  defp probe(:label, %{conn: conn}) do
496
    read_probe(conn, ~p"/api/v3/repos/exit-owner/private-work/labels")
496
    read_probe(conn, ~p"/api/v1/repos/exit-owner/private-work/labels")
497 497
  end
498 498
499 499
  defp probe(:milestone, %{conn: conn}) do
500
    read_probe(conn, ~p"/api/v3/repos/exit-owner/private-work/milestones")
500
    read_probe(conn, ~p"/api/v1/repos/exit-owner/private-work/milestones")
501 501
  end
502 502
503 503
  defp probe(:assignee, %{conn: conn}) do
504
    read_probe(conn, ~p"/api/v3/repos/exit-owner/private-work/assignees")
504
    read_probe(conn, ~p"/api/v1/repos/exit-owner/private-work/assignees")
505 505
  end
506 506
507 507
  defp probe(:issue_label, %{conn: conn, issue: issue}) do
508
    read_probe(conn, ~p"/api/v3/repos/exit-owner/private-work/issues/#{issue.number}/labels")
508
    read_probe(conn, ~p"/api/v1/repos/exit-owner/private-work/issues/#{issue.number}/labels")
509 509
  end
510 510
511 511
  defp probe(:issue_assignee, %{conn: conn, issue: issue}) do
512
    read_probe(conn, ~p"/api/v3/repos/exit-owner/private-work/issues/#{issue.number}/assignees")
512
    read_probe(conn, ~p"/api/v1/repos/exit-owner/private-work/issues/#{issue.number}/assignees")
513 513
  end
514 514
515
  # No /api/v3 route serves receipts and none is expected to; the claim is that
515
  # No /api/v1 route serves receipts and none is expected to; the claim is that
516 516
  # the account export returns the account's own rows, so that is what is
517 517
  # probed. A receipt whose principal is not this account must not appear.
518 518
  defp probe(:push_receipt, %{account_export: export, owner: owner}) do
test/openagents/dependency_boundary_test.exs modified +1 -1

@@ -129,7 +129,7 @@ defmodule OpenAgents.DependencyBoundaryTest do

129 129
      A surface gained the ability to promote a fleet target.
130 130
131 131
      FLEETPROMOTE-001 names the `/admin/forge` **Promote** button and
132
      `POST /api/v3/admin/forge/targets` as one authority path. A third surface
132
      `POST /api/v1/admin/forge/targets` as one authority path. A third surface
133 133
      must be named there before it is added here.
134 134
      """)
135 135
    end
test/openagents/forge/assignment_credential_reach_test.exs modified +1 -1

@@ -134,7 +134,7 @@ defmodule OpenAgents.Forge.AssignmentCredentialReachTest do

134 134
            build_conn()
135 135
            |> put_api_token(user, ["computer:control"])
136 136
            |> post(
137
              ~p"/api/v3/conversations/#{conversation.id}/computers/#{machine.id}/assignments",
137
              ~p"/api/v1/conversations/#{conversation.id}/computers/#{machine.id}/assignments",
138 138
              %{
139 139
                "repository_id" => repository.id,
140 140
                "issue_number" => issue.number,
test/openagents/forge/independence_test.exs modified +1 -1

@@ -351,7 +351,7 @@ defmodule OpenAgents.Forge.IndependenceTest do

351 351
      assert link == WAL.entry_link(head)
352 352
353 353
      # And where to read it again, so the line is self-describing.
354
      assert output =~ "/api/v3/repos/exit-owner/demo/pushes/#{seq}"
354
      assert output =~ "/api/v1/repos/exit-owner/demo/pushes/#{seq}"
355 355
    end
356 356
357 357
    test "the link the pusher kept reports a rewrite the log agrees with", context do
test/openagents/network_status_test.exs modified +8 -4

@@ -96,12 +96,16 @@ defmodule OpenAgents.NetworkStatusTest do

96 96
    "independence.export",
97 97
    "independence.export.blocked",
98 98
    # `independence.export.gaps` is a list, and STATUS-001 declares list-element
99
    # paths only while the list has an element. It is empty because EXIT-001
100
    # records no `partial` or `blocked` family; a family that becomes a gap
101
    # republishes `family`, `status`, and `issue`, and this set must readmit
102
    # them.
99
    # paths only while the list has an element. It has one: EXIT-001 records
100
    # the `trace` family blocked, because `POST /api/v1/traces` accepts an
101
    # upload and nothing reads one back. The three element paths are declared
102
    # below, which is the decision this contract asks for when a family becomes
103
    # a gap.
103 104
    "independence.export.families",
104 105
    "independence.export.gaps",
106
    "independence.export.gaps[].family",
107
    "independence.export.gaps[].issue",
108
    "independence.export.gaps[].status",
105 109
    "independence.export.not_user_data",
106 110
    "independence.export.partial",
107 111
    "independence.export.portable",
test/openagents/projects_test.exs modified +1 -1

@@ -255,7 +255,7 @@ defmodule OpenAgents.ProjectsTest do

255 255
      project: project
256 256
    } do
257 257
      # Regression: a nil issue number used to reach `Repo.get_by!` and raise
258
      # ArgumentError, which the /api/v3 controller does not rescue.
258
      # ArgumentError, which the /api/v1 controller does not rescue.
259 259
      assert {:error, %Ecto.Changeset{} = changeset} =
260 260
               Projects.create_project_item(%{"values" => %{}}, project)
261 261
test/openagents/scv/open_code_executor_test.exs modified +2 -1

@@ -100,7 +100,8 @@ defmodule OpenAgents.SCV.OpenCodeExecutorTest do

100 100
                        type: "opencode_event",
101 101
                        event_type: ^event_type,
102 102
                        session_id: "ses_fixture"
103
                      }}, 5_000
103
                      }},
104
                     5_000
104 105
    end
105 106
106 107
    assert_receive {:scv_event, %{type: "process_finished", status: "succeeded"}}, 5_000
test/openagents/threads/grant_token_reach_test.exs modified +1 -1

@@ -164,7 +164,7 @@ defmodule OpenAgents.Threads.GrantTokenReachTest do

164 164
165 165
    created =
166 166
      conn
167
      |> post(~p"/api/v3/threads", %{"objective" => "Count the doors that hand out authority."})
167
      |> post(~p"/api/v1/threads", %{"objective" => "Count the doors that hand out authority."})
168 168
      |> json_response(201)
169 169
170 170
    thread_id = created["thread"]["id"]
test/openagents_web/api_error_test.exs modified +1 -1

@@ -104,7 +104,7 @@ defmodule OpenAgentsWeb.ApiErrorTest do

104 104
    test "points documentation_url at the published contract", %{conn: conn} do
105 105
      body = conn |> ApiError.refuse("not_found") |> json_response(404)
106 106
107
      assert body["documentation_url"] =~ "/api/v3"
107
      assert body["documentation_url"] =~ "/api/v1"
108 108
    end
109 109
  end
110 110
end
test/openagents_web/api_route_authority_test.exs modified +3 -3

@@ -5,13 +5,13 @@ defmodule OpenAgentsWeb.ApiRouteAuthorityTest do

5 5
6 6
  @endpoint OpenAgentsWeb.Endpoint
7 7
8
  # Every live /api/v3 route must be classified exactly once. A route added to
8
  # Every live /api/v1 route must be classified exactly once. A route added to
9 9
  # the router without an entry in ApiRouteAuthority fails here, so a new
10 10
  # surface cannot inherit authority from a broad pipeline default.
11
  test "the inventory covers every api v3 route and nothing else" do
11
  test "the inventory covers every api v1 route and nothing else" do
12 12
    live =
13 13
      OpenAgentsWeb.Router.__routes__()
14
      |> Enum.filter(&String.starts_with?(&1.path, "/api/v3"))
14
      |> Enum.filter(&String.starts_with?(&1.path, "/api/v1"))
15 15
      |> Enum.map(&{Atom.to_string(&1.verb), &1.path})
16 16
      |> MapSet.new()
17 17
test/openagents_web/contribution_contract_test.exs modified +6 -6

@@ -78,7 +78,7 @@ defmodule OpenAgentsWeb.ContributionContractTest do

78 78
    end
79 79
80 80
    test "the API description points at the contract with a matching digest", %{conn: conn} do
81
      contribution = conn |> get(~p"/api/v3") |> json_response(200) |> Map.fetch!("contribution")
81
      contribution = conn |> get(~p"/api/v1") |> json_response(200) |> Map.fetch!("contribution")
82 82
      document = document(conn)
83 83
84 84
      assert contribution["contract"] == document["contract"]

@@ -129,7 +129,7 @@ defmodule OpenAgentsWeb.ContributionContractTest do

129 129
    end
130 130
131 131
    test "every published API request states what the API authority says", %{conn: conn} do
132
      api = Enum.filter(requests(document(conn)), &String.starts_with?(&1["path"], "/api/v3"))
132
      api = Enum.filter(requests(document(conn)), &String.starts_with?(&1["path"], "/api/v1"))
133 133
134 134
      assert api != []
135 135

@@ -153,13 +153,13 @@ defmodule OpenAgentsWeb.ContributionContractTest do

153 153
    end
154 154
155 155
    # One authority per surface, so the document cannot hold two answers for
156
    # the same route. `POST /api/v3/agents/register` is the case that proves it
156
    # the same route. `POST /api/v1/agents/register` is the case that proves it
157 157
    # matters: the API inventory calls it anonymous, because that is what the
158
    # pipeline does, and the site-wide inventory's catch-all for `/api/v3`
158
    # pipeline does, and the site-wide inventory's catch-all for `/api/v1`
159 159
    # writes would have called it a bearer route.
160 160
    test "no API request is described by the site-wide authority", %{conn: conn} do
161 161
      for request <- requests(document(conn)) do
162
        if String.starts_with?(request["path"], "/api/v3") do
162
        if String.starts_with?(request["path"], "/api/v1") do
163 163
          assert request["access"]["source"] == "api"
164 164
          refute Map.has_key?(request["access"], "class")
165 165
        else

@@ -427,7 +427,7 @@ defmodule OpenAgentsWeb.ContributionContractTest do

427 427
      assert queue["note"] =~ "agent-ready"
428 428
429 429
      response =
430
        get(build_conn(), "/api/v3/repos/OpenAgentsInc/openagents.com/issues?labels=agent-ready")
430
        get(build_conn(), "/api/v1/repos/OpenAgentsInc/openagents.com/issues?labels=agent-ready")
431 431
432 432
      assert json_response(response, 200)["issues"] == []
433 433
    end
test/openagents_web/controllers/agent_controller_test.exs modified +13 -13

@@ -5,7 +5,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

5 5
6 6
  test "registers an agent and returns a one-time credential", %{conn: conn} do
7 7
    conn =
8
      post(conn, "/api/v3/agents/register", %{
8
      post(conn, "/api/v1/agents/register", %{
9 9
        "handle" => "controller-bot",
10 10
        "display_name" => "Controller bot"
11 11
      })

@@ -25,14 +25,14 @@ defmodule OpenAgentsWeb.AgentControllerTest do

25 25
  test "rejects duplicate and malformed handles with typed errors", %{conn: conn} do
26 26
    assert %{"agent" => _agent} =
27 27
             conn
28
             |> post("/api/v3/agents/register", %{
28
             |> post("/api/v1/agents/register", %{
29 29
               "handle" => "duplicate-bot",
30 30
               "display_name" => "Duplicate bot"
31 31
             })
32 32
             |> json_response(201)
33 33
34 34
    duplicate =
35
      post(conn, "/api/v3/agents/register", %{
35
      post(conn, "/api/v1/agents/register", %{
36 36
        "handle" => "duplicate-bot",
37 37
        "display_name" => "Duplicate bot"
38 38
      })

@@ -40,7 +40,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

40 40
    assert json_response(duplicate, 422)["error"]["code"] == "handle_unavailable"
41 41
42 42
    malformed =
43
      post(conn, "/api/v3/agents/register", %{
43
      post(conn, "/api/v1/agents/register", %{
44 44
        "handle" => "bot--name",
45 45
        "display_name" => "Malformed bot"
46 46
      })

@@ -50,7 +50,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

50 50
51 51
  test "rejects reserved handles through the controller", %{conn: conn} do
52 52
    conn =
53
      post(conn, "/api/v3/agents/register", %{
53
      post(conn, "/api/v1/agents/register", %{
54 54
        "handle" => "admin",
55 55
        "display_name" => "Admin bot"
56 56
      })

@@ -71,7 +71,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

71 71
    end)
72 72
73 73
    conn =
74
      post(conn, "/api/v3/agents/register", %{
74
      post(conn, "/api/v1/agents/register", %{
75 75
        "handle" => "limited-bot",
76 76
        "display_name" => "Limited bot"
77 77
      })

@@ -93,7 +93,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

93 93
    rotated =
94 94
      conn
95 95
      |> put_req_header("authorization", "Bearer #{credential}")
96
      |> post("/api/v3/agent/credentials", %{"name" => "rotated"})
96
      |> post("/api/v1/agent/credentials", %{"name" => "rotated"})
97 97
98 98
    assert %{
99 99
             "credential" => "oa_agent_" <> new_credential,

@@ -106,7 +106,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

106 106
      conn
107 107
      |> recycle()
108 108
      |> put_req_header("authorization", "Bearer #{credential}")
109
      |> get("/api/v3/agent")
109
      |> get("/api/v1/agent")
110 110
111 111
    assert json_response(old_still_works, 200)["agent"]["handle"] == "rotate-controller-bot"
112 112
  end

@@ -119,13 +119,13 @@ defmodule OpenAgentsWeb.AgentControllerTest do

119 119
        "registration_ip" => "192.0.2.30"
120 120
      })
121 121
122
    profile = get(conn, "/api/v3/agents/profile-bot")
122
    profile = get(conn, "/api/v1/agents/profile-bot")
123 123
    assert json_response(profile, 200)["agent"]["id"] == agent.id
124 124
125 125
    current =
126 126
      conn
127 127
      |> put_req_header("authorization", "Bearer #{credential}")
128
      |> get("/api/v3/agent")
128
      |> get("/api/v1/agent")
129 129
130 130
    assert json_response(current, 200)["agent"]["handle"] == "profile-bot"
131 131
  end

@@ -149,7 +149,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

149 149
      conn
150 150
      |> recycle()
151 151
      |> put_req_header("authorization", "Bearer #{credential}")
152
      |> post("/api/v3/repos/test-owner/test-repo/deployments/1/approvals", %{})
152
      |> post("/api/v1/repos/test-owner/test-repo/deployments/1/approvals", %{})
153 153
154 154
    assert promotion.status == 401
155 155

@@ -157,7 +157,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

157 157
      conn
158 158
      |> recycle()
159 159
      |> put_req_header("authorization", "Bearer #{credential}")
160
      |> patch("/api/v3/repos/test-owner/test-repo/issues/1", %{"title" => "test"})
160
      |> patch("/api/v1/repos/test-owner/test-repo/issues/1", %{"title" => "test"})
161 161
162 162
    assert membership.status == 401
163 163

@@ -165,7 +165,7 @@ defmodule OpenAgentsWeb.AgentControllerTest do

165 165
      conn
166 166
      |> recycle()
167 167
      |> put_req_header("authorization", "Bearer #{credential}")
168
      |> post("/api/v3/forum/posts/1/tips", %{"amount" => "1"})
168
      |> post("/api/v1/forum/posts/1/tips", %{"amount" => "1"})
169 169
170 170
    assert tip.status == 401
171 171
  end
test/openagents_web/controllers/api_error_contract_test.exs modified +3 -3

@@ -57,12 +57,12 @@ defmodule OpenAgentsWeb.ApiErrorContractTest do

57 57
58 58
    private_body =
59 59
      build_conn()
60
      |> get("/api/v3/repos/#{private.owner}/#{private.name}/issues/1")
60
      |> get("/api/v1/repos/#{private.owner}/#{private.name}/issues/1")
61 61
      |> json_response(404)
62 62
63 63
    absent_body =
64 64
      build_conn()
65
      |> get("/api/v3/repos/#{private.owner}/no-such-repository/issues/1")
65
      |> get("/api/v1/repos/#{private.owner}/no-such-repository/issues/1")
66 66
      |> json_response(404)
67 67
68 68
    assert Map.drop(private_body, ["request_id"]) == Map.drop(absent_body, ["request_id"])

@@ -73,7 +73,7 @@ defmodule OpenAgentsWeb.ApiErrorContractTest do

73 73
  test "a rejected filter names the field it rejected" do
74 74
    body =
75 75
      build_conn()
76
      |> get("/api/v3/repos/nobody/nonexistent/issues?state=sideways")
76
      |> get("/api/v1/repos/nobody/nonexistent/issues?state=sideways")
77 77
      |> json_response(404)
78 78
79 79
    # The repository check runs first, so this path proves only non-disclosure.
test/openagents_web/controllers/api_extension_controller_test.exs modified +11 -11

@@ -1,11 +1,11 @@

1 1
defmodule OpenAgentsWeb.ApiExtensionControllerTest do
2 2
  use OpenAgentsWeb.ConnCase, async: true
3 3
4
  test "GET /api/v3 lists the issue extension fields an agent can rely on", %{conn: conn} do
5
    conn = get(conn, ~p"/api/v3")
4
  test "GET /api/v1 lists the issue extension fields an agent can rely on", %{conn: conn} do
5
    conn = get(conn, ~p"/api/v1")
6 6
7 7
    assert %{
8
             "api_version" => "v3",
8
             "api_version" => "v1",
9 9
             "extensions" => %{
10 10
               "issue.openagents" => %{
11 11
                 "version" => _version,

@@ -22,16 +22,16 @@ defmodule OpenAgentsWeb.ApiExtensionControllerTest do

22 22
    assert Enum.any?(endpoints, &String.contains?(&1, "/dependencies"))
23 23
  end
24 24
25
  test "GET /api/v3 publishes one route entry for every live API v3 route", %{conn: conn} do
25
  test "GET /api/v1 publishes one route entry for every live API v1 route", %{conn: conn} do
26 26
    published =
27 27
      conn
28
      |> get(~p"/api/v3")
28
      |> get(~p"/api/v1")
29 29
      |> json_response(200)
30 30
      |> Map.fetch!("routes")
31 31
32 32
    live =
33 33
      OpenAgentsWeb.Router.__routes__()
34
      |> Enum.filter(&String.starts_with?(&1.path, "/api/v3"))
34
      |> Enum.filter(&String.starts_with?(&1.path, "/api/v1"))
35 35
      |> Enum.map(fn route ->
36 36
        {String.upcase(Atom.to_string(route.verb)),
37 37
         Regex.replace(~r/:([a-z_]+)/, route.path, fn _whole, segment -> "{#{segment}}" end)}

@@ -44,7 +44,7 @@ defmodule OpenAgentsWeb.ApiExtensionControllerTest do

44 44
  test "every published route names its authority, family, and error contract", %{conn: conn} do
45 45
    published =
46 46
      conn
47
      |> get(~p"/api/v3")
47
      |> get(~p"/api/v1")
48 48
      |> json_response(200)
49 49
      |> Map.fetch!("routes")
50 50

@@ -60,8 +60,8 @@ defmodule OpenAgentsWeb.ApiExtensionControllerTest do

60 60
    end
61 61
  end
62 62
63
  test "GET /api/v3 publishes the error envelope and its stable codes", %{conn: conn} do
64
    body = conn |> get(~p"/api/v3") |> json_response(200)
63
  test "GET /api/v1 publishes the error envelope and its stable codes", %{conn: conn} do
64
    body = conn |> get(~p"/api/v1") |> json_response(200)
65 65
66 66
    assert body["errors"]["envelope"] == OpenAgentsWeb.ApiError.envelope_keys()
67 67

@@ -72,8 +72,8 @@ defmodule OpenAgentsWeb.ApiExtensionControllerTest do

72 72
    assert is_binary(body["errors"]["field_errors"])
73 73
  end
74 74
75
  test "GET /api/v3 names every family the router serves", %{conn: conn} do
76
    body = conn |> get(~p"/api/v3") |> json_response(200)
75
  test "GET /api/v1 names every family the router serves", %{conn: conn} do
76
    body = conn |> get(~p"/api/v1") |> json_response(200)
77 77
78 78
    published = MapSet.new(body["routes"], & &1["family"])
79 79
test/openagents_web/controllers/api_extension_governance_test.exs modified +18 -18

@@ -2,7 +2,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

2 2
  @moduledoc """
3 3
  The rule that makes the extension surface governed rather than documented.
4 4
5
  A field only counts as part of the API once `GET /api/v3` enumerates it, and
5
  A field only counts as part of the API once `GET /api/v1` enumerates it, and
6 6
  a filter only counts once the endpoint it names actually rejects a value
7 7
  outside its enum. These tests read the root document and the live responses
8 8
  and refuse any disagreement between them, so adding a field to the

@@ -33,12 +33,12 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

33 33
34 34
    documented = documented_fields(conn, "issue.openagents")
35 35
36
    show = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{blocked.number}")
36
    show = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{blocked.number}")
37 37
    served = show |> json_response(200) |> Map.fetch!("openagents") |> Map.keys() |> Enum.sort()
38 38
39 39
    assert served == documented
40 40
41
    index = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
41
    index = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
42 42
43 43
    for issue <- json_response(index, 200)["issues"] do
44 44
      assert issue |> Map.fetch!("openagents") |> Map.keys() |> Enum.sort() == documented

@@ -51,10 +51,10 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

51 51
  } do
52 52
    {:ok, issue} = Issues.create_issue(repository, %{title: "Named"})
53 53
54
    show = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
54
    show = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
55 55
    [named] = get_resp_header(show, "x-openagents-extensions")
56 56
57
    extensions = conn |> get(~p"/api/v3") |> json_response(200) |> Map.fetch!("extensions")
57
    extensions = conn |> get(~p"/api/v1") |> json_response(200) |> Map.fetch!("extensions")
58 58
59 59
    for name <- String.split(named, ",", trim: true) do
60 60
      assert Map.has_key?(extensions, String.trim(name))

@@ -64,7 +64,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

64 64
  test "every documented filter is enforced by the endpoint that documents it", %{conn: conn} do
65 65
    filters =
66 66
      conn
67
      |> get(~p"/api/v3")
67
      |> get(~p"/api/v1")
68 68
      |> json_response(200)
69 69
      |> get_in(["extensions", "issue.openagents", "filters"])
70 70

@@ -93,7 +93,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

93 93
  } do
94 94
    field =
95 95
      conn
96
      |> get(~p"/api/v3")
96
      |> get(~p"/api/v1")
97 97
      |> json_response(200)
98 98
      |> get_in(["extensions", "issue.openagents", "fields", "progress"])
99 99

@@ -105,7 +105,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

105 105
  } do
106 106
    parameter =
107 107
      conn
108
      |> get(~p"/api/v3")
108
      |> get(~p"/api/v1")
109 109
      |> json_response(200)
110 110
      |> get_in(["extensions", "chat.openagents", "parameters", "model"])
111 111

@@ -115,7 +115,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

115 115
  end
116 116
117 117
  test "every published backend is one a turn actually accepts", %{conn: conn} do
118
    document = conn |> get(~p"/api/v3") |> json_response(200)
118
    document = conn |> get(~p"/api/v1") |> json_response(200)
119 119
    published = get_in(document, ["extensions", "chat.openagents", "backends"])
120 120
121 121
    assert Enum.map(published, & &1["id"]) == OpenAgents.Chat.Backends.ids()

@@ -126,7 +126,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

126 126
      accepted =
127 127
        conn
128 128
        |> put_chat_api_token("governance-backend-" <> backend["id"])
129
        |> post(~p"/api/v3/chat/turns", %{"message" => "Hello.", "model" => backend["id"]})
129
        |> post(~p"/api/v1/chat/turns", %{"message" => "Hello.", "model" => backend["id"]})
130 130
131 131
      assert json_response(accepted, 202)["turn"]["model"] == backend["id"]
132 132
    end

@@ -136,7 +136,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

136 136
    refusal =
137 137
      conn
138 138
      |> put_chat_api_token("governance-backend-unknown")
139
      |> post(~p"/api/v3/chat/turns", %{"message" => "Hello.", "model" => "not-a-legal-value"})
139
      |> post(~p"/api/v1/chat/turns", %{"message" => "Hello.", "model" => "not-a-legal-value"})
140 140
141 141
    assert %{"errors" => errors} = json_response(refusal, 422)
142 142
    assert Map.has_key?(errors, "model")

@@ -147,7 +147,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

147 147
  } do
148 148
    parameters =
149 149
      conn
150
      |> get(~p"/api/v3")
150
      |> get(~p"/api/v1")
151 151
      |> json_response(200)
152 152
      |> get_in(["extensions", "thread.openagents", "parameters"])
153 153

@@ -169,7 +169,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

169 169
  } do
170 170
    limits =
171 171
      conn
172
      |> get(~p"/api/v3")
172
      |> get(~p"/api/v1")
173 173
      |> json_response(200)
174 174
      |> get_in(["extensions", "thread.openagents", "limits"])
175 175

@@ -191,7 +191,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

191 191
    created =
192 192
      conn
193 193
      |> put_chat_api_token("governance-thread-budget")
194
      |> post(~p"/api/v3/threads", %{"objective" => "Measure the published budget."})
194
      |> post(~p"/api/v1/threads", %{"objective" => "Measure the published budget."})
195 195
      |> json_response(201)
196 196
197 197
    granted = created["grant"]["limits"]

@@ -205,7 +205,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

205 205
  test "every published thread parameter value is one the route actually accepts", %{conn: conn} do
206 206
    parameters =
207 207
      conn
208
      |> get(~p"/api/v3")
208
      |> get(~p"/api/v1")
209 209
      |> json_response(200)
210 210
      |> get_in(["extensions", "thread.openagents", "parameters"])
211 211

@@ -214,14 +214,14 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

214 214
      accepted =
215 215
        conn
216 216
        |> put_chat_api_token("governance-thread-#{name}-#{value}")
217
        |> post(~p"/api/v3/threads", %{"objective" => "Accept #{value}.", name => value})
217
        |> post(~p"/api/v1/threads", %{"objective" => "Accept #{value}.", name => value})
218 218
219 219
      assert json_response(accepted, 201)["thread"]
220 220
221 221
      refused =
222 222
        conn
223 223
        |> put_chat_api_token("governance-thread-#{name}-refused")
224
        |> post(~p"/api/v3/threads", %{
224
        |> post(~p"/api/v1/threads", %{
225 225
          "objective" => "Refuse anything else.",
226 226
          name => "not-a-legal-value"
227 227
        })

@@ -235,7 +235,7 @@ defmodule OpenAgentsWeb.ApiExtensionGovernanceTest do

235 235
236 236
  defp documented_fields(conn, extension) do
237 237
    conn
238
    |> get(~p"/api/v3")
238
    |> get(~p"/api/v1")
239 239
    |> json_response(200)
240 240
    |> get_in(["extensions", extension, "fields"])
241 241
    |> Map.keys()
test/openagents_web/controllers/api_token_controller_test.exs modified +1 -1

@@ -32,7 +32,7 @@ defmodule OpenAgentsWeb.ApiTokenControllerTest do

32 32
  test "forge mutations refuse missing, malformed, expired, and revoked credentials", %{
33 33
    conn: conn
34 34
  } do
35
    path = ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues"
35
    path = ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues"
36 36
37 37
    missing = conn |> delete_req_header("authorization") |> post(path, %{title: "denied"})
38 38
    assert assert_api_error(missing, 401, "unauthenticated")["error"] == "invalid_api_token"
test/openagents_web/controllers/assignee_controller_test.exs modified +6 -6

@@ -4,27 +4,27 @@ defmodule OpenAgentsWeb.AssigneeControllerTest do

4 4
  import OpenAgents.AccountsFixtures
5 5
6 6
  describe "index" do
7
    test "GET /api/v3/repos/:owner/:repo/assignees returns the assignable list", %{conn: conn} do
7
    test "GET /api/v1/repos/:owner/:repo/assignees returns the assignable list", %{conn: conn} do
8 8
      user = repository_user_fixture("octocat")
9 9
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), user, "contributor")
10
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/assignees")
10
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/assignees")
11 11
12 12
      assert %{"assignees" => [%{"login" => "octocat"}]} = json_response(conn, 200)
13 13
    end
14 14
  end
15 15
16 16
  describe "show" do
17
    test "GET /api/v3/repos/:owner/:repo/assignees/:assignee returns an assignable user",
17
    test "GET /api/v1/repos/:owner/:repo/assignees/:assignee returns an assignable user",
18 18
         %{conn: conn} do
19 19
      user = repository_user_fixture("octocat")
20 20
      {:ok, _membership} = OpenAgents.Repositories.add_member(repository(), user, "contributor")
21
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/assignees/octocat")
21
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/assignees/octocat")
22 22
23 23
      assert %{"login" => "octocat"} = json_response(conn, 200)
24 24
    end
25 25
26
    test "GET /api/v3/repos/:owner/:repo/assignees/:assignee hides a nonmember", %{conn: conn} do
27
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/assignees/not-a-member")
26
    test "GET /api/v1/repos/:owner/:repo/assignees/:assignee hides a nonmember", %{conn: conn} do
27
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/assignees/not-a-member")
28 28
29 29
      assert_api_error(conn, 404, "not_found")
30 30
    end
test/openagents_web/controllers/box_controller_test.exs modified +2 -2

@@ -240,7 +240,7 @@ defmodule OpenAgentsWeb.BoxControllerTest do

240 240
      conn
241 241
      |> put_req_header("authorization", "Bearer " <> credential)
242 242
      |> post(
243
        "/api/v3/conversations/#{conversation.id}/boxes/#{box.box_id}/assignments",
243
        "/api/v1/conversations/#{conversation.id}/boxes/#{box.box_id}/assignments",
244 244
        %{
245 245
          "repository_id" => repository.id,
246 246
          "issue_number" => issue.number,

@@ -485,7 +485,7 @@ defmodule OpenAgentsWeb.BoxControllerTest do

485 485
    assert Task.await(second) == {:error, :rate_limited}
486 486
  end
487 487
488
  defp box_path(conversation_id), do: "/api/v3/conversations/#{conversation_id}/boxes"
488
  defp box_path(conversation_id), do: "/api/v1/conversations/#{conversation_id}/boxes"
489 489
490 490
  defp box_body(overrides \\ %{}) do
491 491
    %{
test/openagents_web/controllers/box_fanout_controller_test.exs modified +2 -2

@@ -55,7 +55,7 @@ defmodule OpenAgentsWeb.BoxFanoutControllerTest do

55 55
    response =
56 56
      conn
57 57
      |> put_box_api_token("box-fanout-controller")
58
      |> post("/api/v3/conversations/#{conversation.id}/boxes/fanout", %{
58
      |> post("/api/v1/conversations/#{conversation.id}/boxes/fanout", %{
59 59
        "count" => 3,
60 60
        "labels" => ["left", "middle", "right"]
61 61
      })

@@ -81,7 +81,7 @@ defmodule OpenAgentsWeb.BoxFanoutControllerTest do

81 81
    response =
82 82
      conn
83 83
      |> put_box_api_token("box-fanout-foreign-account")
84
      |> post("/api/v3/conversations/#{conversation.id}/boxes/fanout", %{"count" => 1})
84
      |> post("/api/v1/conversations/#{conversation.id}/boxes/fanout", %{"count" => 1})
85 85
      |> json_response(404)
86 86
87 87
    assert response == %{"error" => %{"code" => "conversation_not_found"}}
test/openagents_web/controllers/box_run_controller_test.exs modified +4 -4

@@ -69,7 +69,7 @@ defmodule OpenAgentsWeb.BoxRunControllerTest do

69 69
    response =
70 70
      conn
71 71
      |> put_box_api_token("box-run-controller")
72
      |> get("/api/v3/conversations/#{conversation.id}/boxes/#{box.box_id}/runs/#{run.id}")
72
      |> get("/api/v1/conversations/#{conversation.id}/boxes/#{box.box_id}/runs/#{run.id}")
73 73
      |> json_response(200)
74 74
75 75
    assert response["run"]["id"] == run.id

@@ -80,7 +80,7 @@ defmodule OpenAgentsWeb.BoxRunControllerTest do

80 80
      conn
81 81
      |> put_box_api_token("box-run-controller")
82 82
      |> get(
83
        "/api/v3/conversations/#{conversation.id}/boxes/#{box.box_id}/runs/#{run.id}/output?offset=2"
83
        "/api/v1/conversations/#{conversation.id}/boxes/#{box.box_id}/runs/#{run.id}/output?offset=2"
84 84
      )
85 85
      |> json_response(200)
86 86

@@ -96,7 +96,7 @@ defmodule OpenAgentsWeb.BoxRunControllerTest do

96 96
    {:ok, foreign} = Conversations.ensure_conversation(github_user("box-run-foreign"))
97 97
98 98
    path =
99
      "/api/v3/conversations/#{foreign.id}/boxes/bx_foreign/runs/missing/output?offset=0"
99
      "/api/v1/conversations/#{foreign.id}/boxes/bx_foreign/runs/missing/output?offset=0"
100 100
101 101
    response =
102 102
      conn

@@ -134,7 +134,7 @@ defmodule OpenAgentsWeb.BoxRunControllerTest do

134 134
      Req.Test.json(request, %{"stdout" => "4242\n"})
135 135
    end)
136 136
137
    path = "/api/v3/conversations/#{conversation.id}/boxes/#{box.box_id}/runs"
137
    path = "/api/v1/conversations/#{conversation.id}/boxes/#{box.box_id}/runs"
138 138
139 139
    first =
140 140
      conn
test/openagents_web/controllers/capacity_controller_test.exs modified +2 -2

@@ -39,7 +39,7 @@ defmodule OpenAgentsWeb.CapacityControllerTest do

39 39
    bearer_conn =
40 40
      build_conn()
41 41
      |> put_chat_api_token("capacity-bearer")
42
      |> get("/api/v3/capacity")
42
      |> get("/api/v1/capacity")
43 43
44 44
    assert json_response(session_conn, 200) == json_response(bearer_conn, 200)
45 45
  end

@@ -48,7 +48,7 @@ defmodule OpenAgentsWeb.CapacityControllerTest do

48 48
    conn =
49 49
      conn
50 50
      |> put_chat_api_token("capacity-unsupported")
51
      |> post("/api/v3/capacity/matches", %{
51
      |> post("/api/v1/capacity/matches", %{
52 52
        "requirement" => %{
53 53
          "isolation" => "managed_confidential",
54 54
          "egress" => "policy_broker",
test/openagents_web/controllers/chat_turn_controller_test.exs modified +9 -9

@@ -8,7 +8,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

8 8
    response =
9 9
      conn
10 10
      |> put_chat_api_token(key)
11
      |> post(~p"/api/v3/chat/turns", %{"message" => "Read the README."})
11
      |> post(~p"/api/v1/chat/turns", %{"message" => "Read the README."})
12 12
      |> json_response(202)
13 13
14 14
    assert %{"id" => run_id, "status" => "streaming"} = response["turn"]

@@ -16,7 +16,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

16 16
    response =
17 17
      conn
18 18
      |> put_chat_api_token(key)
19
      |> get(~p"/api/v3/chat/events")
19
      |> get(~p"/api/v1/chat/events")
20 20
      |> json_response(200)
21 21
22 22
    assert [first | _events] = response["events"]

@@ -30,7 +30,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

30 30
    refusal =
31 31
      conn
32 32
      |> put_forge_api_token("chat-wrong-scope")
33
      |> get(~p"/api/v3/chat/events")
33
      |> get(~p"/api/v1/chat/events")
34 34
35 35
    assert assert_api_error(refusal, 401, "unauthenticated")["error"] == "invalid_api_token"
36 36
  end

@@ -82,7 +82,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

82 82
    events =
83 83
      conn
84 84
      |> put_chat_api_token(key)
85
      |> get(~p"/api/v3/chat/events")
85
      |> get(~p"/api/v1/chat/events")
86 86
      |> json_response(200)
87 87
      |> Map.fetch!("events")
88 88

@@ -162,7 +162,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

162 162
    events =
163 163
      conn
164 164
      |> put_chat_api_token(key)
165
      |> get(~p"/api/v3/chat/events")
165
      |> get(~p"/api/v1/chat/events")
166 166
      |> json_response(200)
167 167
      |> Map.fetch!("events")
168 168

@@ -187,7 +187,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

187 187
  end
188 188
189 189
  test "chat API requires a bearer token", %{conn: conn} do
190
    assert conn |> get(~p"/api/v3/chat/events") |> api_error_code(401) == "unauthenticated"
190
    assert conn |> get(~p"/api/v1/chat/events") |> api_error_code(401) == "unauthenticated"
191 191
  end
192 192
193 193
  describe "backend selection" do

@@ -201,7 +201,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

201 201
        response =
202 202
          conn
203 203
          |> put_chat_api_token(key)
204
          |> post(~p"/api/v3/chat/turns", Map.merge(%{"message" => "Hello."}, sent))
204
          |> post(~p"/api/v1/chat/turns", Map.merge(%{"message" => "Hello."}, sent))
205 205
          |> json_response(202)
206 206
207 207
        assert response["turn"]["model"] == expected

@@ -212,7 +212,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

212 212
      refusal =
213 213
        conn
214 214
        |> put_chat_api_token("chat-bad-backend")
215
        |> post(~p"/api/v3/chat/turns", %{"message" => "Hello.", "model" => "gpt-4"})
215
        |> post(~p"/api/v1/chat/turns", %{"message" => "Hello.", "model" => "gpt-4"})
216 216
217 217
      body = json_response(refusal, 422)
218 218

@@ -229,7 +229,7 @@ defmodule OpenAgentsWeb.ChatTurnControllerTest do

229 229
      response =
230 230
        conn
231 231
        |> put_chat_api_token("chat-empty-backend")
232
        |> post(~p"/api/v3/chat/turns", %{"message" => "Hello.", "model" => ""})
232
        |> post(~p"/api/v1/chat/turns", %{"message" => "Hello.", "model" => ""})
233 233
        |> json_response(202)
234 234
235 235
      assert response["turn"]["model"] == "ox-alpha"
test/openagents_web/controllers/comment_controller_test.exs modified +11 -11

@@ -11,27 +11,27 @@ defmodule OpenAgentsWeb.CommentControllerTest do

11 11
    %{issue: issue}
12 12
  end
13 13
14
  test "GET /api/v3/repos/:owner/:repo/issues/:issue_number/comments lists comments", %{
14
  test "GET /api/v1/repos/:owner/:repo/issues/:issue_number/comments lists comments", %{
15 15
    conn: conn,
16 16
    issue: issue
17 17
  } do
18 18
    Issues.create_comment(%{issue_id: issue.id, body: "First comment"})
19 19
20 20
    conn =
21
      get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/comments")
21
      get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/comments")
22 22
23 23
    assert %{"comments" => [comment]} = json_response(conn, 200)
24 24
    assert comment["body"] == "First comment"
25 25
  end
26 26
27
  test "POST /api/v3/repos/:owner/:repo/issues/:issue_number/comments creates a comment", %{
27
  test "POST /api/v1/repos/:owner/:repo/issues/:issue_number/comments creates a comment", %{
28 28
    conn: conn,
29 29
    issue: issue
30 30
  } do
31 31
    conn =
32 32
      post(
33 33
        conn,
34
        ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/comments",
34
        ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/comments",
35 35
        %{
36 36
          body: "New comment"
37 37
        }

@@ -55,7 +55,7 @@ defmodule OpenAgentsWeb.CommentControllerTest do

55 55
      conn
56 56
      |> put_req_header("authorization", "Bearer #{credential}")
57 57
      |> post(
58
        ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/comments",
58
        ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/comments",
59 59
        %{body: "Agent comment"}
60 60
      )
61 61

@@ -63,40 +63,40 @@ defmodule OpenAgentsWeb.CommentControllerTest do

63 63
             json_response(conn, 201)
64 64
  end
65 65
66
  test "GET /api/v3/repos/:owner/:repo/issues/comments/:id returns a comment", %{
66
  test "GET /api/v1/repos/:owner/:repo/issues/comments/:id returns a comment", %{
67 67
    conn: conn,
68 68
    issue: issue
69 69
  } do
70 70
    {:ok, comment} = Issues.create_comment(%{issue_id: issue.id, body: "Show me"})
71 71
72 72
    conn =
73
      get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/comments/#{comment.id}")
73
      get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/comments/#{comment.id}")
74 74
75 75
    assert %{"body" => "Show me"} = json_response(conn, 200)
76 76
  end
77 77
78
  test "PATCH /api/v3/repos/:owner/:repo/issues/comments/:id updates a comment", %{
78
  test "PATCH /api/v1/repos/:owner/:repo/issues/comments/:id updates a comment", %{
79 79
    conn: conn,
80 80
    issue: issue
81 81
  } do
82 82
    {:ok, comment} = Issues.create_comment(%{issue_id: issue.id, body: "Before"})
83 83
84 84
    conn =
85
      patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/comments/#{comment.id}", %{
85
      patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/comments/#{comment.id}", %{
86 86
        body: "After"
87 87
      })
88 88
89 89
    assert %{"body" => "After"} = json_response(conn, 200)
90 90
  end
91 91
92
  test "DELETE /api/v3/repos/:owner/:repo/issues/comments/:id removes a comment", %{
92
  test "DELETE /api/v1/repos/:owner/:repo/issues/comments/:id removes a comment", %{
93 93
    conn: conn,
94 94
    issue: issue
95 95
  } do
96 96
    {:ok, comment} = Issues.create_comment(%{issue_id: issue.id, body: "Delete me"})
97 97
98 98
    conn =
99
      delete(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/comments/#{comment.id}")
99
      delete(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/comments/#{comment.id}")
100 100
101 101
    assert response(conn, 204)
102 102
  end
test/openagents_web/controllers/computer_control_api_test.exs modified +21 -21

@@ -20,7 +20,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

20 20
21 21
    assert build_conn()
22 22
           |> put_api_token(user, ["computer:control"])
23
           |> get(~p"/api/v3/computers")
23
           |> get(~p"/api/v1/computers")
24 24
           |> json_response(200) == %{
25 25
             "schema" => "openagents.computers.v1",
26 26
             "computers" => [],

@@ -29,16 +29,16 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

29 29
30 30
    assert build_conn()
31 31
           |> put_api_token(user, ["box:control"])
32
           |> get(~p"/api/v3/computers")
32
           |> get(~p"/api/v1/computers")
33 33
           |> json_response(401) == %{"error" => %{"code" => "invalid_api_token"}}
34 34
35 35
    assert build_conn()
36 36
           |> put_api_token(user, ["computer:control"])
37
           |> get(~p"/api/v3/conversations/not-a-conversation/boxes")
37
           |> get(~p"/api/v1/conversations/not-a-conversation/boxes")
38 38
           |> json_response(401) == %{"error" => %{"code" => "invalid_api_token"}}
39 39
40 40
    assert conn
41
           |> get(~p"/api/v3/computers")
41
           |> get(~p"/api/v1/computers")
42 42
           |> json_response(401) == %{"error" => %{"code" => "invalid_api_token"}}
43 43
  end
44 44

@@ -49,7 +49,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

49 49
50 50
    listed =
51 51
      token_conn
52
      |> get(~p"/api/v3/computers")
52
      |> get(~p"/api/v1/computers")
53 53
      |> json_response(200)
54 54
      |> get_in(["computers", Access.at(0)])
55 55

@@ -76,7 +76,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

76 76
77 77
    probed =
78 78
      token_conn
79
      |> post(~p"/api/v3/computers/#{machine.id}/probe")
79
      |> post(~p"/api/v1/computers/#{machine.id}/probe")
80 80
      |> json_response(200)
81 81
82 82
    assert probed["computer"]["online"]

@@ -98,14 +98,14 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

98 98
99 99
    assert build_conn()
100 100
           |> bearer(credential)
101
           |> get(~p"/api/v3/computers")
101
           |> get(~p"/api/v1/computers")
102 102
           |> json_response(200)
103 103
           |> Map.get("computers")
104 104
           |> Enum.any?(&(&1["id"] == machine.id))
105 105
106 106
    assert build_conn()
107 107
           |> bearer(credential)
108
           |> patch(~p"/api/v3/computers/#{machine.id}", %{
108
           |> patch(~p"/api/v1/computers/#{machine.id}", %{
109 109
             "scoped_forge_credentials_enabled" => true
110 110
           })
111 111
           |> api_error_code(401) == "unauthenticated"

@@ -114,7 +114,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

114 114
115 115
    assert build_conn()
116 116
           |> bearer(credential)
117
           |> get(~p"/api/v3/computers")
117
           |> get(~p"/api/v1/computers")
118 118
           |> json_response(403) == %{
119 119
             "error" => %{"code" => "agent_computer_control_forbidden"}
120 120
           }

@@ -131,7 +131,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

131 131
132 132
    assert conn
133 133
           |> bearer(box_credential)
134
           |> get(~p"/api/v3/computers")
134
           |> get(~p"/api/v1/computers")
135 135
           |> json_response(403) == %{
136 136
             "error" => %{"code" => "agent_computer_control_forbidden"}
137 137
           }

@@ -143,7 +143,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

143 143
144 144
    assert conn
145 145
           |> bearer(computer_credential)
146
           |> get(~p"/api/v3/conversations/#{conversation.id}/boxes")
146
           |> get(~p"/api/v1/conversations/#{conversation.id}/boxes")
147 147
           |> json_response(403) == %{
148 148
             "error" => %{"code" => "agent_box_control_forbidden"}
149 149
           }

@@ -167,7 +167,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

167 167
168 168
    response =
169 169
      token_conn
170
      |> post(~p"/api/v3/computers/#{machine.id}/agent-jobs", %{
170
      |> post(~p"/api/v1/computers/#{machine.id}/agent-jobs", %{
171 171
        "agent_id" => "codex",
172 172
        "prompt" => "do not echo this prompt",
173 173
        "cwd" => @root

@@ -180,14 +180,14 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

180 180
181 181
    assert build_conn()
182 182
           |> put_api_token(outsider, ["computer:control"])
183
           |> get(~p"/api/v3/computer-agent-jobs/#{job_id}")
183
           |> get(~p"/api/v1/computer-agent-jobs/#{job_id}")
184 184
           |> json_response(404) == %{"error" => "job_not_found"}
185 185
186 186
    [{pid, _value}] = Horde.Registry.lookup(OpenAgents.HordeRegistry, {:work_job, job_id})
187 187
    ref = Process.monitor(pid)
188 188
189 189
    assert token_conn
190
           |> delete(~p"/api/v3/computer-agent-jobs/#{job_id}")
190
           |> delete(~p"/api/v1/computer-agent-jobs/#{job_id}")
191 191
           |> json_response(202)
192 192
           |> Map.take(["job_id", "status"])
193 193
           |> Map.equal?(%{"job_id" => job_id, "status" => "stopping"})

@@ -204,7 +204,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

204 204
205 205
    missing_agent =
206 206
      token_conn
207
      |> post(~p"/api/v3/computers/#{machine.id}/agent-jobs", %{
207
      |> post(~p"/api/v1/computers/#{machine.id}/agent-jobs", %{
208 208
        "agent_id" => "codex",
209 209
        "prompt" => "bounded",
210 210
        "cwd" => @root

@@ -215,7 +215,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

215 215
216 216
    invalid_cwd =
217 217
      token_conn
218
      |> post(~p"/api/v3/computers/#{machine.id}/agent-jobs", %{
218
      |> post(~p"/api/v1/computers/#{machine.id}/agent-jobs", %{
219 219
        "agent_id" => "claude",
220 220
        "prompt" => "bounded",
221 221
        "cwd" => "/tmp/outside"

@@ -244,7 +244,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

244 244
    response =
245 245
      build_conn()
246 246
      |> put_api_token(user, ["computer:control"])
247
      |> post(~p"/api/v3/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
247
      |> post(~p"/api/v1/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
248 248
        "repository_id" => repository.id,
249 249
        "issue_number" => issue.number,
250 250
        "branch" => "agent/computer-#{issue.number}",

@@ -299,7 +299,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

299 299
    response =
300 300
      build_conn()
301 301
      |> put_api_token(user, ["computer:control"])
302
      |> post(~p"/api/v3/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
302
      |> post(~p"/api/v1/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
303 303
        "repository_id" => repository.id,
304 304
        "issue_number" => issue.number,
305 305
        "branch" => "agent/policy-#{issue.number}",

@@ -316,7 +316,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

316 316
    updated =
317 317
      build_conn()
318 318
      |> put_api_token(user, ["computer:control"])
319
      |> patch(~p"/api/v3/computers/#{machine.id}", %{
319
      |> patch(~p"/api/v1/computers/#{machine.id}", %{
320 320
        "scoped_forge_credentials_enabled" => false
321 321
      })
322 322
      |> json_response(200)

@@ -361,7 +361,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

361 361
    response =
362 362
      build_conn()
363 363
      |> put_api_token(user, ["computer:control"])
364
      |> post(~p"/api/v3/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
364
      |> post(~p"/api/v1/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
365 365
        "repository_id" => repository.id,
366 366
        "issue_number" => issue.number,
367 367
        "branch" => "agent/disconnect-#{issue.number}",

@@ -411,7 +411,7 @@ defmodule OpenAgentsWeb.ComputerControlApiTest do

411 411
    response =
412 412
      build_conn()
413 413
      |> put_api_token(user, ["computer:control"])
414
      |> post(~p"/api/v3/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
414
      |> post(~p"/api/v1/conversations/#{conversation.id}/computers/#{machine.id}/assignments", %{
415 415
        "repository_id" => repository.id,
416 416
        "issue_number" => issue.number,
417 417
        "branch" => "agent/computer-#{issue.number}",
test/openagents_web/controllers/delegations_controller_test.exs modified +24 -24

@@ -18,7 +18,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

18 18
    response =
19 19
      conn
20 20
      |> put_api_token(user, ["computer:control"])
21
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegation-targets")
21
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegation-targets")
22 22
      |> json_response(200)
23 23
24 24
    assert response["schema"] == "openagents.delegation_targets.v1"

@@ -28,7 +28,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

28 28
    response =
29 29
      build_conn()
30 30
      |> put_api_token(user, ["box:control"])
31
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegation-targets")
31
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegation-targets")
32 32
      |> json_response(200)
33 33
34 34
    assert [%{"kind" => "box", "id" => "box:" <> _}] = response["targets"]

@@ -43,7 +43,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

43 43
    response =
44 44
      conn
45 45
      |> put_api_token(outsider, ["box:control"])
46
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegation-targets")
46
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegation-targets")
47 47
      |> json_response(404)
48 48
49 49
    assert response == %{"error" => %{"code" => "conversation_not_found"}}

@@ -52,7 +52,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

52 52
      build_conn()
53 53
      |> put_api_token(owner, ["box:control"])
54 54
      |> get(
55
        ~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{Ecto.UUID.generate()}"
55
        ~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{Ecto.UUID.generate()}"
56 56
      )
57 57
      |> json_response(404)
58 58

@@ -68,7 +68,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

68 68
      response =
69 69
        conn
70 70
        |> put_api_token(user, ["box:control", "computer:control"])
71
        |> get(~p"/api/v3/conversations/#{conversation.id}/delegations/#{id}")
71
        |> get(~p"/api/v1/conversations/#{conversation.id}/delegations/#{id}")
72 72
        |> json_response(404)
73 73
74 74
      assert response == %{"error" => %{"code" => "delegation_not_found"}}

@@ -85,7 +85,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

85 85
86 86
    missing_agent =
87 87
      token_conn
88
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
88
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
89 89
        "target_id" => "computer:#{machine.id}",
90 90
        "agent_id" => "codex",
91 91
        "prompt" => "private prompt",

@@ -99,7 +99,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

99 99
    invalid_cwd =
100 100
      build_conn()
101 101
      |> put_api_token(user, ["computer:control"])
102
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
102
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
103 103
        "target_id" => "computer:#{machine.id}",
104 104
        "agent_id" => "claude",
105 105
        "prompt" => "private prompt",

@@ -119,7 +119,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

119 119
    response =
120 120
      conn
121 121
      |> put_api_token(user, ["computer:control"])
122
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
122
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
123 123
        "target_id" => "computer:#{machine.id}",
124 124
        "agent_id" => "codex",
125 125
        "prompt" => "do not queue",

@@ -140,7 +140,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

140 140
    response =
141 141
      conn
142 142
      |> put_api_token(user, ["box:control"])
143
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
143
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
144 144
        "target_id" => "computer:#{machine.id}",
145 145
        "agent_id" => "codex",
146 146
        "prompt" => "bounded",

@@ -153,7 +153,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

153 153
    response =
154 154
      build_conn()
155 155
      |> put_api_token(user, ["computer:control"])
156
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
156
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
157 157
        "target_id" => "box:#{box.id}",
158 158
        "command" => "echo bounded"
159 159
      })

@@ -178,7 +178,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

178 178
    response =
179 179
      conn
180 180
      |> bearer(box_credential)
181
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
181
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
182 182
        "target_id" => "computer:#{machine.id}",
183 183
        "agent_id" => "codex",
184 184
        "prompt" => "must refuse",

@@ -191,7 +191,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

191 191
    response =
192 192
      build_conn()
193 193
      |> bearer(box_credential)
194
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegations/computer-job:#{job.id}")
194
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegations/computer-job:#{job.id}")
195 195
      |> json_response(403)
196 196
197 197
    assert response == %{"error" => %{"code" => "agent_computer_control_forbidden"}}

@@ -199,7 +199,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

199 199
    response =
200 200
      build_conn()
201 201
      |> bearer(box_credential)
202
      |> delete(~p"/api/v3/conversations/#{conversation.id}/delegations/computer-job:#{job.id}")
202
      |> delete(~p"/api/v1/conversations/#{conversation.id}/delegations/computer-job:#{job.id}")
203 203
      |> json_response(403)
204 204
205 205
    assert response == %{"error" => %{"code" => "agent_computer_control_forbidden"}}

@@ -212,7 +212,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

212 212
    response =
213 213
      build_conn()
214 214
      |> bearer(computer_credential)
215
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
215
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
216 216
        "target_id" => "box:#{box.id}",
217 217
        "command" => "must refuse"
218 218
      })

@@ -223,7 +223,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

223 223
    response =
224 224
      build_conn()
225 225
      |> bearer(computer_credential)
226
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
226
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
227 227
      |> json_response(403)
228 228
229 229
    assert response == %{"error" => %{"code" => "agent_box_control_forbidden"}}

@@ -231,7 +231,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

231 231
    response =
232 232
      build_conn()
233 233
      |> bearer(computer_credential)
234
      |> delete(~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
234
      |> delete(~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
235 235
      |> json_response(403)
236 236
237 237
    assert response == %{"error" => %{"code" => "agent_box_control_forbidden"}}

@@ -251,7 +251,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

251 251
    response =
252 252
      conn
253 253
      |> bearer(credential)
254
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
254
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
255 255
        "target_id" => "box:#{box.id}",
256 256
        "command" => "must refuse"
257 257
      })

@@ -262,7 +262,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

262 262
    response =
263 263
      build_conn()
264 264
      |> bearer(credential)
265
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
265
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
266 266
      |> json_response(404)
267 267
268 268
    assert response == %{"error" => %{"code" => "delegation_not_found"}}

@@ -270,7 +270,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

270 270
    response =
271 271
      build_conn()
272 272
      |> bearer(credential)
273
      |> delete(~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
273
      |> delete(~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
274 274
      |> json_response(404)
275 275
276 276
    assert response == %{"error" => %{"code" => "delegation_not_found"}}

@@ -290,7 +290,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

290 290
    response =
291 291
      conn
292 292
      |> bearer(credential)
293
      |> post(~p"/api/v3/conversations/#{conversation.id}/delegations", %{
293
      |> post(~p"/api/v1/conversations/#{conversation.id}/delegations", %{
294 294
        "target_id" => "box:#{box.id}",
295 295
        "command" => "must refuse"
296 296
      })

@@ -301,7 +301,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

301 301
    response =
302 302
      build_conn()
303 303
      |> bearer(credential)
304
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
304
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
305 305
      |> json_response(403)
306 306
307 307
    assert response == %{"error" => %{"code" => "agent_box_control_forbidden"}}

@@ -309,7 +309,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

309 309
    response =
310 310
      build_conn()
311 311
      |> bearer(credential)
312
      |> delete(~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
312
      |> delete(~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
313 313
      |> json_response(403)
314 314
315 315
    assert response == %{"error" => %{"code" => "agent_box_control_forbidden"}}

@@ -334,7 +334,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

334 334
    response =
335 335
      conn
336 336
      |> put_api_token(user, ["box:control", "computer:control"])
337
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
337
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegations/box-run:#{run.id}")
338 338
      |> json_response(200)
339 339
340 340
    assert response["delegation"]["kind"] == "box"

@@ -345,7 +345,7 @@ defmodule OpenAgentsWeb.DelegationsControllerTest do

345 345
    response =
346 346
      build_conn()
347 347
      |> put_api_token(user, ["computer:control"])
348
      |> get(~p"/api/v3/conversations/#{conversation.id}/delegations/computer-job:#{job.id}")
348
      |> get(~p"/api/v1/conversations/#{conversation.id}/delegations/computer-job:#{job.id}")
349 349
      |> json_response(200)
350 350
351 351
    assert response["delegation"]["kind"] == "computer"
test/openagents_web/controllers/deployment_controller_test.exs modified +3 -3

@@ -25,7 +25,7 @@ defmodule OpenAgentsWeb.DeploymentControllerTest do

25 25
      conn: put_deployments_api_token(conn, owner),
26 26
      owner: owner,
27 27
      repository: repository,
28
      path: "/api/v3/repos/#{repository.owner}/#{repository.name}"
28
      path: "/api/v1/repos/#{repository.owner}/#{repository.name}"
29 29
    }
30 30
  end
31 31

@@ -61,7 +61,7 @@ defmodule OpenAgentsWeb.DeploymentControllerTest do

61 61
      response =
62 62
        build_conn()
63 63
        |> put_req_header("authorization", "Bearer " <> plaintext)
64
        |> post("/api/v3/repos/#{other.owner}/#{other.name}/deployments", %{
64
        |> post("/api/v1/repos/#{other.owner}/#{other.name}/deployments", %{
65 65
          "environment" => "production",
66 66
          "commit_sha" => commit_sha(),
67 67
          "artifact_digest" => artifact_digest(),

@@ -222,7 +222,7 @@ defmodule OpenAgentsWeb.DeploymentControllerTest do

222 222
      response =
223 223
        build_conn()
224 224
        |> put_deployments_api_token(other_owner)
225
        |> get("/api/v3/repos/#{other.owner}/#{other.name}/deployments/#{created["id"]}")
225
        |> get("/api/v1/repos/#{other.owner}/#{other.name}/deployments/#{created["id"]}")
226 226
        |> json_response(403)
227 227
228 228
      assert response["error"]["detail"] == "cross_repository"
test/openagents_web/controllers/device_authorization_controller_test.exs modified +17 -17

@@ -5,7 +5,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

5 5
  alias OpenAgents.Repo
6 6
7 7
  test "a pending device authorization is digested and polling is paced", %{conn: conn} do
8
    created = post(conn, ~p"/api/v3/device/authorizations", %{})
8
    created = post(conn, ~p"/api/v1/device/authorizations", %{})
9 9
10 10
    assert %{
11 11
             "device_code" => device_code,

@@ -29,12 +29,12 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

29 29
    refute inspect(authorization) =~ user_code
30 30
31 31
    pending =
32
      post(recycle(conn), ~p"/api/v3/device/authorizations/token", %{device_code: device_code})
32
      post(recycle(conn), ~p"/api/v1/device/authorizations/token", %{device_code: device_code})
33 33
34 34
    assert json_response(pending, 428) == %{"code" => "authorization_pending"}
35 35
36 36
    paced =
37
      post(recycle(conn), ~p"/api/v3/device/authorizations/token", %{device_code: device_code})
37
      post(recycle(conn), ~p"/api/v1/device/authorizations/token", %{device_code: device_code})
38 38
39 39
    assert json_response(paced, 429) == %{"code" => "slow_down"}
40 40
    assert get_resp_header(paced, "cache-control") == ["no-store"]

@@ -43,7 +43,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

43 43
  test "approval returns one PAT exactly once", %{conn: conn} do
44 44
    %{"device_code" => device_code, "user_code" => user_code} =
45 45
      conn
46
      |> post(~p"/api/v3/device/authorizations", %{})
46
      |> post(~p"/api/v1/device/authorizations", %{})
47 47
      |> json_response(201)
48 48
49 49
    user = github_user("device-approval", "device-owner")

@@ -52,7 +52,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

52 52
    claimed =
53 53
      conn
54 54
      |> recycle()
55
      |> post(~p"/api/v3/device/authorizations/token", %{device_code: device_code})
55
      |> post(~p"/api/v1/device/authorizations/token", %{device_code: device_code})
56 56
57 57
    assert %{
58 58
             "access_token" => "oa_pat_" <> _secret,

@@ -66,7 +66,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

66 66
    repeated =
67 67
      conn
68 68
      |> recycle()
69
      |> post(~p"/api/v3/device/authorizations/token", %{device_code: device_code})
69
      |> post(~p"/api/v1/device/authorizations/token", %{device_code: device_code})
70 70
71 71
    assert json_response(repeated, 400) == %{"code" => "access_denied"}
72 72

@@ -78,14 +78,14 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

78 78
79 79
  test "unknown, denied, expired, and claimed codes share one refusal", %{conn: conn} do
80 80
    unknown =
81
      post(conn, ~p"/api/v3/device/authorizations/token", %{device_code: "unknown-device"})
81
      post(conn, ~p"/api/v1/device/authorizations/token", %{device_code: "unknown-device"})
82 82
83 83
    assert json_response(unknown, 400) == %{"code" => "access_denied"}
84 84
85 85
    %{"device_code" => denied_code, "user_code" => user_code} =
86 86
      conn
87 87
      |> recycle()
88
      |> post(~p"/api/v3/device/authorizations", %{})
88
      |> post(~p"/api/v1/device/authorizations", %{})
89 89
      |> json_response(201)
90 90
91 91
    user = github_user("device-denial")

@@ -94,14 +94,14 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

94 94
    denied =
95 95
      conn
96 96
      |> recycle()
97
      |> post(~p"/api/v3/device/authorizations/token", %{device_code: denied_code})
97
      |> post(~p"/api/v1/device/authorizations/token", %{device_code: denied_code})
98 98
99 99
    assert json_response(denied, 400) == %{"code" => "access_denied"}
100 100
101 101
    %{"device_code" => expired_code} =
102 102
      conn
103 103
      |> recycle()
104
      |> post(~p"/api/v3/device/authorizations", %{})
104
      |> post(~p"/api/v1/device/authorizations", %{})
105 105
      |> json_response(201)
106 106
107 107
    DeviceAuthorization

@@ -113,7 +113,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

113 113
    expired =
114 114
      conn
115 115
      |> recycle()
116
      |> post(~p"/api/v3/device/authorizations/token", %{device_code: expired_code})
116
      |> post(~p"/api/v1/device/authorizations/token", %{device_code: expired_code})
117 117
118 118
    assert json_response(expired, 400) == %{"code" => "access_denied"}
119 119
  end

@@ -121,7 +121,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

121 121
  test "a privileged scope can be requested but only an operator may grant it", %{conn: conn} do
122 122
    %{"device_code" => device_code, "user_code" => user_code, "scope" => scope} =
123 123
      conn
124
      |> post(~p"/api/v3/device/authorizations", %{"scope" => "deployments:promote"})
124
      |> post(~p"/api/v1/device/authorizations", %{"scope" => "deployments:promote"})
125 125
      |> json_response(201)
126 126
127 127
    assert scope == "deployments:promote"

@@ -136,7 +136,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

136 136
    claimed =
137 137
      conn
138 138
      |> recycle()
139
      |> post(~p"/api/v3/device/authorizations/token", %{device_code: device_code})
139
      |> post(~p"/api/v1/device/authorizations/token", %{device_code: device_code})
140 140
141 141
    assert %{"scope" => "deployments:promote", "expires_in" => expires_in} =
142 142
             json_response(claimed, 200)

@@ -152,7 +152,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

152 152
  test "a login that names no scope can open a thread", %{conn: conn} do
153 153
    %{"device_code" => device_code, "user_code" => user_code} =
154 154
      conn
155
      |> post(~p"/api/v3/device/authorizations", %{})
155
      |> post(~p"/api/v1/device/authorizations", %{})
156 156
      |> json_response(201)
157 157
158 158
    user = github_user("device-chat", "device-chat-owner")

@@ -161,7 +161,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

161 161
    %{"access_token" => token, "scope" => scope} =
162 162
      conn
163 163
      |> recycle()
164
      |> post(~p"/api/v3/device/authorizations/token", %{device_code: device_code})
164
      |> post(~p"/api/v1/device/authorizations/token", %{device_code: device_code})
165 165
      |> json_response(200)
166 166
167 167
    assert scope == "chat:account forge:write"

@@ -170,7 +170,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

170 170
      conn
171 171
      |> recycle()
172 172
      |> put_req_header("authorization", "Bearer " <> token)
173
      |> post(~p"/api/v3/threads", %{"objective" => "run the coder"})
173
      |> post(~p"/api/v1/threads", %{"objective" => "run the coder"})
174 174
175 175
    assert %{"thread" => %{"id" => _id}, "grant" => %{"token" => _grant, "limits" => limits}} =
176 176
             json_response(opened, 201)

@@ -183,7 +183,7 @@ defmodule OpenAgentsWeb.DeviceAuthorizationControllerTest do

183 183
184 184
  test "an unknown scope is refused rather than silently narrowed", %{conn: conn} do
185 185
    refused =
186
      post(conn, ~p"/api/v3/device/authorizations", %{"scope" => "deployments:everything"})
186
      post(conn, ~p"/api/v1/device/authorizations", %{"scope" => "deployments:everything"})
187 187
188 188
    assert json_response(refused, 400) == %{"code" => "invalid_scope"}
189 189
  end
test/openagents_web/controllers/fleet_target_controller_test.exs modified +2 -2

@@ -18,7 +18,7 @@ defmodule OpenAgentsWeb.FleetTargetControllerTest do

18 18
  alias OpenAgentsWeb.ApiError
19 19
20 20
  @repo "openagents.com"
21
  @create "/api/v3/admin/forge/targets"
21
  @create "/api/v1/admin/forge/targets"
22 22
23 23
  setup do
24 24
    isolate_forge_storage!()

@@ -58,7 +58,7 @@ defmodule OpenAgentsWeb.FleetTargetControllerTest do

58 58
    assert response["replayed"] == false
59 59
    assert response["terminal"] == false
60 60
    assert is_binary(response["request_id"])
61
    assert response["status_url"] =~ "/api/v3/admin/forge/targets/#{response["id"]}"
61
    assert response["status_url"] =~ "/api/v1/admin/forge/targets/#{response["id"]}"
62 62
63 63
    assert Targets.current(@repo).id == response["id"]
64 64
  end
test/openagents_web/controllers/forge_user_controller_test.exs modified +4 -4

@@ -17,7 +17,7 @@ defmodule OpenAgentsWeb.ForgeUserControllerTest do

17 17
    :ok
18 18
  end
19 19
20
  test "GET /api/v3/user returns the GitHub identity and eligible namespaces", %{conn: conn} do
20
  test "GET /api/v1/user returns the GitHub identity and eligible namespaces", %{conn: conn} do
21 21
    user = github_user("forge-user", "octavia")
22 22
23 23
    {:ok, user} =

@@ -39,7 +39,7 @@ defmodule OpenAgentsWeb.ForgeUserControllerTest do

39 39
    response =
40 40
      conn
41 41
      |> put_req_header("authorization", "Bearer " <> plaintext)
42
      |> get(~p"/api/v3/user")
42
      |> get(~p"/api/v1/user")
43 43
44 44
    assert %{
45 45
             "id" => github_id,

@@ -56,10 +56,10 @@ defmodule OpenAgentsWeb.ForgeUserControllerTest do

56 56
    assert get_resp_header(response, "cache-control") == ["no-store"]
57 57
  end
58 58
59
  test "GET /api/v3/user requires a bearer token", %{conn: conn} do
59
  test "GET /api/v1/user requires a bearer token", %{conn: conn} do
60 60
    assert %{"error" => "invalid_api_token"} =
61 61
             conn
62
             |> get(~p"/api/v3/user")
62
             |> get(~p"/api/v1/user")
63 63
             |> json_response(401)
64 64
  end
65 65
end
test/openagents_web/controllers/forum_api_controller_test.exs modified +55 -55

@@ -36,18 +36,18 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

36 36
    topic
37 37
  end
38 38
39
  test "GET /api/v3/forum lists public boards", %{conn: conn} do
40
    conn = get(conn, ~p"/api/v3/forum")
39
  test "GET /api/v1/forum lists public boards", %{conn: conn} do
40
    conn = get(conn, ~p"/api/v1/forum")
41 41
42 42
    assert %{"boards" => [board]} = json_response(conn, 200)
43 43
    assert board["slug"] == "general"
44 44
    assert board["title"] == "General"
45 45
  end
46 46
47
  test "GET /api/v3/forum/topics lists a board's topics", %{conn: conn, forum: forum} do
47
  test "GET /api/v1/forum/topics lists a board's topics", %{conn: conn, forum: forum} do
48 48
    topic(forum)
49 49
50
    conn = get(conn, ~p"/api/v3/forum/topics?forum=general")
50
    conn = get(conn, ~p"/api/v1/forum/topics?forum=general")
51 51
52 52
    assert %{"topics" => [t], "pagination" => pagination} = json_response(conn, 200)
53 53
    assert t["title"] == "Hello world"

@@ -55,21 +55,21 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

55 55
    assert pagination["total"] == 1
56 56
  end
57 57
58
  test "GET /api/v3/forum/topics/:id returns the thread with posts", %{conn: conn, forum: forum} do
58
  test "GET /api/v1/forum/topics/:id returns the thread with posts", %{conn: conn, forum: forum} do
59 59
    topic = topic(forum)
60 60
61
    conn = get(conn, ~p"/api/v3/forum/topics/#{topic.id}")
61
    conn = get(conn, ~p"/api/v1/forum/topics/#{topic.id}")
62 62
63 63
    assert %{"topic" => t, "posts" => posts} = json_response(conn, 200)
64 64
    assert t["title"] == "Hello world"
65 65
    assert [%{"body_text" => "First post body"}] = posts
66 66
  end
67 67
68
  test "POST /api/v3/forum/topics creates a topic attributed to the token account", %{conn: conn} do
68
  test "POST /api/v1/forum/topics creates a topic attributed to the token account", %{conn: conn} do
69 69
    conn =
70 70
      conn
71 71
      |> put_forge_api_token("forum-api-create")
72
      |> post(~p"/api/v3/forum/topics", %{
72
      |> post(~p"/api/v1/forum/topics", %{
73 73
        forum: "general",
74 74
        title: "From the CLI",
75 75
        body_text: "Posted over the API"

@@ -80,8 +80,8 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

80 80
    assert hd(response["posts"])["author"]["is_agent"] == false
81 81
  end
82 82
83
  test "POST /api/v3/forum/topics requires authentication", %{conn: conn} do
84
    conn = post(conn, ~p"/api/v3/forum/topics", %{forum: "general", title: "x", body_text: "y"})
83
  test "POST /api/v1/forum/topics requires authentication", %{conn: conn} do
84
    conn = post(conn, ~p"/api/v1/forum/topics", %{forum: "general", title: "x", body_text: "y"})
85 85
86 86
    assert json_response(conn, 401)
87 87
  end

@@ -100,7 +100,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

100 100
    topic_conn =
101 101
      conn
102 102
      |> put_req_header("authorization", "Bearer #{credential}")
103
      |> post(~p"/api/v3/forum/topics", %{
103
      |> post(~p"/api/v1/forum/topics", %{
104 104
        forum: forum.slug,
105 105
        title: "Agent topic",
106 106
        body_text: "Created without a human link"

@@ -118,7 +118,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

118 118
    reply_conn =
119 119
      conn
120 120
      |> put_req_header("authorization", "Bearer #{credential}")
121
      |> post(~p"/api/v3/forum/topics/#{topic_id}/posts", %{
121
      |> post(~p"/api/v1/forum/topics/#{topic_id}/posts", %{
122 122
        body_text: "Agent reply"
123 123
      })
124 124

@@ -126,13 +126,13 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

126 126
             json_response(reply_conn, 201)
127 127
  end
128 128
129
  test "POST /api/v3/forum/topics/:id/posts replies to a topic", %{conn: conn, forum: forum} do
129
  test "POST /api/v1/forum/topics/:id/posts replies to a topic", %{conn: conn, forum: forum} do
130 130
    topic = topic(forum)
131 131
132 132
    conn =
133 133
      conn
134 134
      |> put_forge_api_token("forum-api-reply")
135
      |> post(~p"/api/v3/forum/topics/#{topic.id}/posts", %{body_text: "API reply"})
135
      |> post(~p"/api/v1/forum/topics/#{topic.id}/posts", %{body_text: "API reply"})
136 136
137 137
    assert %{"post" => post} = json_response(conn, 201)
138 138
    assert post["post_number"] == 2

@@ -163,7 +163,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

163 163
    conn =
164 164
      conn
165 165
      |> put_req_header("authorization", "Bearer #{credential}")
166
      |> post(~p"/api/v3/forum/topics/#{topic.id}/posts", %{body_text: "Should fail"})
166
      |> post(~p"/api/v1/forum/topics/#{topic.id}/posts", %{body_text: "Should fail"})
167 167
168 168
    assert conn.status == 401
169 169
  end

@@ -186,41 +186,41 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

186 186
    created =
187 187
      conn
188 188
      |> put_req_header("authorization", "Bearer #{credential}")
189
      |> post(~p"/api/v3/forum/topics", %{
189
      |> post(~p"/api/v1/forum/topics", %{
190 190
        forum: forum.slug,
191 191
        title: "Stable topic",
192 192
        body_text: "Authorship must remain stable"
193 193
      })
194 194
195 195
    assert %{"topic" => %{"id" => topic_id}} = json_response(created, 201)
196
    before = get(conn, ~p"/api/v3/forum/topics/#{topic_id}") |> json_response(200)
196
    before = get(conn, ~p"/api/v1/forum/topics/#{topic_id}") |> json_response(200)
197 197
198 198
    assert {:ok, pending} = Agents.request_link(agent, user)
199 199
    assert {:ok, _linked} = Agents.accept_link(user, pending.id)
200 200
    assert {:ok, _unlinked} = Agents.unlink(agent, user)
201 201
202
    after_link = get(conn, ~p"/api/v3/forum/topics/#{topic_id}") |> json_response(200)
202
    after_link = get(conn, ~p"/api/v1/forum/topics/#{topic_id}") |> json_response(200)
203 203
    assert before["topic"]["actor_ref"] == after_link["topic"]["actor_ref"]
204 204
    assert before["topic"]["actor_display_name"] == after_link["topic"]["actor_display_name"]
205 205
    assert before["topic"]["actor_slug"] == after_link["topic"]["actor_slug"]
206 206
    assert before["posts"] == after_link["posts"]
207 207
208
    profile = get(conn, ~p"/api/v3/agents/#{agent.handle}") |> json_response(200)
208
    profile = get(conn, ~p"/api/v1/agents/#{agent.handle}") |> json_response(200)
209 209
    refute Map.has_key?(profile["agent"], "owner")
210 210
  end
211 211
212
  test "POST /api/v3/forum/claims starts an identity claim", %{conn: conn} do
212
  test "POST /api/v1/forum/claims starts an identity claim", %{conn: conn} do
213 213
    conn =
214 214
      conn
215 215
      |> put_forge_api_token("forum-api-claim")
216
      |> post(~p"/api/v3/forum/claims", %{
216
      |> post(~p"/api/v1/forum/claims", %{
217 217
        actor_ref: "agent:user_ed8297d8-1279-4b43-a1e7-f7867da19e20"
218 218
      })
219 219
220 220
    assert %{"claim" => %{"status" => "pending"}} = json_response(conn, 201)
221 221
  end
222 222
223
  test "GET /api/v3/forum/claims lists the caller's claims", %{conn: conn} do
223
  test "GET /api/v1/forum/claims lists the caller's claims", %{conn: conn} do
224 224
    authed = put_forge_api_token(conn, "forum-api-list-claims")
225 225
226 226
    {:ok, _} =

@@ -228,14 +228,14 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

228 228
229 229
    _ = authed
230 230
231
    conn = get(authed, ~p"/api/v3/forum/claims")
231
    conn = get(authed, ~p"/api/v1/forum/claims")
232 232
233 233
    assert %{"claims" => claims} = json_response(conn, 200)
234 234
    assert length(claims) >= 1
235 235
  end
236 236
237 237
  test "unknown board is 404", %{conn: conn} do
238
    conn = get(conn, ~p"/api/v3/forum/topics?forum=nope")
238
    conn = get(conn, ~p"/api/v1/forum/topics?forum=nope")
239 239
240 240
    assert json_response(conn, 404)
241 241
  end

@@ -245,7 +245,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

245 245
      topic(forum, %{title: "Router latency", slug: "router-latency"})
246 246
      topic(forum, %{title: "Something else", slug: "something-else"})
247 247
248
      conn = get(conn, ~p"/api/v3/forum/topics?q=latency")
248
      conn = get(conn, ~p"/api/v1/forum/topics?q=latency")
249 249
250 250
      assert %{"query" => "latency", "topics" => [t], "board" => nil} = json_response(conn, 200)
251 251
      assert t["title"] == "Router latency"

@@ -255,7 +255,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

255 255
    test "matches a visible post body", %{conn: conn, forum: forum} do
256 256
      topic(forum, %{title: "Deploy notes", slug: "deploy-notes", body_text: "watch the walrus"})
257 257
258
      conn = get(conn, ~p"/api/v3/forum/topics?q=walrus")
258
      conn = get(conn, ~p"/api/v1/forum/topics?q=walrus")
259 259
260 260
      assert %{"topics" => [t]} = json_response(conn, 200)
261 261
      assert t["title"] == "Deploy notes"

@@ -270,7 +270,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

270 270
      topic(forum, %{title: "Shared word here", slug: "shared-general"})
271 271
      topic(other, %{title: "Shared word there", slug: "shared-meta"})
272 272
273
      conn = get(conn, ~p"/api/v3/forum/topics?forum=meta&q=shared")
273
      conn = get(conn, ~p"/api/v1/forum/topics?forum=meta&q=shared")
274 274
275 275
      assert %{"topics" => [t], "board" => board} = json_response(conn, 200)
276 276
      assert board["slug"] == "meta"

@@ -281,7 +281,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

281 281
      private = private_forum()
282 282
      topic(private, %{title: "Private plan", slug: "private-plan"})
283 283
284
      conn = get(conn, ~p"/api/v3/forum/topics?q=private")
284
      conn = get(conn, ~p"/api/v1/forum/topics?q=private")
285 285
286 286
      assert %{"topics" => []} = json_response(conn, 200)
287 287
    end

@@ -293,7 +293,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

293 293
      conn =
294 294
        conn
295 295
        |> operator_token("forum-api-search-operator")
296
        |> get(~p"/api/v3/forum/topics?q=private")
296
        |> get(~p"/api/v1/forum/topics?q=private")
297 297
298 298
      assert %{"topics" => [t]} = json_response(conn, 200)
299 299
      assert t["title"] == "Private plan"

@@ -301,19 +301,19 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

301 301
  end
302 302
303 303
  describe "authorization of reads" do
304
    test "GET /api/v3/forum omits a private board", %{conn: conn} do
304
    test "GET /api/v1/forum omits a private board", %{conn: conn} do
305 305
      private_forum()
306 306
307
      conn = get(conn, ~p"/api/v3/forum")
307
      conn = get(conn, ~p"/api/v1/forum")
308 308
309 309
      assert %{"boards" => boards} = json_response(conn, 200)
310 310
      assert Enum.map(boards, & &1["slug"]) == ["general"]
311 311
    end
312 312
313
    test "GET /api/v3/forum lists a private board for an operator", %{conn: conn} do
313
    test "GET /api/v1/forum lists a private board for an operator", %{conn: conn} do
314 314
      private_forum()
315 315
316
      conn = conn |> operator_token("forum-api-boards-operator") |> get(~p"/api/v3/forum")
316
      conn = conn |> operator_token("forum-api-boards-operator") |> get(~p"/api/v1/forum")
317 317
318 318
      assert %{"boards" => boards} = json_response(conn, 200)
319 319
      assert "private" in Enum.map(boards, & &1["slug"])

@@ -322,7 +322,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

322 322
    test "a private board's topic list is 404 for an anonymous caller", %{conn: conn} do
323 323
      private_forum()
324 324
325
      conn = get(conn, ~p"/api/v3/forum/topics?forum=private")
325
      conn = get(conn, ~p"/api/v1/forum/topics?forum=private")
326 326
327 327
      assert json_response(conn, 404)
328 328
    end

@@ -330,7 +330,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

330 330
    test "a private board's thread is 404 for an anonymous caller", %{conn: conn} do
331 331
      topic = topic(private_forum(), %{title: "Private plan", slug: "private-plan"})
332 332
333
      conn = get(conn, ~p"/api/v3/forum/topics/#{topic.id}")
333
      conn = get(conn, ~p"/api/v1/forum/topics/#{topic.id}")
334 334
335 335
      assert json_response(conn, 404)
336 336
    end

@@ -340,75 +340,75 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

340 340
      [first] = Forum.list_posts(topic)
341 341
      {:ok, _hidden} = Forum.hide_post(first)
342 342
343
      conn = get(conn, ~p"/api/v3/forum/topics/#{topic.id}")
343
      conn = get(conn, ~p"/api/v1/forum/topics/#{topic.id}")
344 344
345 345
      assert %{"posts" => []} = json_response(conn, 200)
346 346
    end
347 347
348 348
    test "a malformed topic identifier is 404", %{conn: conn} do
349
      conn = get(conn, ~p"/api/v3/forum/topics/not-a-uuid")
349
      conn = get(conn, ~p"/api/v1/forum/topics/not-a-uuid")
350 350
351 351
      assert json_response(conn, 404)
352 352
    end
353 353
  end
354 354
355 355
  describe "moderation" do
356
    test "PATCH /api/v3/forum/topics/:id closes and pins a topic", %{conn: conn, forum: forum} do
356
    test "PATCH /api/v1/forum/topics/:id closes and pins a topic", %{conn: conn, forum: forum} do
357 357
      topic = topic(forum)
358 358
359 359
      conn =
360 360
        conn
361 361
        |> operator_token("forum-api-close")
362
        |> patch(~p"/api/v3/forum/topics/#{topic.id}", %{state: "closed", pinned: true})
362
        |> patch(~p"/api/v1/forum/topics/#{topic.id}", %{state: "closed", pinned: true})
363 363
364 364
      assert %{"topic" => t} = json_response(conn, 200)
365 365
      assert t["state"] == "closed"
366 366
      assert t["pinned"] == true
367 367
    end
368 368
369
    test "PATCH /api/v3/forum/topics/:id refuses a non-operator", %{conn: conn, forum: forum} do
369
    test "PATCH /api/v1/forum/topics/:id refuses a non-operator", %{conn: conn, forum: forum} do
370 370
      topic = topic(forum)
371 371
372 372
      conn =
373 373
        conn
374 374
        |> put_forge_api_token("forum-api-close-denied")
375
        |> patch(~p"/api/v3/forum/topics/#{topic.id}", %{state: "closed"})
375
        |> patch(~p"/api/v1/forum/topics/#{topic.id}", %{state: "closed"})
376 376
377 377
      assert json_response(conn, 403)
378 378
    end
379 379
380
    test "PATCH /api/v3/forum/topics/:id rejects an unknown state", %{conn: conn, forum: forum} do
380
    test "PATCH /api/v1/forum/topics/:id rejects an unknown state", %{conn: conn, forum: forum} do
381 381
      topic = topic(forum)
382 382
383 383
      conn =
384 384
        conn
385 385
        |> operator_token("forum-api-bad-state")
386
        |> patch(~p"/api/v3/forum/topics/#{topic.id}", %{state: "melted"})
386
        |> patch(~p"/api/v1/forum/topics/#{topic.id}", %{state: "melted"})
387 387
388 388
      assert %{"errors" => %{"state" => [_message]}} = json_response(conn, 422)
389 389
    end
390 390
391
    test "PATCH /api/v3/forum/posts/:id hides a post", %{conn: conn, forum: forum} do
391
    test "PATCH /api/v1/forum/posts/:id hides a post", %{conn: conn, forum: forum} do
392 392
      topic = topic(forum)
393 393
      [first] = Forum.list_posts(topic)
394 394
395 395
      conn =
396 396
        conn
397 397
        |> operator_token("forum-api-hide")
398
        |> patch(~p"/api/v3/forum/posts/#{first.id}", %{state: "hidden"})
398
        |> patch(~p"/api/v1/forum/posts/#{first.id}", %{state: "hidden"})
399 399
400 400
      assert %{"post" => %{"state" => "hidden"}} = json_response(conn, 200)
401 401
      assert Forum.list_posts(topic) == []
402 402
    end
403 403
404
    test "PATCH /api/v3/forum/posts/:id refuses a non-operator", %{conn: conn, forum: forum} do
404
    test "PATCH /api/v1/forum/posts/:id refuses a non-operator", %{conn: conn, forum: forum} do
405 405
      topic = topic(forum)
406 406
      [first] = Forum.list_posts(topic)
407 407
408 408
      conn =
409 409
        conn
410 410
        |> put_forge_api_token("forum-api-hide-denied")
411
        |> patch(~p"/api/v3/forum/posts/#{first.id}", %{state: "hidden"})
411
        |> patch(~p"/api/v1/forum/posts/#{first.id}", %{state: "hidden"})
412 412
413 413
      assert json_response(conn, 403)
414 414
      assert [_post] = Forum.list_posts(topic)

@@ -416,37 +416,37 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

416 416
  end
417 417
418 418
  describe "claim review" do
419
    test "GET /api/v3/forum/claims/pending lists every pending claim", %{conn: conn} do
419
    test "GET /api/v1/forum/claims/pending lists every pending claim", %{conn: conn} do
420 420
      {:ok, _claim} =
421 421
        Forum.start_actor_link(github_user("forum-api-pending-claimant"), "agent:user_2")
422 422
423
      conn = conn |> operator_token("forum-api-pending") |> get(~p"/api/v3/forum/claims/pending")
423
      conn = conn |> operator_token("forum-api-pending") |> get(~p"/api/v1/forum/claims/pending")
424 424
425 425
      assert %{"claims" => [%{"actor_ref" => "agent:user_2"}]} = json_response(conn, 200)
426 426
    end
427 427
428
    test "GET /api/v3/forum/claims/pending refuses a non-operator", %{conn: conn} do
428
    test "GET /api/v1/forum/claims/pending refuses a non-operator", %{conn: conn} do
429 429
      conn =
430 430
        conn
431 431
        |> put_forge_api_token("forum-api-pending-denied")
432
        |> get(~p"/api/v3/forum/claims/pending")
432
        |> get(~p"/api/v1/forum/claims/pending")
433 433
434 434
      assert json_response(conn, 403)
435 435
    end
436 436
437
    test "PATCH /api/v3/forum/claims/:id links a claim", %{conn: conn} do
437
    test "PATCH /api/v1/forum/claims/:id links a claim", %{conn: conn} do
438 438
      {:ok, claim} =
439 439
        Forum.start_actor_link(github_user("forum-api-review-claimant"), "agent:user_3")
440 440
441 441
      conn =
442 442
        conn
443 443
        |> operator_token("forum-api-review")
444
        |> patch(~p"/api/v3/forum/claims/#{claim.id}", %{status: "linked"})
444
        |> patch(~p"/api/v1/forum/claims/#{claim.id}", %{status: "linked"})
445 445
446 446
      assert %{"claim" => %{"status" => "linked"}} = json_response(conn, 200)
447 447
    end
448 448
449
    test "PATCH /api/v3/forum/claims/:id is a conflict once a claim is settled", %{conn: conn} do
449
    test "PATCH /api/v1/forum/claims/:id is a conflict once a claim is settled", %{conn: conn} do
450 450
      {:ok, claim} =
451 451
        Forum.start_actor_link(github_user("forum-api-settled-claimant"), "agent:user_4")
452 452

@@ -455,7 +455,7 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

455 455
      conn =
456 456
        conn
457 457
        |> operator_token("forum-api-settled")
458
        |> patch(~p"/api/v3/forum/claims/#{claim.id}", %{status: "linked"})
458
        |> patch(~p"/api/v1/forum/claims/#{claim.id}", %{status: "linked"})
459 459
460 460
      assert %{"error" => "claim_not_pending"} = json_response(conn, 409)
461 461
    end
test/openagents_web/controllers/forum_tips_api_controller_test.exs modified +18 -18

@@ -55,7 +55,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

55 55
      authed = put_forge_api_token(conn, "forum-tips-destination")
56 56
57 57
      created =
58
        post(authed, ~p"/api/v3/forum/tips/destination", %{
58
        post(authed, ~p"/api/v1/forum/tips/destination", %{
59 59
          kind: "bolt12",
60 60
          destination: "lno1qsgpayer",
61 61
          label: "Phone wallet"

@@ -71,7 +71,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

71 71
      read =
72 72
        conn
73 73
        |> put_forge_api_token("forum-tips-destination")
74
        |> get(~p"/api/v3/forum/tips/destination")
74
        |> get(~p"/api/v1/forum/tips/destination")
75 75
76 76
      assert %{"destination" => same} = json_response(read, 200)
77 77
      assert same["fingerprint"] == destination["fingerprint"]

@@ -81,7 +81,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

81 81
    test "a caller opts out of tips without deleting the destination", %{conn: conn} do
82 82
      authed = put_forge_api_token(conn, "forum-tips-optout")
83 83
84
      post(authed, ~p"/api/v3/forum/tips/destination", %{
84
      post(authed, ~p"/api/v1/forum/tips/destination", %{
85 85
        kind: "lnurl",
86 86
        destination: "lnurl1dp68gurn8ghj7"
87 87
      })

@@ -89,7 +89,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

89 89
      updated =
90 90
        patch(
91 91
          put_forge_api_token(conn, "forum-tips-optout"),
92
          ~p"/api/v3/forum/tips/destination",
92
          ~p"/api/v1/forum/tips/destination",
93 93
          %{accepting_tips: false}
94 94
        )
95 95

@@ -99,10 +99,10 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

99 99
    end
100 100
101 101
    test "the destination endpoints need a bearer token", %{conn: conn} do
102
      assert json_response(get(conn, ~p"/api/v3/forum/tips/destination"), 401)
102
      assert json_response(get(conn, ~p"/api/v1/forum/tips/destination"), 401)
103 103
104 104
      assert json_response(
105
               post(conn, ~p"/api/v3/forum/tips/destination", %{
105
               post(conn, ~p"/api/v1/forum/tips/destination", %{
106 106
                 kind: "bolt12",
107 107
                 destination: "lno1x"
108 108
               }),

@@ -118,7 +118,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

118 118
      created =
119 119
        conn
120 120
        |> put_forge_api_token("forum-tips-payer")
121
        |> post(~p"/api/v3/forum/posts/#{author.post.id}/tips", %{
121
        |> post(~p"/api/v1/forum/posts/#{author.post.id}/tips", %{
122 122
          amount_sats: 1_000,
123 123
          idempotency_key: Ecto.UUID.generate()
124 124
        })

@@ -139,7 +139,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

139 139
      first =
140 140
        conn
141 141
        |> put_forge_api_token("forum-tips-retry")
142
        |> post(~p"/api/v3/forum/posts/#{author.post.id}/tips", %{
142
        |> post(~p"/api/v1/forum/posts/#{author.post.id}/tips", %{
143 143
          amount_sats: 400,
144 144
          idempotency_key: key
145 145
        })

@@ -147,7 +147,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

147 147
      second =
148 148
        conn
149 149
        |> put_forge_api_token("forum-tips-retry")
150
        |> post(~p"/api/v3/forum/posts/#{author.post.id}/tips", %{
150
        |> post(~p"/api/v1/forum/posts/#{author.post.id}/tips", %{
151 151
          amount_sats: 400,
152 152
          idempotency_key: key
153 153
        })

@@ -164,7 +164,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

164 164
      created =
165 165
        conn
166 166
        |> put_forge_api_token("forum-tips-failed")
167
        |> post(~p"/api/v3/forum/posts/#{author.post.id}/tips", %{
167
        |> post(~p"/api/v1/forum/posts/#{author.post.id}/tips", %{
168 168
          amount_sats: 100,
169 169
          idempotency_key: Ecto.UUID.generate()
170 170
        })

@@ -186,14 +186,14 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

186 186
      created =
187 187
        conn
188 188
        |> put_forge_api_token("forum-tips-outage")
189
        |> post(~p"/api/v3/forum/posts/#{author.post.id}/tips", %{
189
        |> post(~p"/api/v1/forum/posts/#{author.post.id}/tips", %{
190 190
          amount_sats: 100,
191 191
          idempotency_key: Ecto.UUID.generate()
192 192
        })
193 193
194 194
      assert json_response(created, 503)["error"]
195 195
196
      listed = get(conn, ~p"/api/v3/forum/topics?forum=general")
196
      listed = get(conn, ~p"/api/v1/forum/topics?forum=general")
197 197
      assert [topic] = json_response(listed, 200)["topics"]
198 198
      assert topic["tip_sats"] == 0
199 199
    end

@@ -202,7 +202,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

202 202
      author = author(forum)
203 203
204 204
      assert json_response(
205
               post(conn, ~p"/api/v3/forum/posts/#{author.post.id}/tips", %{amount_sats: 100}),
205
               post(conn, ~p"/api/v1/forum/posts/#{author.post.id}/tips", %{amount_sats: 100}),
206 206
               401
207 207
             )
208 208
    end

@@ -211,7 +211,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

211 211
      created =
212 212
        conn
213 213
        |> put_forge_api_token("forum-tips-missing")
214
        |> post(~p"/api/v3/forum/posts/#{Ecto.UUID.generate()}/tips", %{
214
        |> post(~p"/api/v1/forum/posts/#{Ecto.UUID.generate()}/tips", %{
215 215
          amount_sats: 100,
216 216
          idempotency_key: Ecto.UUID.generate()
217 217
        })

@@ -229,7 +229,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

229 229
230 230
      conn
231 231
      |> put_forge_api_token("forum-tips-recipient-payer")
232
      |> post(~p"/api/v3/forum/posts/#{author.post.id}/tips", %{
232
      |> post(~p"/api/v1/forum/posts/#{author.post.id}/tips", %{
233 233
        amount_sats: 900,
234 234
        idempotency_key: Ecto.UUID.generate()
235 235
      })

@@ -238,7 +238,7 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

238 238
      listed =
239 239
        conn
240 240
        |> put_forge_api_token("forum-tips-author")
241
        |> get(~p"/api/v3/forum/tips/received")
241
        |> get(~p"/api/v1/forum/tips/received")
242 242
243 243
      export = json_response(listed, 200)
244 244
      assert export["custody"] == "self"

@@ -256,13 +256,13 @@ defmodule OpenAgentsWeb.ForumTipsApiControllerTest do

256 256
257 257
      conn
258 258
      |> put_forge_api_token("forum-tips-public")
259
      |> post(~p"/api/v3/forum/posts/#{author.post.id}/tips", %{
259
      |> post(~p"/api/v1/forum/posts/#{author.post.id}/tips", %{
260 260
        amount_sats: 1_500,
261 261
        idempotency_key: Ecto.UUID.generate()
262 262
      })
263 263
      |> json_response(201)
264 264
265
      thread = get(conn, ~p"/api/v3/forum/topics/#{author.topic.id}")
265
      thread = get(conn, ~p"/api/v1/forum/topics/#{author.topic.id}")
266 266
      body = response(thread, 200)
267 267
268 268
      assert %{"topic" => topic, "posts" => [post]} = Jason.decode!(body)
test/openagents_web/controllers/gym_run_controller_test.exs modified +8 -8

@@ -37,7 +37,7 @@ defmodule OpenAgentsWeb.GymRunControllerTest do

37 37
38 38
    created =
39 39
      authenticated
40
      |> post(~p"/api/v3/gym/runs", payload())
40
      |> post(~p"/api/v1/gym/runs", payload())
41 41
      |> json_response(201)
42 42
43 43
    assert created["run"]["score"] == 0.7

@@ -45,7 +45,7 @@ defmodule OpenAgentsWeb.GymRunControllerTest do

45 45
46 46
    replayed =
47 47
      authenticated
48
      |> post(~p"/api/v3/gym/runs", payload(%{"tasks_passed" => 1}))
48
      |> post(~p"/api/v1/gym/runs", payload(%{"tasks_passed" => 1}))
49 49
      |> json_response(200)
50 50
51 51
    assert replayed["replayed"] == true

@@ -57,7 +57,7 @@ defmodule OpenAgentsWeb.GymRunControllerTest do

57 57
    refused =
58 58
      conn
59 59
      |> put_forge_api_token("gym-ordinary")
60
      |> post(~p"/api/v3/gym/runs", payload())
60
      |> post(~p"/api/v1/gym/runs", payload())
61 61
      |> json_response(403)
62 62
63 63
    assert refused["code"] == "not_operator"

@@ -68,7 +68,7 @@ defmodule OpenAgentsWeb.GymRunControllerTest do

68 68
    refused =
69 69
      conn
70 70
      |> operator_token("gym-invalid")
71
      |> post(~p"/api/v3/gym/runs", payload(%{"tasks_passed" => 99}))
71
      |> post(~p"/api/v1/gym/runs", payload(%{"tasks_passed" => 99}))
72 72
      |> json_response(422)
73 73
74 74
    assert refused["errors"]["tasks_passed"]

@@ -78,14 +78,14 @@ defmodule OpenAgentsWeb.GymRunControllerTest do

78 78
    authenticated = operator_token(conn, "gym-lister")
79 79
80 80
    _created =
81
      authenticated |> post(~p"/api/v3/gym/runs", payload()) |> json_response(201)
81
      authenticated |> post(~p"/api/v1/gym/runs", payload()) |> json_response(201)
82 82
83
    listed = authenticated |> get(~p"/api/v3/gym/runs") |> json_response(200)
83
    listed = authenticated |> get(~p"/api/v1/gym/runs") |> json_response(200)
84 84
    assert [%{"suite" => "terminal-bench@2.0"}] = listed["runs"]
85 85
86 86
    filtered =
87 87
      authenticated
88
      |> get(~p"/api/v3/gym/runs?suite=swebench@lite")
88
      |> get(~p"/api/v1/gym/runs?suite=swebench@lite")
89 89
      |> json_response(200)
90 90
91 91
    assert filtered["runs"] == []

@@ -93,7 +93,7 @@ defmodule OpenAgentsWeb.GymRunControllerTest do

93 93
    refused =
94 94
      conn
95 95
      |> put_forge_api_token("gym-list-ordinary")
96
      |> get(~p"/api/v3/gym/runs")
96
      |> get(~p"/api/v1/gym/runs")
97 97
      |> json_response(403)
98 98
99 99
    assert refused["code"] == "not_operator"
test/openagents_web/controllers/issue_assignee_controller_test.exs modified +11 -11

@@ -22,7 +22,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

22 22
      issue: issue
23 23
    } do
24 24
      conn =
25
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees")
25
        get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees")
26 26
27 27
      assert json_response(conn, 200) == %{"assignees" => []}
28 28
    end

@@ -34,14 +34,14 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

34 34
      {:ok, _issue} = Issues.add_assignees(issue, ["octocat"])
35 35
36 36
      conn =
37
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees")
37
        get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees")
38 38
39 39
      assert %{"assignees" => [assignee]} = json_response(conn, 200)
40 40
      assert assignee["login"] == "octocat"
41 41
    end
42 42
43 43
    test "GET .../issues/:issue_number/assignees returns 404 for a missing issue", %{conn: conn} do
44
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999/assignees")
44
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/assignees")
45 45
46 46
      assert_api_error(conn, 404, "not_found")
47 47
    end

@@ -52,7 +52,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

52 52
      conn =
53 53
        post(
54 54
          conn,
55
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
55
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
56 56
          %{
57 57
            assignees: ["octocat"]
58 58
          }

@@ -71,7 +71,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

71 71
      conn =
72 72
        post(
73 73
          conn,
74
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
74
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
75 75
          %{
76 76
            assignees: ["octocat", "hubot"]
77 77
          }

@@ -90,7 +90,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

90 90
      conn =
91 91
        post(
92 92
          conn,
93
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
93
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
94 94
          %{
95 95
            assignees: ["octocat"]
96 96
          }

@@ -104,7 +104,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

104 104
      conn =
105 105
        post(
106 106
          conn,
107
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
107
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
108 108
          %{}
109 109
        )
110 110

@@ -113,7 +113,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

113 113
114 114
    test "POST .../issues/:issue_number/assignees returns 404 for a missing issue", %{conn: conn} do
115 115
      conn =
116
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999/assignees", %{
116
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/assignees", %{
117 117
          assignees: ["octocat"]
118 118
        })
119 119

@@ -131,7 +131,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

131 131
      conn =
132 132
        delete(
133 133
          conn,
134
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
134
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
135 135
          %{
136 136
            assignees: ["octocat"]
137 137
          }

@@ -152,7 +152,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

152 152
      conn =
153 153
        delete(
154 154
          conn,
155
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
155
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/assignees",
156 156
          %{
157 157
            assignees: ["nobody"]
158 158
          }

@@ -165,7 +165,7 @@ defmodule OpenAgentsWeb.IssueAssigneeControllerTest do

165 165
      conn: conn
166 166
    } do
167 167
      conn =
168
        delete(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999/assignees", %{
168
        delete(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/assignees", %{
169 169
          assignees: ["octocat"]
170 170
        })
171 171
test/openagents_web/controllers/issue_capture_controller_test.exs modified +9 -9

@@ -1,6 +1,6 @@

1 1
defmodule OpenAgentsWeb.IssueCaptureControllerTest do
2 2
  @moduledoc """
3
  `POST /api/v3/repos/:owner/:repo/issues/capture` (#77).
3
  `POST /api/v1/repos/:owner/:repo/issues/capture` (#77).
4 4
5 5
  The API operation and the `capture_issue` chat tool are two transports over
6 6
  `OpenAgents.Issues.Capture`, so what this file proves is that the transport

@@ -21,7 +21,7 @@ defmodule OpenAgentsWeb.IssueCaptureControllerTest do

21 21
22 22
    test "POST .../issues/capture files a drafted issue and answers 201", %{conn: conn} do
23 23
      conn =
24
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/capture", %{
24
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/capture", %{
25 25
          problem: "Let me export a board to CSV.",
26 26
          current_behavior: "The board only renders on screen.",
27 27
          acceptance_criteria: ["A CSV downloads"]

@@ -44,14 +44,14 @@ defmodule OpenAgentsWeb.IssueCaptureControllerTest do

44 44
    # client can tell the two apart without a body key of ours.
45 45
    test "a repeat answers 200 with the same issue", %{conn: conn} do
46 46
      created =
47
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/capture", %{
47
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/capture", %{
48 48
          problem: "Let me export a board to CSV."
49 49
        })
50 50
51 51
      assert %{"number" => number} = json_response(created, 201)
52 52
53 53
      repeated =
54
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/capture", %{
54
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/capture", %{
55 55
          problem: "let me export a board to CSV!"
56 56
        })
57 57

@@ -61,7 +61,7 @@ defmodule OpenAgentsWeb.IssueCaptureControllerTest do

61 61
62 62
    test "a blank statement is a validation failure, not a filed issue", %{conn: conn} do
63 63
      conn =
64
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/capture", %{problem: "  "})
64
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/capture", %{problem: "  "})
65 65
66 66
      assert %{"code" => "validation_failed", "errors" => errors} = json_response(conn, 422)
67 67
      assert errors["problem"] == ["can't be blank"]

@@ -76,7 +76,7 @@ defmodule OpenAgentsWeb.IssueCaptureControllerTest do

76 76
      conn = put_forge_api_token(conn, "issue-capture-reader")
77 77
78 78
      conn =
79
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/capture", %{
79
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/capture", %{
80 80
          problem: "Let me export a board to CSV."
81 81
        })
82 82

@@ -96,7 +96,7 @@ defmodule OpenAgentsWeb.IssueCaptureControllerTest do

96 96
      conn = put_forge_api_token(conn, "issue-capture-private-stranger")
97 97
98 98
      conn =
99
        post(conn, ~p"/api/v3/repos/#{private.owner}/#{private.name}/issues/capture", %{
99
        post(conn, ~p"/api/v1/repos/#{private.owner}/#{private.name}/issues/capture", %{
100 100
          problem: "Let me export a board to CSV."
101 101
        })
102 102

@@ -105,7 +105,7 @@ defmodule OpenAgentsWeb.IssueCaptureControllerTest do

105 105
106 106
    test "an anonymous caller is refused before the controller runs", %{conn: conn} do
107 107
      conn =
108
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/capture", %{
108
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/capture", %{
109 109
          problem: "Let me export a board to CSV."
110 110
        })
111 111

@@ -127,7 +127,7 @@ defmodule OpenAgentsWeb.IssueCaptureControllerTest do

127 127
      conn =
128 128
        conn
129 129
        |> put_req_header("authorization", "Bearer #{credential}")
130
        |> post(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/capture", %{
130
        |> post(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/capture", %{
131 131
          problem: "Let me export a board to CSV."
132 132
        })
133 133
test/openagents_web/controllers/issue_completion_claim_controller_test.exs modified +3 -3

@@ -132,7 +132,7 @@ defmodule OpenAgentsWeb.IssueCompletionClaimControllerTest do

132 132
      build_conn()
133 133
      |> put_req_header("authorization", "Bearer #{context.credential}")
134 134
      |> post(
135
        ~p"/api/v3/repos/#{elsewhere.owner}/#{elsewhere.name}/issues/#{context.issue.number}/completion_claim",
135
        ~p"/api/v1/repos/#{elsewhere.owner}/#{elsewhere.name}/issues/#{context.issue.number}/completion_claim",
136 136
        %{
137 137
          "assignment_id" => context.assignment.id,
138 138
          "evidence" => [%{"criterion" => @criterion, "evidence_id" => context.evidence.id}]

@@ -156,7 +156,7 @@ defmodule OpenAgentsWeb.IssueCompletionClaimControllerTest do

156 156
    conn =
157 157
      get(
158 158
        build_conn(),
159
        ~p"/api/v3/repos/#{context.repository.owner}/#{context.repository.name}/issues/#{context.issue.number}"
159
        ~p"/api/v1/repos/#{context.repository.owner}/#{context.repository.name}/issues/#{context.issue.number}"
160 160
      )
161 161
162 162
    assert %{"openagents" => %{"completion_claims" => [claim]}} = json_response(conn, 200)

@@ -175,7 +175,7 @@ defmodule OpenAgentsWeb.IssueCompletionClaimControllerTest do

175 175
  defp post_claim_conn(conn, context) do
176 176
    post(
177 177
      conn,
178
      ~p"/api/v3/repos/#{context.repository.owner}/#{context.repository.name}/issues/#{context.issue.number}/completion_claim",
178
      ~p"/api/v1/repos/#{context.repository.owner}/#{context.repository.name}/issues/#{context.issue.number}/completion_claim",
179 179
      %{
180 180
        "assignment_id" => context.assignment.id,
181 181
        "evidence" => [%{"criterion" => @criterion, "evidence_id" => context.evidence.id}]
test/openagents_web/controllers/issue_controller_test.exs modified +74 -74

@@ -13,25 +13,25 @@ defmodule OpenAgentsWeb.IssueControllerTest do

13 13
  import OpenAgents.LabelsFixtures
14 14
15 15
  describe "index" do
16
    test "GET /api/v3/repos/:owner/:repo/issues lists open issues by default", %{
16
    test "GET /api/v1/repos/:owner/:repo/issues lists open issues by default", %{
17 17
      conn: conn
18 18
    } do
19 19
      {:ok, _issue} = Issues.create_issue(repository(), %{title: "First issue"})
20 20
21
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
21
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
22 22
23 23
      assert %{"issues" => [issue | _]} = json_response(conn, 200)
24 24
      assert issue["title"] == "First issue"
25 25
      assert issue["state"] == "open"
26 26
    end
27 27
28
    test "GET /api/v3/repos/:owner/:repo/issues filters by state", %{conn: conn} do
28
    test "GET /api/v1/repos/:owner/:repo/issues filters by state", %{conn: conn} do
29 29
      {:ok, _open_issue} = Issues.create_issue(repository(), %{title: "Open issue"})
30 30
31 31
      {:ok, _closed_issue} =
32 32
        Issues.create_issue(repository(), %{title: "Closed issue", state: "closed"})
33 33
34
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?state=closed")
34
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?state=closed")
35 35
36 36
      assert %{"issues" => [issue]} = json_response(conn, 200)
37 37
      assert issue["title"] == "Closed issue"

@@ -40,9 +40,9 @@ defmodule OpenAgentsWeb.IssueControllerTest do

40 40
  end
41 41
42 42
  describe "create" do
43
    test "POST /api/v3/repos/:owner/:repo/issues creates an issue", %{conn: conn} do
43
    test "POST /api/v1/repos/:owner/:repo/issues creates an issue", %{conn: conn} do
44 44
      conn =
45
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{
45
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{
46 46
          title: "New issue",
47 47
          body: "A description"
48 48
        })

@@ -55,11 +55,11 @@ defmodule OpenAgentsWeb.IssueControllerTest do

55 55
             } = json_response(conn, 201)
56 56
    end
57 57
58
    test "POST /api/v3/repos/:owner/:repo/issues returns 422 for missing title", %{
58
    test "POST /api/v1/repos/:owner/:repo/issues returns 422 for missing title", %{
59 59
      conn: conn
60 60
    } do
61 61
      conn =
62
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{body: "No title"})
62
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{body: "No title"})
63 63
64 64
      assert json_response(conn, 422)["errors"] != %{}
65 65
    end

@@ -75,7 +75,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

75 75
      conn =
76 76
        conn
77 77
        |> put_req_header("authorization", "Bearer #{credential}")
78
        |> post(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{
78
        |> post(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{
79 79
          title: "Agent issue",
80 80
          body: "Filed without a human link"
81 81
        })

@@ -100,7 +100,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

100 100
      conn =
101 101
        conn
102 102
        |> put_req_header("authorization", "Bearer #{credential}")
103
        |> post(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{
103
        |> post(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{
104 104
          title: "Should fail",
105 105
          body: "Suspended"
106 106
        })

@@ -126,7 +126,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

126 126
      conn =
127 127
        conn
128 128
        |> put_req_header("authorization", "Bearer #{credential}")
129
        |> post("/api/v3/repos/#{private.owner}/#{private.name}/issues", %{
129
        |> post("/api/v1/repos/#{private.owner}/#{private.name}/issues", %{
130 130
          title: "Should fail",
131 131
          body: "Private"
132 132
        })

@@ -152,7 +152,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

152 152
      created =
153 153
        conn
154 154
        |> put_req_header("authorization", "Bearer #{credential}")
155
        |> post(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{
155
        |> post(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{
156 156
          title: "Stable issue",
157 157
          body: "Authorship must remain stable"
158 158
        })

@@ -163,7 +163,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

163 163
      assert {:ok, _unlinked} = Agents.unlink(agent, user)
164 164
165 165
      after_link =
166
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{number}")
166
        get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{number}")
167 167
        |> json_response(200)
168 168
169 169
      assert after_link["user"] == before_author

@@ -191,7 +191,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

191 191
        conn
192 192
        |> put_req_header("authorization", "Bearer #{credential}")
193 193
        |> patch(
194
          "/api/v3/repos/#{repository.owner}/#{repository.name}/issues/#{issue.number}",
194
          "/api/v1/repos/#{repository.owner}/#{repository.name}/issues/#{issue.number}",
195 195
          %{state: "closed"}
196 196
        )
197 197

@@ -202,12 +202,12 @@ defmodule OpenAgentsWeb.IssueControllerTest do

202 202
  end
203 203
204 204
  describe "show" do
205
    test "GET /api/v3/repos/:owner/:repo/issues/:issue_number returns the issue", %{
205
    test "GET /api/v1/repos/:owner/:repo/issues/:issue_number returns the issue", %{
206 206
      conn: conn
207 207
    } do
208 208
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Show me"})
209 209
210
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
210
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
211 211
212 212
      assert %{
213 213
               "title" => "Show me",

@@ -217,10 +217,10 @@ defmodule OpenAgentsWeb.IssueControllerTest do

217 217
      assert n == issue.number
218 218
    end
219 219
220
    test "GET /api/v3/repos/:owner/:repo/issues/:issue_number returns 404 when missing", %{
220
    test "GET /api/v1/repos/:owner/:repo/issues/:issue_number returns 404 when missing", %{
221 221
      conn: conn
222 222
    } do
223
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999")
223
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999")
224 224
225 225
      assert json_response(conn, 404)
226 226
    end

@@ -232,7 +232,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

232 232
      {:ok, issue} = Issues.create_issue(private_repository, %{title: "Private issue"})
233 233
234 234
      path =
235
        "/api/v3/repos/#{private_repository.owner}/#{private_repository.name}/issues"
235
        "/api/v1/repos/#{private_repository.owner}/#{private_repository.name}/issues"
236 236
237 237
      assert get(build_conn(), path) |> json_response(404)
238 238

@@ -257,7 +257,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

257 257
      conn =
258 258
        get(
259 259
          conn,
260
          "/api/v3/repos/#{private_repository.owner}/#{private_repository.name}/issues"
260
          "/api/v1/repos/#{private_repository.owner}/#{private_repository.name}/issues"
261 261
        )
262 262
263 263
      assert json_response(conn, 404)

@@ -272,7 +272,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

272 272
  describe "a name the repository does not have" do
273 273
    test "POST with an unknown label answers 422 and names the label", %{conn: conn} do
274 274
      conn =
275
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{
275
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{
276 276
          title: "Labelled",
277 277
          labels: ["area:data-rights"]
278 278
        })

@@ -287,7 +287,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

287 287
288 288
    test "POST with an unknown assignee answers 422 and names the login", %{conn: conn} do
289 289
      conn =
290
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{
290
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{
291 291
          title: "Assigned",
292 292
          assignees: ["not-a-member"]
293 293
        })

@@ -301,7 +301,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

301 301
302 302
    test "POST with an unknown milestone answers 422 and names the number", %{conn: conn} do
303 303
      conn =
304
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues", %{
304
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues", %{
305 305
          title: "Planned",
306 306
          milestone: 4004
307 307
        })

@@ -317,7 +317,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

317 317
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Relabel me"})
318 318
319 319
      conn =
320
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}", %{
320
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}", %{
321 321
          labels: ["area:data-rights"]
322 322
        })
323 323

@@ -335,7 +335,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

335 335
      conn =
336 336
        post(
337 337
          conn,
338
          "/api/v3/repos/#{private_repository.owner}/#{private_repository.name}/issues",
338
          "/api/v1/repos/#{private_repository.owner}/#{private_repository.name}/issues",
339 339
          %{title: "Filed blind", labels: ["area:data-rights"]}
340 340
        )
341 341

@@ -380,14 +380,14 @@ defmodule OpenAgentsWeb.IssueControllerTest do

380 380
      unknown_label =
381 381
        post(
382 382
          put_forge_api_token(build_conn(), "issue-create-typo", repository()),
383
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues",
383
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues",
384 384
          %{title: "Typo", labels: ["area:data-rights"]}
385 385
        )
386 386
387 387
      unreadable_repository =
388 388
        post(
389 389
          put_forge_api_token(build_conn(), "issue-create-blind"),
390
          "/api/v3/repos/#{private_repository.owner}/#{private_repository.name}/issues",
390
          "/api/v1/repos/#{private_repository.owner}/#{private_repository.name}/issues",
391 391
          %{title: "Blind", labels: ["area:data-rights"]}
392 392
        )
393 393

@@ -400,13 +400,13 @@ defmodule OpenAgentsWeb.IssueControllerTest do

400 400
  end
401 401
402 402
  describe "update" do
403
    test "PATCH /api/v3/repos/:owner/:repo/issues/:issue_number closes an issue", %{
403
    test "PATCH /api/v1/repos/:owner/:repo/issues/:issue_number closes an issue", %{
404 404
      conn: conn
405 405
    } do
406 406
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Close me"})
407 407
408 408
      conn =
409
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}", %{
409
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}", %{
410 410
          state: "closed"
411 411
        })
412 412

@@ -422,7 +422,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

422 422
      conn =
423 423
        conn
424 424
        |> Map.replace!(:host, "staging.example.com")
425
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
425
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
426 426
427 427
      assert %{"issues" => [issue]} = json_response(conn, 200)
428 428

@@ -436,7 +436,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

436 436
      conn =
437 437
        conn
438 438
        |> put_req_header("x-forwarded-host", "evil.example.net")
439
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
439
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
440 440
441 441
      assert %{"issues" => [issue]} = json_response(conn, 200)
442 442
      refute issue["html_url"] =~ "evil.example.net"

@@ -550,7 +550,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

550 550
    test "index returns bounded pagination metadata", %{conn: conn} do
551 551
      {:ok, _issue} = Issues.create_issue(repository(), %{title: "Counted issue"})
552 552
553
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
553
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
554 554
555 555
      assert %{"issues" => issues, "pagination" => pagination} = json_response(conn, 200)
556 556
      assert length(issues) <= Issues.per_page()

@@ -583,7 +583,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

583 583
            %{"milestone" => "3"},
584 584
            %{"q" => "wombat"}
585 585
          ] do
586
        conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?#{params}")
586
        conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?#{params}")
587 587
        assert %{"issues" => [issue], "pagination" => %{"total" => 1}} = json_response(conn, 200)
588 588
        assert issue["title"] == "Wombat routing"
589 589
      end

@@ -594,7 +594,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

594 594
        {:ok, _} = Issues.create_issue(repository(), %{title: "Paged #{n}"})
595 595
      end)
596 596
597
      first = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
597
      first = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
598 598
599 599
      assert %{"issues" => page_one, "pagination" => %{"total_pages" => 2}} =
600 600
               json_response(first, 200)

@@ -602,7 +602,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

602 602
      assert length(page_one) == Issues.per_page()
603 603
604 604
      second =
605
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?page=2")
605
        get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?page=2")
606 606
607 607
      assert %{"issues" => page_two} = json_response(second, 200)
608 608
      assert length(page_two) == 30 - Issues.per_page()

@@ -611,14 +611,14 @@ defmodule OpenAgentsWeb.IssueControllerTest do

611 611
    end
612 612
613 613
    test "index rejects an unknown state with a stable error", %{conn: conn} do
614
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?state=bogus")
614
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?state=bogus")
615 615
616 616
      assert %{"errors" => %{"state" => [message]}} = json_response(conn, 422)
617 617
      assert message =~ "open"
618 618
    end
619 619
620 620
    test "index rejects a non-integer page with a stable error", %{conn: conn} do
621
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?page=zero")
621
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?page=zero")
622 622
623 623
      assert %{"errors" => %{"page" => [message]}} = json_response(conn, 422)
624 624
      assert message =~ "positive integer"

@@ -631,7 +631,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

631 631
      {:ok, blocker} = Issues.create_issue(repository(), %{title: "Prerequisite"})
632 632
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
633 633
634
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{blocked.number}")
634
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{blocked.number}")
635 635
636 636
      assert %{
637 637
               "openagents" => %{

@@ -650,7 +650,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

650 650
      {:ok, blocker} = Issues.create_issue(repository(), %{title: "Prerequisite"})
651 651
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
652 652
653
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
653
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
654 654
655 655
      assert %{"issues" => issues} = json_response(conn, 200)
656 656
      by_number = Map.new(issues, &{&1["number"], &1["openagents"]})

@@ -662,7 +662,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

662 662
    test "an issue without prerequisites reports an empty graph", %{conn: conn} do
663 663
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Ready"})
664 664
665
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
665
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
666 666
667 667
      assert %{"openagents" => %{"blocked" => false, "blocked_by" => [], "blocks" => []}} =
668 668
               json_response(conn, 200)

@@ -671,7 +671,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

671 671
    test "an issue nobody has worked reports no attempts, not a missing field", %{conn: conn} do
672 672
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Unworked"})
673 673
674
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
674
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
675 675
676 676
      assert %{"openagents" => %{"work" => []}} = json_response(conn, 200)
677 677
    end

@@ -683,7 +683,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

683 683
      record_attempt(issue, "agent/first", -600, %{state: "failed", failure_reason: "timeout"})
684 684
      record_attempt(issue, "agent/second", -60, %{state: "completed", terminal_commit: sha})
685 685
686
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
686
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
687 687
688 688
      assert %{"openagents" => %{"work" => [first, second]}} = json_response(conn, 200)
689 689

@@ -702,7 +702,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

702 702
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Bounded"})
703 703
      record_attempt(issue, "agent/bounded", -30, %{})
704 704
705
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
705
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
706 706
707 707
      assert %{"openagents" => %{"work" => [attempt]}} = json_response(conn, 200)
708 708

@@ -737,12 +737,12 @@ defmodule OpenAgentsWeb.IssueControllerTest do

737 737
      # `Repositories.readable_by/2` admits both.
738 738
      member =
739 739
        conn
740
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
740
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
741 741
        |> json_response(200)
742 742
743 743
      anonymous =
744 744
        Phoenix.ConnTest.build_conn()
745
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
745
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
746 746
        |> json_response(200)
747 747
748 748
      assert [%{"branch" => "agent/secret-rename"}] = member["openagents"]["work"]

@@ -761,7 +761,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

761 761
      {:ok, _idle} = Issues.create_issue(repository(), %{title: "Idle"})
762 762
      record_attempt(worked, "agent/page", -45, %{})
763 763
764
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
764
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
765 765
766 766
      assert %{"issues" => issues} = json_response(conn, 200)
767 767
      by_title = Map.new(issues, &{&1["title"], &1["openagents"]["work"]})

@@ -775,7 +775,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

775 775
    test "an issue nothing has evaluated reports no evidence, not a missing field", %{conn: conn} do
776 776
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Unevaluated"})
777 777
778
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
778
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
779 779
780 780
      assert %{"openagents" => %{"evidence" => []}} = json_response(conn, 200)
781 781
    end

@@ -785,7 +785,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

785 785
      sha = String.duplicate("9a", 20)
786 786
      record_evidence(issue, sha)
787 787
788
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
788
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
789 789
790 790
      assert %{"openagents" => %{"evidence" => [build, deploy]}} = json_response(conn, 200)
791 791

@@ -804,7 +804,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

804 804
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Bounded evidence"})
805 805
      record_evidence(issue, String.duplicate("7b", 20))
806 806
807
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
807
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
808 808
809 809
      assert %{"openagents" => %{"evidence" => [entry | _]}} = json_response(conn, 200)
810 810

@@ -826,7 +826,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

826 826
      {:ok, _idle} = Issues.create_issue(repository(), %{title: "Unevidenced"})
827 827
      record_evidence(shipped, String.duplicate("4c", 20))
828 828
829
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
829
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
830 830
831 831
      assert %{"issues" => issues} = json_response(conn, 200)
832 832
      by_title = Map.new(issues, &{&1["title"], &1["openagents"]["evidence"]})

@@ -843,7 +843,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

843 843
      {:ok, _ready} = Issues.create_issue(repository(), %{title: "Ready"})
844 844
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
845 845
846
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?blocked=true")
846
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?blocked=true")
847 847
848 848
      assert %{"issues" => [issue], "pagination" => %{"total" => 1}} = json_response(conn, 200)
849 849
      assert issue["title"] == "Waiting"

@@ -855,7 +855,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

855 855
      {:ok, _ready} = Issues.create_issue(repository(), %{title: "Ready"})
856 856
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
857 857
858
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?blocked=false")
858
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?blocked=false")
859 859
860 860
      assert %{"issues" => issues, "pagination" => %{"total" => 2}} = json_response(conn, 200)
861 861
      assert Enum.map(issues, & &1["title"]) |> Enum.sort() == ["Prerequisite", "Ready"]

@@ -866,14 +866,14 @@ defmodule OpenAgentsWeb.IssueControllerTest do

866 866
      {:ok, blocker} = Issues.create_issue(repository(), %{title: "Prerequisite"})
867 867
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
868 868
869
      patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{blocker.number}", %{
869
      patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{blocker.number}", %{
870 870
        state: "closed"
871 871
      })
872 872
873
      blocked_conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?blocked=true")
873
      blocked_conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?blocked=true")
874 874
      assert %{"issues" => [], "pagination" => %{"total" => 0}} = json_response(blocked_conn, 200)
875 875
876
      ready_conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?blocked=false")
876
      ready_conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?blocked=false")
877 877
878 878
      assert %{"issues" => [issue], "pagination" => %{"total" => 1}} =
879 879
               json_response(ready_conn, 200)

@@ -882,7 +882,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

882 882
    end
883 883
884 884
    test "index rejects a blocked value that is not a boolean", %{conn: conn} do
885
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?blocked=maybe")
885
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?blocked=maybe")
886 886
887 887
      assert %{"errors" => %{"blocked" => [message]}} = json_response(conn, 422)
888 888
      assert message =~ "true or false"

@@ -894,7 +894,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

894 894
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Underway"})
895 895
      place(repository(), issue, "In Progress")
896 896
897
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
897
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
898 898
899 899
      assert %{"openagents" => %{"progress" => "in_progress"}} = json_response(conn, 200)
900 900
    end

@@ -902,7 +902,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

902 902
    test "an issue on no board has not started", %{conn: conn} do
903 903
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Queued"})
904 904
905
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
905
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
906 906
907 907
      assert %{"openagents" => %{"progress" => "to_do"}} = json_response(conn, 200)
908 908
    end

@@ -912,7 +912,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

912 912
      place(repository(), issue, "In Progress")
913 913
914 914
      conn =
915
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}", %{
915
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}", %{
916 916
          state: "closed"
917 917
        })
918 918

@@ -924,7 +924,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

924 924
      {:ok, queued} = Issues.create_issue(repository(), %{title: "Queued"})
925 925
      place(repository(), started, "In Progress")
926 926
927
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
927
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
928 928
929 929
      assert %{"issues" => issues} = json_response(conn, 200)
930 930
      by_number = Map.new(issues, &{&1["number"], &1["openagents"]["progress"]})

@@ -937,7 +937,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

937 937
      {:ok, issue} = Issues.create_issue(repository(), %{title: "Underway"})
938 938
      place(repository(), issue, "In Progress")
939 939
940
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
940
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}")
941 941
      body = json_response(conn, 200)
942 942
943 943
      assert body["state"] == "open"

@@ -959,7 +959,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

959 959
      {:ok, _queued} = Issues.create_issue(repository(), %{title: "Queued"})
960 960
      place(repository(), started, "In Progress")
961 961
962
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?progress=in_progress")
962
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?progress=in_progress")
963 963
964 964
      assert %{"issues" => [issue], "pagination" => %{"total" => 1}} = json_response(conn, 200)
965 965
      assert issue["title"] == "Underway"

@@ -976,7 +976,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

976 976
        conn =
977 977
          get(
978 978
            conn,
979
            ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?state=all&progress=#{value}"
979
            ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?state=all&progress=#{value}"
980 980
          )
981 981
982 982
        for issue <- json_response(conn, 200)["issues"] do

@@ -993,7 +993,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

993 993
      anonymous =
994 994
        get(
995 995
          build_conn(),
996
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}"
996
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}"
997 997
        )
998 998
999 999
      assert %{"openagents" => %{"progress" => "to_do"}} = json_response(anonymous, 200)

@@ -1001,14 +1001,14 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1001 1001
      listed =
1002 1002
        get(
1003 1003
          build_conn(),
1004
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?progress=in_progress"
1004
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?progress=in_progress"
1005 1005
        )
1006 1006
1007 1007
      assert %{"issues" => [], "pagination" => %{"total" => 0}} = json_response(listed, 200)
1008 1008
    end
1009 1009
1010 1010
    test "index rejects a progress value outside the published enum", %{conn: conn} do
1011
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?progress=doing")
1011
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?progress=doing")
1012 1012
1013 1013
      assert %{"errors" => %{"progress" => [message]}} = json_response(conn, 422)
1014 1014
      assert message =~ "to_do"

@@ -1046,7 +1046,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1046 1046
    } do
1047 1047
      %{"issues" => issues} =
1048 1048
        conn
1049
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
1049
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
1050 1050
        |> json_response(200)
1051 1051
1052 1052
      numbers = Enum.map(issues, & &1["number"])

@@ -1060,14 +1060,14 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1060 1060
    } do
1061 1061
      %{"issues" => issues} =
1062 1062
        conn
1063
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues")
1063
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues")
1064 1064
        |> json_response(200)
1065 1065
1066 1066
      entry = Enum.find(issues, &(&1["number"] == proposal.number))
1067 1067
1068 1068
      assert entry["draft"] == false
1069 1069
      assert entry["pull_request"]["html_url"] =~ "/pulls/#{proposal.number}"
1070
      assert entry["pull_request"]["url"] =~ "/api/v3/repos/OpenAgentsInc/openagents.com/pulls/"
1070
      assert entry["pull_request"]["url"] =~ "/api/v1/repos/OpenAgentsInc/openagents.com/pulls/"
1071 1071
      assert entry["pull_request"]["merged_at"] == nil
1072 1072
    end
1073 1073

@@ -1077,7 +1077,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1077 1077
    } do
1078 1078
      entry =
1079 1079
        conn
1080
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{plain.number}")
1080
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{plain.number}")
1081 1081
        |> json_response(200)
1082 1082
1083 1083
      refute Map.has_key?(entry, "pull_request")

@@ -1087,7 +1087,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1087 1087
    test "the show endpoint marks a pull request too", %{conn: conn, proposal: proposal} do
1088 1088
      entry =
1089 1089
        conn
1090
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{proposal.number}")
1090
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{proposal.number}")
1091 1091
        |> json_response(200)
1092 1092
1093 1093
      assert entry["pull_request"]["html_url"] =~ "/pulls/#{proposal.number}"

@@ -1100,7 +1100,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1100 1100
    } do
1101 1101
      %{"issues" => issues} =
1102 1102
        conn
1103
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?type=issue")
1103
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?type=issue")
1104 1104
        |> json_response(200)
1105 1105
1106 1106
      numbers = Enum.map(issues, & &1["number"])

@@ -1115,7 +1115,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1115 1115
    } do
1116 1116
      %{"issues" => issues} =
1117 1117
        conn
1118
        |> get(~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?type=pull_request")
1118
        |> get(~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?type=pull_request")
1119 1119
        |> json_response(200)
1120 1120
1121 1121
      numbers = Enum.map(issues, & &1["number"])

@@ -1124,7 +1124,7 @@ defmodule OpenAgentsWeb.IssueControllerTest do

1124 1124
    end
1125 1125
1126 1126
    test "index rejects a type outside the published enum", %{conn: conn} do
1127
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues?type=proposal")
1127
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues?type=proposal")
1128 1128
1129 1129
      assert %{"errors" => %{"type" => [message]}} = json_response(conn, 422)
1130 1130
      assert message =~ "pull_request"
test/openagents_web/controllers/issue_dependency_controller_test.exs modified +4 -4

@@ -70,7 +70,7 @@ defmodule OpenAgentsWeb.IssueDependencyControllerTest do

70 70
      conn =
71 71
        post(
72 72
          conn,
73
          "/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999/dependencies",
73
          "/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/dependencies",
74 74
          %{blocked_by: []}
75 75
        )
76 76

@@ -127,7 +127,7 @@ defmodule OpenAgentsWeb.IssueDependencyControllerTest do

127 127
      {:ok, issue} = Issues.create_issue(private_repository, %{title: "Private"})
128 128
129 129
      path =
130
        "/api/v3/repos/#{private_repository.owner}/#{private_repository.name}" <>
130
        "/api/v1/repos/#{private_repository.owner}/#{private_repository.name}" <>
131 131
          "/issues/#{issue.number}/dependencies"
132 132
133 133
      assert get(build_conn(), path) |> json_response(404)

@@ -144,7 +144,7 @@ defmodule OpenAgentsWeb.IssueDependencyControllerTest do

144 144
      conn = put_forge_api_token(build_conn(), "dependency-nonmember")
145 145
146 146
      path =
147
        "/api/v3/repos/#{other_repository.owner}/#{other_repository.name}" <>
147
        "/api/v1/repos/#{other_repository.owner}/#{other_repository.name}" <>
148 148
          "/issues/#{blocked.number}/dependencies"
149 149
150 150
      assert post(conn, path, %{blocked_by: [blocker.number]}) |> json_response(404)

@@ -163,7 +163,7 @@ defmodule OpenAgentsWeb.IssueDependencyControllerTest do

163 163
  end
164 164
165 165
  defp dependencies_path(issue) do
166
    "/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/dependencies"
166
    "/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/dependencies"
167 167
  end
168 168
169 169
  defp repository do
test/openagents_web/controllers/issue_label_controller_test.exs modified +14 -14

@@ -19,7 +19,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

19 19
      issue: issue
20 20
    } do
21 21
      conn =
22
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels")
22
        get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels")
23 23
24 24
      assert json_response(conn, 200) == %{"labels" => []}
25 25
    end

@@ -32,14 +32,14 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

32 32
      {:ok, _issue} = Issues.add_labels(issue, ["bug"])
33 33
34 34
      conn =
35
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels")
35
        get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels")
36 36
37 37
      assert %{"labels" => [label]} = json_response(conn, 200)
38 38
      assert label["name"] == "bug"
39 39
    end
40 40
41 41
    test "GET .../issues/:issue_number/labels returns 404 for a missing issue", %{conn: conn} do
42
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999/labels")
42
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/labels")
43 43
44 44
      assert_api_error(conn, 404, "not_found")
45 45
    end

@@ -55,7 +55,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

55 55
      conn =
56 56
        post(
57 57
          conn,
58
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
58
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
59 59
          %{
60 60
            labels: ["bug"]
61 61
          }

@@ -77,7 +77,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

77 77
      conn =
78 78
        post(
79 79
          conn,
80
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
80
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
81 81
          %{
82 82
            labels: ["bug", "docs"]
83 83
          }

@@ -97,7 +97,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

97 97
      conn =
98 98
        post(
99 99
          conn,
100
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
100
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
101 101
          %{
102 102
            labels: ["bug"]
103 103
          }

@@ -113,7 +113,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

113 113
      conn =
114 114
        post(
115 115
          conn,
116
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
116
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
117 117
          %{}
118 118
        )
119 119

@@ -130,7 +130,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

130 130
      conn =
131 131
        post(
132 132
          conn,
133
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
133
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
134 134
          %{
135 135
            labels: ["never-created"]
136 136
          }

@@ -154,7 +154,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

154 154
      conn =
155 155
        post(
156 156
          conn,
157
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
157
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels",
158 158
          %{labels: [""]}
159 159
        )
160 160

@@ -164,7 +164,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

164 164
165 165
    test "POST .../issues/:issue_number/labels returns 404 for a missing issue", %{conn: conn} do
166 166
      conn =
167
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999/labels", %{
167
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/labels", %{
168 168
          labels: ["bug"]
169 169
        })
170 170

@@ -184,7 +184,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

184 184
      conn =
185 185
        delete(
186 186
          conn,
187
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels/bug"
187
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels/bug"
188 188
        )
189 189
190 190
      assert %{"labels" => [label]} = json_response(conn, 200)

@@ -204,7 +204,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

204 204
      conn =
205 205
        delete(
206 206
          conn,
207
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels/good first issue"
207
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels/good first issue"
208 208
        )
209 209
210 210
      assert json_response(conn, 200) == %{"labels" => []}

@@ -223,7 +223,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

223 223
      conn =
224 224
        delete(
225 225
          conn,
226
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels/docs"
226
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/#{issue.number}/labels/docs"
227 227
        )
228 228
229 229
      assert json_response(conn, 404)

@@ -233,7 +233,7 @@ defmodule OpenAgentsWeb.IssueLabelControllerTest do

233 233
    test "DELETE .../issues/:issue_number/labels/:name returns 404 for a missing issue", %{
234 234
      conn: conn
235 235
    } do
236
      conn = delete(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/999999/labels/bug")
236
      conn = delete(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/999999/labels/bug")
237 237
238 238
      assert_api_error(conn, 404, "not_found")
239 239
    end
test/openagents_web/controllers/label_controller_test.exs modified +36 -36

@@ -8,38 +8,38 @@ defmodule OpenAgentsWeb.LabelControllerTest do

8 8
  alias OpenAgents.Labels
9 9
10 10
  describe "index" do
11
    test "GET /api/v3/repos/:owner/:repo/labels lists labels", %{conn: conn} do
11
    test "GET /api/v1/repos/:owner/:repo/labels lists labels", %{conn: conn} do
12 12
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
13 13
14
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels")
14
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels")
15 15
16 16
      assert %{"labels" => [label]} = json_response(conn, 200)
17 17
      assert label["name"] == "bug"
18 18
      assert label["color"] == "d73a4a"
19 19
    end
20 20
21
    test "GET /api/v3/repos/:owner/:repo/labels renders a repo-scoped url", %{conn: conn} do
21
    test "GET /api/v1/repos/:owner/:repo/labels renders a repo-scoped url", %{conn: conn} do
22 22
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
23 23
24
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels")
24
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels")
25 25
26 26
      assert %{"labels" => [label]} = json_response(conn, 200)
27 27
28 28
      assert label["url"] ==
29
               "http://www.example.com/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug"
29
               "http://www.example.com/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug"
30 30
    end
31 31
32
    test "GET /api/v3/repos/:owner/:repo/labels returns an empty list", %{conn: conn} do
33
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels")
32
    test "GET /api/v1/repos/:owner/:repo/labels returns an empty list", %{conn: conn} do
33
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels")
34 34
35 35
      assert json_response(conn, 200) == %{"labels" => []}
36 36
    end
37 37
  end
38 38
39 39
  describe "create" do
40
    test "POST /api/v3/repos/:owner/:repo/labels creates a label", %{conn: conn} do
40
    test "POST /api/v1/repos/:owner/:repo/labels creates a label", %{conn: conn} do
41 41
      conn =
42
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels", %{
42
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels", %{
43 43
          name: "enhancement",
44 44
          color: "a2eeef",
45 45
          description: "New feature"

@@ -55,21 +55,21 @@ defmodule OpenAgentsWeb.LabelControllerTest do

55 55
      assert Labels.get_label_by_name!(repository(), "enhancement").color == "a2eeef"
56 56
    end
57 57
58
    test "POST /api/v3/repos/:owner/:repo/labels returns 422 without a color", %{conn: conn} do
58
    test "POST /api/v1/repos/:owner/:repo/labels returns 422 without a color", %{conn: conn} do
59 59
      conn =
60
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels", %{name: "no-color"})
60
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels", %{name: "no-color"})
61 61
62 62
      assert %{"errors" => %{"color" => _}} = json_response(conn, 422)
63 63
    end
64 64
65
    test "POST /api/v3/repos/:owner/:repo/labels returns 422 without a name", %{conn: conn} do
66
      conn = post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels", %{color: "ffffff"})
65
    test "POST /api/v1/repos/:owner/:repo/labels returns 422 without a name", %{conn: conn} do
66
      conn = post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels", %{color: "ffffff"})
67 67
68 68
      assert %{"errors" => %{"name" => _}} = json_response(conn, 422)
69 69
    end
70 70
71
    test "POST /api/v3/repos/:owner/:repo/labels does not persist owner or repo", %{conn: conn} do
72
      post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels", %{
71
    test "POST /api/v1/repos/:owner/:repo/labels does not persist owner or repo", %{conn: conn} do
72
      post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels", %{
73 73
        name: "scoped",
74 74
        color: "ffffff"
75 75
      })

@@ -81,25 +81,25 @@ defmodule OpenAgentsWeb.LabelControllerTest do

81 81
  end
82 82
83 83
  describe "show" do
84
    test "GET /api/v3/repos/:owner/:repo/labels/:name returns the label", %{conn: conn} do
84
    test "GET /api/v1/repos/:owner/:repo/labels/:name returns the label", %{conn: conn} do
85 85
      label = label_fixture(repository(), %{name: "bug", color: "d73a4a"})
86 86
87
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug")
87
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug")
88 88
89 89
      assert %{"name" => "bug", "id" => id} = json_response(conn, 200)
90 90
      assert id == label.id
91 91
    end
92 92
93
    test "GET /api/v3/repos/:owner/:repo/labels/:name decodes an escaped name", %{conn: conn} do
93
    test "GET /api/v1/repos/:owner/:repo/labels/:name decodes an escaped name", %{conn: conn} do
94 94
      label_fixture(repository(), %{name: "good first issue", color: "7057ff"})
95 95
96
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/good first issue")
96
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/good first issue")
97 97
98 98
      assert json_response(conn, 200)["name"] == "good first issue"
99 99
    end
100 100
101
    test "GET /api/v3/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do
102
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/nope")
101
    test "GET /api/v1/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do
102
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/nope")
103 103
104 104
      assert_api_error(conn, 404, "not_found")
105 105
    end

@@ -109,7 +109,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

109 109
    test "the rendered url round-trips through the show endpoint", %{conn: conn} do
110 110
      label_fixture(repository(), %{name: "good first issue", color: "7057ff"})
111 111
112
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels")
112
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels")
113 113
114 114
      assert %{"labels" => [label]} = json_response(conn, 200)
115 115
      assert label["url"] =~ "good%20first%20issue"

@@ -120,21 +120,21 @@ defmodule OpenAgentsWeb.LabelControllerTest do

120 120
  end
121 121
122 122
  describe "update" do
123
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name updates the color", %{conn: conn} do
123
    test "PATCH /api/v1/repos/:owner/:repo/labels/:name updates the color", %{conn: conn} do
124 124
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
125 125
126 126
      conn =
127
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", %{color: "000000"})
127
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug", %{color: "000000"})
128 128
129 129
      assert %{"name" => "bug", "color" => "000000"} = json_response(conn, 200)
130 130
      assert Labels.get_label_by_name!(repository(), "bug").color == "000000"
131 131
    end
132 132
133
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name updates the description", %{conn: conn} do
133
    test "PATCH /api/v1/repos/:owner/:repo/labels/:name updates the description", %{conn: conn} do
134 134
      label_fixture(repository(), %{name: "bug", color: "d73a4a", description: "old"})
135 135
136 136
      conn =
137
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", %{
137
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug", %{
138 138
          description: "new"
139 139
        })
140 140

@@ -143,11 +143,11 @@ defmodule OpenAgentsWeb.LabelControllerTest do

143 143
144 144
    # GitHub renames through `new_name`; the name in the path identifies the
145 145
    # label and `new_name` is what it becomes.
146
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name renames with new_name", %{conn: conn} do
146
    test "PATCH /api/v1/repos/:owner/:repo/labels/:name renames with new_name", %{conn: conn} do
147 147
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
148 148
149 149
      conn =
150
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", %{
150
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug", %{
151 151
          new_name: "defect"
152 152
        })
153 153

@@ -157,7 +157,7 @@ defmodule OpenAgentsWeb.LabelControllerTest do

157 157
      assert_raise Ecto.NoResultsError, fn -> Labels.get_label_by_name!(repository(), "bug") end
158 158
    end
159 159
160
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name returns 422 for a blank color", %{
160
    test "PATCH /api/v1/repos/:owner/:repo/labels/:name returns 422 for a blank color", %{
161 161
      conn: conn
162 162
    } do
163 163
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})

@@ -165,32 +165,32 @@ defmodule OpenAgentsWeb.LabelControllerTest do

165 165
      conn =
166 166
        conn
167 167
        |> put_req_header("content-type", "application/json")
168
        |> patch(~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug", ~s({"color": null}))
168
        |> patch(~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug", ~s({"color": null}))
169 169
170 170
      assert %{"errors" => %{"color" => _}} = json_response(conn, 422)
171 171
      assert Labels.get_label_by_name!(repository(), "bug").color == "d73a4a"
172 172
    end
173 173
174
    test "PATCH /api/v3/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do
174
    test "PATCH /api/v1/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do
175 175
      conn =
176
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/nope", %{color: "000000"})
176
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/nope", %{color: "000000"})
177 177
178 178
      assert_api_error(conn, 404, "not_found")
179 179
    end
180 180
  end
181 181
182 182
  describe "delete" do
183
    test "DELETE /api/v3/repos/:owner/:repo/labels/:name returns 204", %{conn: conn} do
183
    test "DELETE /api/v1/repos/:owner/:repo/labels/:name returns 204", %{conn: conn} do
184 184
      label_fixture(repository(), %{name: "bug", color: "d73a4a"})
185 185
186
      conn = delete(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/bug")
186
      conn = delete(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug")
187 187
188 188
      assert response(conn, 204) == ""
189 189
      assert Labels.list_labels(repository()) == []
190 190
    end
191 191
192
    test "DELETE /api/v3/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do
193
      conn = delete(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/labels/nope")
192
    test "DELETE /api/v1/repos/:owner/:repo/labels/:name returns 404 when missing", %{conn: conn} do
193
      conn = delete(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/labels/nope")
194 194
195 195
      assert_api_error(conn, 404, "not_found")
196 196
    end
test/openagents_web/controllers/milestone_controller_test.exs modified +30 -30

@@ -9,17 +9,17 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

9 9
  alias OpenAgents.Milestones
10 10
11 11
  describe "index" do
12
    test "GET /api/v3/repos/:owner/:repo/milestones lists milestones", %{conn: conn} do
12
    test "GET /api/v1/repos/:owner/:repo/milestones lists milestones", %{conn: conn} do
13 13
      milestone_fixture(repository(), %{title: "v1.0", state: "open"})
14 14
15
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones")
15
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones")
16 16
17 17
      assert %{"milestones" => [milestone]} = json_response(conn, 200)
18 18
      assert milestone["title"] == "v1.0"
19 19
      assert milestone["state"] == "open"
20 20
    end
21 21
22
    test "GET /api/v3/repos/:owner/:repo/milestones renders issue counts and url", %{conn: conn} do
22
    test "GET /api/v1/repos/:owner/:repo/milestones renders issue counts and url", %{conn: conn} do
23 23
      milestone = milestone_fixture(repository(), %{title: "v1.0"})
24 24
25 25
      {:ok, _open_issue} =

@@ -32,27 +32,27 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

32 32
          state: "closed"
33 33
        })
34 34
35
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones")
35
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones")
36 36
37 37
      assert %{"milestones" => [rendered]} = json_response(conn, 200)
38 38
      assert rendered["open_issues"] == 1
39 39
      assert rendered["closed_issues"] == 1
40 40
41 41
      assert rendered["url"] ==
42
               "http://www.example.com/api/v3/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}"
42
               "http://www.example.com/api/v1/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}"
43 43
    end
44 44
45
    test "GET /api/v3/repos/:owner/:repo/milestones returns an empty list", %{conn: conn} do
46
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones")
45
    test "GET /api/v1/repos/:owner/:repo/milestones returns an empty list", %{conn: conn} do
46
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones")
47 47
48 48
      assert json_response(conn, 200) == %{"milestones" => []}
49 49
    end
50 50
  end
51 51
52 52
  describe "create" do
53
    test "POST /api/v3/repos/:owner/:repo/milestones creates a milestone", %{conn: conn} do
53
    test "POST /api/v1/repos/:owner/:repo/milestones creates a milestone", %{conn: conn} do
54 54
      conn =
55
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones", %{
55
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones", %{
56 56
          title: "v2.0",
57 57
          description: "Second release"
58 58
        })

@@ -67,19 +67,19 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

67 67
      assert Milestones.get_milestone_by_number!(repository(), number).title == "v2.0"
68 68
    end
69 69
70
    test "POST /api/v3/repos/:owner/:repo/milestones assigns sequential numbers", %{conn: conn} do
70
    test "POST /api/v1/repos/:owner/:repo/milestones assigns sequential numbers", %{conn: conn} do
71 71
      first =
72
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones", %{title: "one"})
72
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones", %{title: "one"})
73 73
74 74
      second =
75
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones", %{title: "two"})
75
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones", %{title: "two"})
76 76
77 77
      assert json_response(second, 201)["number"] == json_response(first, 201)["number"] + 1
78 78
    end
79 79
80
    test "POST /api/v3/repos/:owner/:repo/milestones returns 422 without a title", %{conn: conn} do
80
    test "POST /api/v1/repos/:owner/:repo/milestones returns 422 without a title", %{conn: conn} do
81 81
      conn =
82
        post(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones", %{
82
        post(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones", %{
83 83
          description: "No title"
84 84
        })
85 85

@@ -89,32 +89,32 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

89 89
  end
90 90
91 91
  describe "show" do
92
    test "GET /api/v3/repos/:owner/:repo/milestones/:milestone_number returns it", %{conn: conn} do
92
    test "GET /api/v1/repos/:owner/:repo/milestones/:milestone_number returns it", %{conn: conn} do
93 93
      milestone = milestone_fixture(repository(), %{title: "Show me"})
94 94
95 95
      conn =
96
        get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}")
96
        get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}")
97 97
98 98
      assert %{"title" => "Show me", "id" => id} = json_response(conn, 200)
99 99
      assert id == milestone.id
100 100
    end
101 101
102
    test "GET /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",
102
    test "GET /api/v1/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",
103 103
         %{conn: conn} do
104
      conn = get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/999999")
104
      conn = get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/999999")
105 105
106 106
      assert_api_error(conn, 404, "not_found")
107 107
    end
108 108
  end
109 109
110 110
  describe "update" do
111
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number closes it", %{conn: conn} do
111
    test "PATCH /api/v1/repos/:owner/:repo/milestones/:milestone_number closes it", %{conn: conn} do
112 112
      milestone = milestone_fixture(repository(), %{title: "Close me", state: "open"})
113 113
114 114
      conn =
115 115
        patch(
116 116
          conn,
117
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}",
117
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}",
118 118
          %{
119 119
            state: "closed"
120 120
          }

@@ -124,7 +124,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

124 124
      assert Milestones.get_milestone_by_number!(repository(), milestone.number).state == "closed"
125 125
    end
126 126
127
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number retitles it", %{
127
    test "PATCH /api/v1/repos/:owner/:repo/milestones/:milestone_number retitles it", %{
128 128
      conn: conn
129 129
    } do
130 130
      milestone = milestone_fixture(repository(), %{title: "Old title"})

@@ -132,7 +132,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

132 132
      conn =
133 133
        patch(
134 134
          conn,
135
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}",
135
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}",
136 136
          %{
137 137
            title: "New title"
138 138
          }

@@ -141,7 +141,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

141 141
      assert json_response(conn, 200)["title"] == "New title"
142 142
    end
143 143
144
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 422 for a blank title",
144
    test "PATCH /api/v1/repos/:owner/:repo/milestones/:milestone_number returns 422 for a blank title",
145 145
         %{conn: conn} do
146 146
      milestone = milestone_fixture(repository(), %{title: "Keep me"})
147 147

@@ -149,7 +149,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

149 149
        conn
150 150
        |> put_req_header("content-type", "application/json")
151 151
        |> patch(
152
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}",
152
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}",
153 153
          ~s({"title": null})
154 154
        )
155 155

@@ -159,10 +159,10 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

159 159
               "Keep me"
160 160
    end
161 161
162
    test "PATCH /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",
162
    test "PATCH /api/v1/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",
163 163
         %{conn: conn} do
164 164
      conn =
165
        patch(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/999999", %{
165
        patch(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/999999", %{
166 166
          state: "closed"
167 167
        })
168 168

@@ -171,7 +171,7 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

171 171
  end
172 172
173 173
  describe "delete" do
174
    test "DELETE /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 204", %{
174
    test "DELETE /api/v1/repos/:owner/:repo/milestones/:milestone_number returns 204", %{
175 175
      conn: conn
176 176
    } do
177 177
      milestone = milestone_fixture(repository(), %{title: "Delete me"})

@@ -179,16 +179,16 @@ defmodule OpenAgentsWeb.MilestoneControllerTest do

179 179
      conn =
180 180
        delete(
181 181
          conn,
182
          ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}"
182
          ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/#{milestone.number}"
183 183
        )
184 184
185 185
      assert response(conn, 204) == ""
186 186
      assert Milestones.list_milestones(repository()) == []
187 187
    end
188 188
189
    test "DELETE /api/v3/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",
189
    test "DELETE /api/v1/repos/:owner/:repo/milestones/:milestone_number returns 404 when missing",
190 190
         %{conn: conn} do
191
      conn = delete(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/milestones/999999")
191
      conn = delete(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/milestones/999999")
192 192
193 193
      assert_api_error(conn, 404, "not_found")
194 194
    end
test/openagents_web/controllers/model_catalog_controller_test.exs modified +4 -4

@@ -1,6 +1,6 @@

1 1
defmodule OpenAgentsWeb.ModelCatalogControllerTest do
2 2
  @moduledoc """
3
  `GET /api/v3/models`: the typed catalog a client selects from (PROVIDER-002).
3
  `GET /api/v1/models`: the typed catalog a client selects from (PROVIDER-002).
4 4
5 5
  The catalog is what makes model selection honest: the CLI renders this list,
6 6
  the thread admission refuses against the same list, and a lane whose

@@ -15,7 +15,7 @@ defmodule OpenAgentsWeb.ModelCatalogControllerTest do

15 15
    body =
16 16
      conn
17 17
      |> put_chat_api_token("model-catalog")
18
      |> get(~p"/api/v3/models")
18
      |> get(~p"/api/v1/models")
19 19
      |> json_response(200)
20 20
21 21
    assert body["default"] == Models.default_id()

@@ -50,7 +50,7 @@ defmodule OpenAgentsWeb.ModelCatalogControllerTest do

50 50
    body =
51 51
      conn
52 52
      |> put_chat_api_token("model-catalog-unavailable")
53
      |> get(~p"/api/v3/models")
53
      |> get(~p"/api/v1/models")
54 54
      |> json_response(200)
55 55
56 56
    # The model stays in the list — "served here, not currently configured"

@@ -70,7 +70,7 @@ defmodule OpenAgentsWeb.ModelCatalogControllerTest do

70 70
  test "the catalog requires the same bearer as threads", %{conn: conn} do
71 71
    body =
72 72
      conn
73
      |> get(~p"/api/v3/models")
73
      |> get(~p"/api/v1/models")
74 74
      |> json_response(401)
75 75
76 76
    assert body["code"] == "unauthenticated"
test/openagents_web/controllers/private_issue_metadata_test.exs modified +8 -8

@@ -49,7 +49,7 @@ defmodule OpenAgentsWeb.PrivateIssueMetadataTest do

49 49
  end
50 50
51 51
  defp paths(%{issue: issue, comment: comment, label: label, milestone: milestone, owner: owner}) do
52
    base = "/api/v3/repos/metadata-owner/private-work"
52
    base = "/api/v1/repos/metadata-owner/private-work"
53 53
54 54
    %{
55 55
      "issue comments" => "#{base}/issues/#{issue.number}/comments",

@@ -80,7 +80,7 @@ defmodule OpenAgentsWeb.PrivateIssueMetadataTest do

80 80
81 81
      assert %{"comments" => [read]} =
82 82
               conn
83
               |> get("/api/v3/repos/metadata-owner/private-work/issues/#{issue.number}/comments")
83
               |> get("/api/v1/repos/metadata-owner/private-work/issues/#{issue.number}/comments")
84 84
               |> json_response(200)
85 85
86 86
      assert read["id"] == comment.id

@@ -88,28 +88,28 @@ defmodule OpenAgentsWeb.PrivateIssueMetadataTest do

88 88
89 89
      assert %{"labels" => labels} =
90 90
               conn
91
               |> get("/api/v3/repos/metadata-owner/private-work/labels")
91
               |> get("/api/v1/repos/metadata-owner/private-work/labels")
92 92
               |> json_response(200)
93 93
94 94
      assert "shipped" in Enum.map(labels, & &1["name"])
95 95
96 96
      assert %{"milestones" => [milestone]} =
97 97
               conn
98
               |> get("/api/v3/repos/metadata-owner/private-work/milestones")
98
               |> get("/api/v1/repos/metadata-owner/private-work/milestones")
99 99
               |> json_response(200)
100 100
101 101
      assert milestone["title"] == "first release"
102 102
103 103
      assert %{"assignees" => assignees} =
104 104
               conn
105
               |> get("/api/v3/repos/metadata-owner/private-work/assignees")
105
               |> get("/api/v1/repos/metadata-owner/private-work/assignees")
106 106
               |> json_response(200)
107 107
108 108
      assert context.owner.github_login in Enum.map(assignees, & &1["login"])
109 109
110 110
      assert %{"labels" => [issue_label]} =
111 111
               conn
112
               |> get("/api/v3/repos/metadata-owner/private-work/issues/#{issue.number}/labels")
112
               |> get("/api/v1/repos/metadata-owner/private-work/issues/#{issue.number}/labels")
113 113
               |> json_response(200)
114 114
115 115
      assert issue_label["name"] == "mine"

@@ -163,7 +163,7 @@ defmodule OpenAgentsWeb.PrivateIssueMetadataTest do

163 163
      public_issue: issue,
164 164
      public_comment: comment
165 165
    } do
166
      base = "/api/v3/repos/metadata-public/open-work"
166
      base = "/api/v1/repos/metadata-public/open-work"
167 167
168 168
      for path <- [
169 169
            "#{base}/issues/#{issue.number}/comments",

@@ -192,7 +192,7 @@ defmodule OpenAgentsWeb.PrivateIssueMetadataTest do

192 192
      conn: conn,
193 193
      public_issue: issue
194 194
    } do
195
      base = "/api/v3/repos/metadata-public/open-work"
195
      base = "/api/v1/repos/metadata-public/open-work"
196 196
      path = "#{base}/issues/#{issue.number}/comments"
197 197
198 198
      anonymous = build_conn() |> get(path) |> json_response(200)
test/openagents_web/controllers/project_controller_test.exs modified +114 -114

@@ -27,12 +27,12 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

27 27
  end
28 28
29 29
  describe "index" do
30
    test "GET /api/v3/repos/:owner/:repo/projectsV2 lists the repository's projects", %{
30
    test "GET /api/v1/repos/:owner/:repo/projectsV2 lists the repository's projects", %{
31 31
      conn: conn
32 32
    } do
33 33
      project_fixture(%{title: "Roadmap", owner: "alice", state: "open"})
34 34
35
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2")
35
      conn = get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2")
36 36
37 37
      assert %{"projects" => [project]} = json_response(conn, 200)
38 38
      assert project["title"] == "Roadmap"

@@ -40,28 +40,28 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

40 40
      assert project["state"] == "open"
41 41
    end
42 42
43
    test "GET /api/v3/repos/:owner/:repo/projectsV2 excludes another repository", %{conn: conn} do
43
    test "GET /api/v1/repos/:owner/:repo/projectsV2 excludes another repository", %{conn: conn} do
44 44
      project_fixture(%{title: "Mine", owner: "alice"})
45 45
      other_repository = repository_fixture(%{owner: "Elsewhere", name: "other-projects"})
46 46
47 47
      {:ok, _project} =
48 48
        Projects.create_project(other_repository, %{title: "Theirs", owner: "bob"})
49 49
50
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2")
50
      conn = get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2")
51 51
52 52
      assert %{"projects" => [%{"title" => "Mine"}]} = json_response(conn, 200)
53 53
    end
54 54
55
    test "GET /api/v3/repos/:owner/:repo/projectsV2 hides a private repository from a non-member" do
56
      conn = get(build_conn(), ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2")
55
    test "GET /api/v1/repos/:owner/:repo/projectsV2 hides a private repository from a non-member" do
56
      conn = get(build_conn(), ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2")
57 57
      assert_api_error(conn, 404, "not_found")
58 58
    end
59 59
  end
60 60
61 61
  describe "create" do
62
    test "POST /api/v3/repos/:owner/:repo/projectsV2 creates a project", %{conn: conn} do
62
    test "POST /api/v1/repos/:owner/:repo/projectsV2 creates a project", %{conn: conn} do
63 63
      conn =
64
        post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2", %{
64
        post(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2", %{
65 65
          title: "New board"
66 66
        })
67 67

@@ -71,11 +71,11 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

71 71
      assert Projects.get_project_by_number!(repository(), number).title == "New board"
72 72
    end
73 73
74
    test "POST /api/v3/repos/:owner/:repo/projectsV2 accepts a Markdown description", %{
74
    test "POST /api/v1/repos/:owner/:repo/projectsV2 accepts a Markdown description", %{
75 75
      conn: conn
76 76
    } do
77 77
      conn =
78
        post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2", %{
78
        post(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2", %{
79 79
          title: "Stress testing Ox Alpha",
80 80
          description: "## Why\n\nProvider order is under test."
81 81
        })

@@ -84,11 +84,11 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

84 84
               json_response(conn, 201)
85 85
    end
86 86
87
    test "POST /api/v3/repos/:owner/:repo/projectsV2 ignores repository override params", %{
87
    test "POST /api/v1/repos/:owner/:repo/projectsV2 ignores repository override params", %{
88 88
      conn: conn
89 89
    } do
90 90
      conn =
91
        post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2", %{
91
        post(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2", %{
92 92
          title: "Board",
93 93
          owner: "mallory",
94 94
          repo: "elsewhere"

@@ -97,10 +97,10 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

97 97
      assert json_response(conn, 201)["owner"] == "alice"
98 98
    end
99 99
100
    test "POST /api/v3/repos/:owner/:repo/projectsV2 returns 422 without a title", %{
100
    test "POST /api/v1/repos/:owner/:repo/projectsV2 returns 422 without a title", %{
101 101
      conn: conn
102 102
    } do
103
      conn = post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2", %{})
103
      conn = post(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2", %{})
104 104
105 105
      assert %{"errors" => %{"title" => _}} = json_response(conn, 422)
106 106
      assert Projects.list_projects(repository()) == []

@@ -108,21 +108,21 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

108 108
  end
109 109
110 110
  describe "show" do
111
    test "GET /api/v3/repos/:owner/:repo/projectsV2/:project_number returns the project", %{
111
    test "GET /api/v1/repos/:owner/:repo/projectsV2/:project_number returns the project", %{
112 112
      conn: conn
113 113
    } do
114 114
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
115 115
116
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}")
116
      conn = get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}")
117 117
118 118
      assert %{"title" => "Roadmap", "id" => id} = json_response(conn, 200)
119 119
      assert id == project.id
120 120
    end
121 121
122
    test "GET /api/v3/repos/:owner/:repo/projectsV2/:project_number returns 404 when missing", %{
122
    test "GET /api/v1/repos/:owner/:repo/projectsV2/:project_number returns 404 when missing", %{
123 123
      conn: conn
124 124
    } do
125
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999")
125
      conn = get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/999999")
126 126
127 127
      assert_api_error(conn, 404, "not_found")
128 128
    end

@@ -135,7 +135,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

135 135
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
136 136
137 137
      conn =
138
        get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
138
        get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
139 139
140 140
      assert json_response(conn, 200) == %{"items" => []}
141 141
    end

@@ -145,7 +145,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

145 145
      item = project_item_fixture(%{project_id: project.id, values: %{"Status" => "Todo"}})
146 146
147 147
      conn =
148
        get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
148
        get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
149 149
150 150
      assert %{"items" => [rendered]} = json_response(conn, 200)
151 151
      assert rendered["id"] == item.id

@@ -174,7 +174,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

174 174
        )
175 175
176 176
      conn =
177
        get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
177
        get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
178 178
179 179
      body = response(conn, 200)
180 180
      refute body =~ "hidden-api"

@@ -189,7 +189,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

189 189
    test "GET .../projectsV2/:project_number/items returns 404 for a missing project", %{
190 190
      conn: conn
191 191
    } do
192
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999/items")
192
      conn = get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/999999/items")
193 193
194 194
      assert_api_error(conn, 404, "not_found")
195 195
    end

@@ -228,7 +228,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

228 228
      conn =
229 229
        get(
230 230
          conn,
231
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items?promise_state=LIVE"
231
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items?promise_state=LIVE"
232 232
        )
233 233
234 234
      assert %{

@@ -286,7 +286,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

286 286
      conn =
287 287
        get(
288 288
          conn,
289
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items?promise_state=GATED&bounty_candidate=true"
289
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items?promise_state=GATED&bounty_candidate=true"
290 290
        )
291 291
292 292
      assert %{

@@ -327,7 +327,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

327 327
      conn =
328 328
        get(
329 329
          conn,
330
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}/events"
330
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}/events"
331 331
        )
332 332
333 333
      assert %{

@@ -364,7 +364,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

364 364
365 365
      conn =
366 366
        build_conn()
367
        |> get(~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
367
        |> get(~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
368 368
369 369
      assert %{"items" => [_item]} = json_response(conn, 200)
370 370
    end

@@ -404,7 +404,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

404 404
        )
405 405
406 406
      conn =
407
        get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
407
        get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items")
408 408
409 409
      assert %{"items" => [%{"openagents" => %{"promise" => %{"record" => record}}}]} =
410 410
               json_response(conn, 200)

@@ -428,7 +428,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

428 428
      conn =
429 429
        post(
430 430
          conn,
431
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
431
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
432 432
          %{
433 433
            issue_number: issue.number
434 434
          }

@@ -453,7 +453,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

453 453
      conn =
454 454
        post(
455 455
          conn,
456
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
456
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
457 457
          %{issue: %{owner: "SourceOrg", repo: "source-api", number: issue.number}}
458 458
        )
459 459

@@ -495,7 +495,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

495 495
      conn =
496 496
        post(
497 497
          conn,
498
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
498
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
499 499
          %{issue: %{owner: "ReadableOrg", repo: "readable-api", number: issue.number}}
500 500
        )
501 501

@@ -522,7 +522,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

522 522
      conn =
523 523
        post(
524 524
          put_forge_api_token(build_conn(), "project-reader", "carol"),
525
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
525
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
526 526
          %{issue: %{owner: "WriterOrg", repo: "writer-api", number: issue.number}}
527 527
        )
528 528

@@ -537,7 +537,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

537 537
      conn =
538 538
        post(
539 539
          conn,
540
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
540
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
541 541
          %{issue: %{owner: "NoSuchOrg", repo: "no-such-api", number: 1}}
542 542
        )
543 543

@@ -554,7 +554,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

554 554
      conn =
555 555
        post(
556 556
          conn,
557
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
557
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
558 558
          %{issue: %{owner: "SourceOrg", repo: "empty-api", number: 999_999}}
559 559
        )
560 560

@@ -576,7 +576,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

576 576
      conn =
577 577
        post(
578 578
          conn,
579
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
579
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
580 580
          %{issue: %{owner: "SourceOrg", repo: "same-number-api", number: source_issue.number}}
581 581
        )
582 582

@@ -594,7 +594,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

594 594
595 595
      {:ok, issue} = Issues.create_issue(source, %{title: "Added once"})
596 596
      body = %{issue: %{owner: "SourceOrg", repo: "repeat-api", number: issue.number}}
597
      path = ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items"
597
      path = ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items"
598 598
599 599
      assert %{"items" => [%{"id" => id}]} = json_response(post(conn, path, body), 201)
600 600
      assert %{"items" => [%{"id" => ^id}]} = json_response(post(recycle(conn), path, body), 200)

@@ -613,7 +613,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

613 613
      conn =
614 614
        post(
615 615
          conn,
616
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
616
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
617 617
          %{issue: %{owner: "SecretOrg", repo: "secret-api", number: issue.number}}
618 618
        )
619 619

@@ -629,7 +629,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

629 629
      conn =
630 630
        post(
631 631
          conn,
632
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
632
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
633 633
          %{
634 634
            issue_number: issue.number,
635 635
            values: %{"Status" => "In Progress"}

@@ -648,7 +648,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

648 648
      conn =
649 649
        post(
650 650
          conn,
651
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
651
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
652 652
          %{
653 653
            issue_number: issue.number,
654 654
            values: "not-a-map"

@@ -666,7 +666,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

666 666
      conn =
667 667
        post(
668 668
          conn,
669
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
669
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
670 670
          %{}
671 671
        )
672 672

@@ -679,7 +679,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

679 679
      conn =
680 680
        post(
681 681
          conn,
682
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
682
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
683 683
          %{
684 684
            issue_number: "not-a-number"
685 685
          }

@@ -696,7 +696,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

696 696
      conn =
697 697
        post(
698 698
          conn,
699
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
699
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
700 700
          %{
701 701
            issue_number: 999_999
702 702
          }

@@ -710,7 +710,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

710 710
      issue: issue
711 711
    } do
712 712
      conn =
713
        post(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999/items", %{
713
        post(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/999999/items", %{
714 714
          issue_number: issue.number
715 715
        })
716 716

@@ -733,7 +733,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

733 733
      conn =
734 734
        patch(
735 735
          conn,
736
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
736
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
737 737
          %{
738 738
            values: %{"Status" => "Done"}
739 739
          }

@@ -761,7 +761,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

761 761
      conn =
762 762
        patch(
763 763
          conn,
764
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
764
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
765 765
          %{values: %{"Status" => "Done"}}
766 766
        )
767 767

@@ -777,7 +777,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

777 777
      conn =
778 778
        patch(
779 779
          conn,
780
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
780
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
781 781
          %{
782 782
            values: %{"Priority" => "P1"}
783 783
          }

@@ -795,7 +795,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

795 795
      conn =
796 796
        patch(
797 797
          conn,
798
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
798
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
799 799
          %{
800 800
            values: "not-a-map"
801 801
          }

@@ -812,7 +812,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

812 812
      conn =
813 813
        patch(
814 814
          conn,
815
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/999999",
815
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/999999",
816 816
          %{
817 817
            values: %{"Status" => "Done"}
818 818
          }

@@ -831,7 +831,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

831 831
      conn =
832 832
        get(
833 833
          conn,
834
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
834
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
835 835
        )
836 836
837 837
      assert json_response(conn, 200) == %{"fields" => []}

@@ -851,7 +851,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

851 851
      conn =
852 852
        get(
853 853
          conn,
854
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
854
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
855 855
        )
856 856
857 857
      assert %{"fields" => [rendered]} = json_response(conn, 200)

@@ -864,7 +864,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

864 864
    test "GET .../projectsV2/:project_number/fields returns 404 for a missing project", %{
865 865
      conn: conn
866 866
    } do
867
      conn = get(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/999999/fields")
867
      conn = get(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/999999/fields")
868 868
869 869
      assert_api_error(conn, 404, "not_found")
870 870
    end

@@ -875,7 +875,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

875 875
      conn =
876 876
        post(
877 877
          conn,
878
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
878
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
879 879
          %{
880 880
            name: "Status",
881 881
            data_type: "single_select",

@@ -905,7 +905,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

905 905
      conn =
906 906
        post(
907 907
          conn,
908
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
908
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
909 909
          %{}
910 910
        )
911 911

@@ -924,7 +924,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

924 924
      conn =
925 925
        post(
926 926
          conn,
927
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
927
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
928 928
          %{
929 929
            name: "Status",
930 930
            data_type: "text",

@@ -957,39 +957,39 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

957 957
958 958
      assert get(
959 959
               mallory_conn,
960
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
960
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
961 961
             )
962 962
             |> api_error_code(404) == "not_found"
963 963
964 964
      assert get(
965 965
               recycle(mallory_conn),
966
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items"
966
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items"
967 967
             )
968 968
             |> api_error_code(404) == "not_found"
969 969
970 970
      assert get(
971 971
               recycle(mallory_conn),
972
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
972
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
973 973
             )
974 974
             |> api_error_code(404) == "not_found"
975 975
976 976
      assert post(
977 977
               recycle(mallory_conn),
978
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
978
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
979 979
               %{issue_number: issue.number}
980 980
             )
981 981
             |> api_error_code(404) == "not_found"
982 982
983 983
      assert patch(
984 984
               recycle(mallory_conn),
985
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
985
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
986 986
               %{values: %{"Status" => "Done"}}
987 987
             )
988 988
             |> api_error_code(404) == "not_found"
989 989
990 990
      assert post(
991 991
               recycle(mallory_conn),
992
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
992
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
993 993
               %{name: "Priority", data_type: "text"}
994 994
             )
995 995
             |> api_error_code(404) == "not_found"

@@ -1001,7 +1001,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1001 1001
      other_repository =
1002 1002
        repository_fixture(%{owner: "OtherOrg", name: "other-private", visibility: "private"})
1003 1003
1004
      assert post(conn, ~p"/api/v3/repos/OtherOrg/other-private/projectsV2", %{
1004
      assert post(conn, ~p"/api/v1/repos/OtherOrg/other-private/projectsV2", %{
1005 1005
               title: "Not Alice's"
1006 1006
             })
1007 1007
             |> api_error_code(404) == "not_found"

@@ -1015,7 +1015,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1015 1015
      project = project_fixture(%{title: "Roadmap", owner: "alice", state: "open"})
1016 1016
1017 1017
      conn =
1018
        patch(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1018
        patch(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1019 1019
          title: "Stress testing Ox Alpha",
1020 1020
          description: "## Why\n\nProvider order is under test.",
1021 1021
          state: "closed"

@@ -1034,7 +1034,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1034 1034
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
1035 1035
1036 1036
      conn =
1037
        patch(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1037
        patch(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1038 1038
          title: "Renamed",
1039 1039
          number: 9999,
1040 1040
          owner: "mallory"

@@ -1050,7 +1050,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1050 1050
      project = project_fixture(%{title: "Roadmap", owner: "alice", state: "open"})
1051 1051
1052 1052
      conn =
1053
        patch(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1053
        patch(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1054 1054
          state: "sideways"
1055 1055
        })
1056 1056

@@ -1064,7 +1064,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1064 1064
1065 1065
      assert patch(
1066 1066
               mallory,
1067
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}",
1067
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}",
1068 1068
               %{title: "Mine now"}
1069 1069
             )
1070 1070
             |> api_error_code(404) == "not_found"

@@ -1080,7 +1080,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1080 1080
      created =
1081 1081
        post(
1082 1082
          conn,
1083
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes",
1083
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes",
1084 1084
          %{body: "- paused lane 3"}
1085 1085
        )
1086 1086

@@ -1096,7 +1096,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1096 1096
      listed =
1097 1097
        get(
1098 1098
          recycle(conn),
1099
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes"
1099
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes"
1100 1100
        )
1101 1101
1102 1102
      assert %{"notes" => [note], "page" => 1, "per_page" => per_page, "total_count" => 1} =

@@ -1120,7 +1120,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1120 1120
      page_two =
1121 1121
        get(
1122 1122
          conn,
1123
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes?page=2"
1123
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes?page=2"
1124 1124
        )
1125 1125
1126 1126
      assert %{"notes" => notes, "page" => 2} = json_response(page_two, 200)

@@ -1129,7 +1129,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1129 1129
      activity =
1130 1130
        get(
1131 1131
          recycle(conn),
1132
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes?kind=activity"
1132
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes?kind=activity"
1133 1133
        )
1134 1134
1135 1135
      assert %{"notes" => [%{"kind" => "activity", "body" => body}], "total_count" => 1} =

@@ -1148,20 +1148,20 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1148 1148
1149 1149
      assert patch(
1150 1150
               mallory_conn,
1151
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}",
1151
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}",
1152 1152
               %{body: "not mine"}
1153 1153
             )
1154 1154
             |> api_error_code(403) == "forbidden"
1155 1155
1156 1156
      assert delete(
1157 1157
               recycle(mallory_conn),
1158
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}"
1158
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}"
1159 1159
             )
1160 1160
             |> api_error_code(403) == "forbidden"
1161 1161
1162 1162
      assert patch(
1163 1163
               conn,
1164
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}",
1164
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}",
1165 1165
               %{body: "mine, edited"}
1166 1166
             )
1167 1167
             |> json_response(200)

@@ -1169,7 +1169,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1169 1169
1170 1170
      assert delete(
1171 1171
               recycle(conn),
1172
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}"
1172
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{note.id}"
1173 1173
             )
1174 1174
             |> response(204)
1175 1175

@@ -1185,14 +1185,14 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1185 1185
1186 1186
      assert patch(
1187 1187
               conn,
1188
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{activity.id}",
1188
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{activity.id}",
1189 1189
               %{body: "rewritten"}
1190 1190
             )
1191 1191
             |> api_error_code(403) == "forbidden"
1192 1192
1193 1193
      assert delete(
1194 1194
               recycle(conn),
1195
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{activity.id}"
1195
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes/#{activity.id}"
1196 1196
             )
1197 1197
             |> api_error_code(403) == "forbidden"
1198 1198

@@ -1211,13 +1211,13 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1211 1211
1212 1212
      assert get(
1213 1213
               build_conn(),
1214
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes"
1214
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes"
1215 1215
             )
1216 1216
             |> api_error_code(404) == "not_found"
1217 1217
1218 1218
      assert post(
1219 1219
               put_forge_api_token(build_conn(), "project-outsider-notes", "outsider"),
1220
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes",
1220
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes",
1221 1221
               %{body: "mine now"}
1222 1222
             )
1223 1223
             |> api_error_code(404) == "not_found"

@@ -1225,7 +1225,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1225 1225
      assert %{"notes" => [_note]} =
1226 1226
               get(
1227 1227
                 conn,
1228
                 ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes"
1228
                 ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes"
1229 1229
               )
1230 1230
               |> json_response(200)
1231 1231
    end

@@ -1234,7 +1234,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1234 1234
  describe "lifecycle" do
1235 1235
    test "PATCH projectsV2/:number closes, reopens, and archives a project", %{conn: conn} do
1236 1236
      project = project_fixture(%{title: "Roadmap", owner: "alice", state: "open"})
1237
      path = ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1237
      path = ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1238 1238
1239 1239
      assert %{"state" => "closed", "archived" => false} =
1240 1240
               conn |> patch(path, %{state: "closed"}) |> json_response(200)

@@ -1258,7 +1258,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1258 1258
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
1259 1259
1260 1260
      conn =
1261
        patch(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1261
        patch(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}", %{
1262 1262
          archived: "sideways"
1263 1263
        })
1264 1264

@@ -1270,7 +1270,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1270 1270
      conn: conn
1271 1271
    } do
1272 1272
      project = project_fixture(%{title: "Roadmap", owner: "alice", state: "open"})
1273
      path = ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1273
      path = ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1274 1274
1275 1275
      assert json_response(patch(conn, path, %{state: "closed"}), 200)
1276 1276
      assert json_response(patch(conn, path, %{archived: true}), 200)

@@ -1278,7 +1278,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1278 1278
      assert %{"notes" => notes} =
1279 1279
               conn
1280 1280
               |> get(
1281
                 ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes?kind=activity"
1281
                 ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/notes?kind=activity"
1282 1282
               )
1283 1283
               |> json_response(200)
1284 1284

@@ -1292,7 +1292,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1292 1292
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
1293 1293
1294 1294
      conn =
1295
        delete(conn, ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}")
1295
        delete(conn, ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}")
1296 1296
1297 1297
      assert %{"errors" => %{"archived" => [message]}} = json_response(conn, 422)
1298 1298
      assert message =~ "archived"

@@ -1307,7 +1307,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1307 1307
      item = project_item_fixture(%{project_id: project.id, issue_id: issue.id})
1308 1308
      field = project_field_fixture(%{project_id: project.id, name: "Status", data_type: "text"})
1309 1309
1310
      path = ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1310
      path = ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1311 1311
      assert json_response(patch(conn, path, %{archived: true}), 200)
1312 1312
      assert response(delete(conn, path), 204) == ""
1313 1313

@@ -1324,7 +1324,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1324 1324
1325 1325
    test "DELETE projectsV2/:number hides a private repository from a non-member", %{conn: conn} do
1326 1326
      project = project_fixture(%{title: "Alice only", owner: "alice"})
1327
      path = ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1327
      path = ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}"
1328 1328
      assert json_response(patch(conn, path, %{archived: true}), 200)
1329 1329
1330 1330
      mallory = put_forge_api_token(build_conn(), "project-mallory-delete", "mallory")

@@ -1342,7 +1342,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1342 1342
      assert json_response(
1343 1343
               patch(
1344 1344
                 conn,
1345
                 ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{retired.number}",
1345
                 ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{retired.number}",
1346 1346
                 %{archived: true}
1347 1347
               ),
1348 1348
               200

@@ -1350,14 +1350,14 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1350 1350
1351 1351
      assert %{"projects" => listed} =
1352 1352
               conn
1353
               |> get(~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2")
1353
               |> get(~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2")
1354 1354
               |> json_response(200)
1355 1355
1356 1356
      assert Enum.map(listed, & &1["number"]) == [kept.number]
1357 1357
1358 1358
      assert %{"projects" => all} =
1359 1359
               conn
1360
               |> get(~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2?archived=true")
1360
               |> get(~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2?archived=true")
1361 1361
               |> json_response(200)
1362 1362
1363 1363
      assert Enum.sort(Enum.map(all, & &1["number"])) == Enum.sort([kept.number, retired.number])

@@ -1371,7 +1371,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1371 1371
      conn =
1372 1372
        post(
1373 1373
          conn,
1374
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1374
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1375 1375
          %{name: "Status", data_type: "rocket"}
1376 1376
        )
1377 1377

@@ -1381,7 +1381,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1381 1381
1382 1382
    test "POST fields rejects a duplicate name, ignoring case", %{conn: conn} do
1383 1383
      project = project_fixture(%{title: "Roadmap", owner: "alice"})
1384
      path = ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
1384
      path = ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields"
1385 1385
1386 1386
      assert json_response(post(conn, path, %{name: "Status", data_type: "text"}), 201)
1387 1387

@@ -1399,7 +1399,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1399 1399
        assert json_response(
1400 1400
                 post(
1401 1401
                   conn,
1402
                   ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{board.number}/fields",
1402
                   ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{board.number}/fields",
1403 1403
                   %{name: "Status", data_type: "text"}
1404 1404
                 ),
1405 1405
                 201

@@ -1413,7 +1413,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1413 1413
      conn =
1414 1414
        post(
1415 1415
          conn,
1416
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1416
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1417 1417
          %{name: "Status", data_type: "single_select"}
1418 1418
        )
1419 1419

@@ -1427,7 +1427,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1427 1427
      conn =
1428 1428
        post(
1429 1429
          conn,
1430
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1430
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1431 1431
          %{name: "Status", data_type: "single_select", options: %{values: ["Todo", "Todo"]}}
1432 1432
        )
1433 1433

@@ -1441,7 +1441,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1441 1441
      conn =
1442 1442
        post(
1443 1443
          conn,
1444
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1444
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1445 1445
          %{
1446 1446
            name: "Status",
1447 1447
            data_type: "single_select",

@@ -1467,7 +1467,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1467 1467
      conn =
1468 1468
        post(
1469 1469
          conn,
1470
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1470
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields",
1471 1471
          %{name: "Points", data_type: "number", options: %{values: ["1", "2"]}}
1472 1472
        )
1473 1473

@@ -1501,7 +1501,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1501 1501
      conn =
1502 1502
        patch(
1503 1503
          conn,
1504
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1504
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1505 1505
          %{name: "Stage"}
1506 1506
        )
1507 1507

@@ -1518,7 +1518,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1518 1518
      conn =
1519 1519
        patch(
1520 1520
          conn,
1521
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1521
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1522 1522
          %{data_type: "text"}
1523 1523
        )
1524 1524

@@ -1534,7 +1534,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1534 1534
      conn =
1535 1535
        patch(
1536 1536
          conn,
1537
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1537
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1538 1538
          %{options: %{values: ["Todo", "In progress", "Done"]}}
1539 1539
        )
1540 1540

@@ -1552,7 +1552,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1552 1552
      conn =
1553 1553
        patch(
1554 1554
          conn,
1555
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1555
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1556 1556
          %{options: %{values: ["Done"]}}
1557 1557
        )
1558 1558

@@ -1576,7 +1576,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1576 1576
      conn =
1577 1577
        patch(
1578 1578
          conn,
1579
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1579
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1580 1580
          %{name: "Priority"}
1581 1581
        )
1582 1582

@@ -1593,7 +1593,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1593 1593
      conn =
1594 1594
        patch(
1595 1595
          conn,
1596
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{other.number}/fields/#{field.id}",
1596
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{other.number}/fields/#{field.id}",
1597 1597
          %{name: "Stage"}
1598 1598
        )
1599 1599

@@ -1608,7 +1608,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1608 1608
1609 1609
      assert patch(
1610 1610
               mallory,
1611
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1611
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}",
1612 1612
               %{name: "Mine now"}
1613 1613
             )
1614 1614
             |> api_error_code(404) == "not_found"

@@ -1640,7 +1640,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1640 1640
      conn =
1641 1641
        delete(
1642 1642
          conn,
1643
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}"
1643
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}"
1644 1644
        )
1645 1645
1646 1646
      assert response(conn, 204) == ""

@@ -1657,7 +1657,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1657 1657
      conn =
1658 1658
        delete(
1659 1659
          conn,
1660
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}"
1660
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}"
1661 1661
        )
1662 1662
1663 1663
      assert %{"errors" => %{"name" => [message]}} = json_response(conn, 422)

@@ -1674,7 +1674,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1674 1674
1675 1675
      assert delete(
1676 1676
               mallory,
1677
               ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}"
1677
               ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/fields/#{field.id}"
1678 1678
             )
1679 1679
             |> api_error_code(404) == "not_found"
1680 1680

@@ -1708,7 +1708,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1708 1708
      conn =
1709 1709
        post(
1710 1710
          conn,
1711
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
1711
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
1712 1712
          %{
1713 1713
            issue_number: issue.number,
1714 1714
            values: %{"Status" => "Done", "Points" => 3, "Due" => "2026-09-01"}

@@ -1727,7 +1727,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1727 1727
      conn =
1728 1728
        post(
1729 1729
          conn,
1730
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
1730
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
1731 1731
          %{issue_number: issue.number, values: %{"Status" => "Sideways"}}
1732 1732
        )
1733 1733

@@ -1741,7 +1741,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1741 1741
      project: project,
1742 1742
      issue: issue
1743 1743
    } do
1744
      path = ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items"
1744
      path = ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items"
1745 1745
1746 1746
      assert %{"errors" => %{"values" => [_ | _]}} =
1747 1747
               conn

@@ -1764,7 +1764,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1764 1764
      conn =
1765 1765
        post(
1766 1766
          conn,
1767
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
1767
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items",
1768 1768
          %{issue_number: issue.number, values: %{"Squad" => "Platform"}}
1769 1769
        )
1770 1770

@@ -1786,7 +1786,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1786 1786
      conn =
1787 1787
        patch(
1788 1788
          conn,
1789
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
1789
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}",
1790 1790
          %{values: %{"Status" => "Sideways"}}
1791 1791
        )
1792 1792

@@ -1810,7 +1810,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1810 1810
      conn =
1811 1811
        delete(
1812 1812
          conn,
1813
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1813
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1814 1814
        )
1815 1815
1816 1816
      assert response(conn, 204) == ""

@@ -1824,7 +1824,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1824 1824
      item: item
1825 1825
    } do
1826 1826
      path =
1827
        ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1827
        ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1828 1828
1829 1829
      assert response(delete(conn, path), 204) == ""
1830 1830
      assert_api_error(delete(recycle(conn), path), 404, "not_found")

@@ -1839,7 +1839,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1839 1839
      conn =
1840 1840
        delete(
1841 1841
          mallory_conn,
1842
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1842
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1843 1843
        )
1844 1844
1845 1845
      assert_api_error(conn, 404, "not_found")

@@ -1868,7 +1868,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1868 1868
      conn =
1869 1869
        delete(
1870 1870
          conn,
1871
          ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1871
          ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}"
1872 1872
        )
1873 1873
1874 1874
      assert_api_error(conn, 404, "not_found")

@@ -1898,7 +1898,7 @@ defmodule OpenAgentsWeb.ProjectControllerTest do

1898 1898
1899 1899
    defp move_path(project, item),
1900 1900
      do:
1901
        ~p"/api/v3/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}/move"
1901
        ~p"/api/v1/repos/ProjectTestOrg/project-api/projectsV2/#{project.number}/items/#{item.id}/move"
1902 1902
1903 1903
    defp add_item(project, title, values) do
1904 1904
      {:ok, issue} = Issues.create_issue(repository(), %{title: title})
test/openagents_web/controllers/pull_request_controller_test.exs modified +14 -14

@@ -25,7 +25,7 @@ defmodule OpenAgentsWeb.PullRequestControllerTest do

25 25
    :ok
26 26
  end
27 27
28
  test "POST, GET, and PATCH /api/v3/repos/:owner/:repo/pulls manage a pull request", %{
28
  test "POST, GET, and PATCH /api/v1/repos/:owner/:repo/pulls manage a pull request", %{
29 29
    conn: conn
30 30
  } do
31 31
    target = repository_fixture()

@@ -38,7 +38,7 @@ defmodule OpenAgentsWeb.PullRequestControllerTest do

38 38
    conn = put_forge_api_token(conn, "pull-request-lifecycle", target)
39 39
40 40
    create_conn =
41
      post(conn, "/api/v3/repos/#{target.owner}/#{target.name}/pulls", %{
41
      post(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls", %{
42 42
        title: "Add pull request support",
43 43
        body: "Issue-backed pull request",
44 44
        head_repository: "#{source.owner}/#{source.name}",

@@ -55,13 +55,13 @@ defmodule OpenAgentsWeb.PullRequestControllerTest do

55 55
             "base" => %{"ref" => "main"}
56 56
           } = json_response(create_conn, 201)
57 57
58
    show_conn = get(conn, "/api/v3/repos/#{target.owner}/#{target.name}/pulls/#{number}")
58
    show_conn = get(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls/#{number}")
59 59
60 60
    assert %{"number" => ^number, "title" => "Add pull request support"} =
61 61
             json_response(show_conn, 200)
62 62
63 63
    update_conn =
64
      patch(conn, "/api/v3/repos/#{target.owner}/#{target.name}/pulls/#{number}", %{
64
      patch(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls/#{number}", %{
65 65
        title: "Ship pull request support",
66 66
        state: "closed",
67 67
        draft: false

@@ -75,12 +75,12 @@ defmodule OpenAgentsWeb.PullRequestControllerTest do

75 75
           } = json_response(update_conn, 200)
76 76
  end
77 77
78
  test "POST /api/v3/repos/:owner/:repo/pulls rejects a disabled repository", %{conn: conn} do
78
  test "POST /api/v1/repos/:owner/:repo/pulls rejects a disabled repository", %{conn: conn} do
79 79
    target = repository_fixture(%{pull_requests_enabled: false})
80 80
    conn = put_forge_api_token(conn, "pull-request-disabled", target)
81 81
82 82
    conn =
83
      post(conn, "/api/v3/repos/#{target.owner}/#{target.name}/pulls", %{
83
      post(conn, "/api/v1/repos/#{target.owner}/#{target.name}/pulls", %{
84 84
        title: "Cannot open",
85 85
        head_repository: "#{target.owner}/#{target.name}",
86 86
        head: "main"

@@ -90,44 +90,44 @@ defmodule OpenAgentsWeb.PullRequestControllerTest do

90 90
             json_response(conn, 409)
91 91
  end
92 92
93
  test "POST /api/v3/repos/:owner/:repo/pulls requires a bearer token", %{conn: conn} do
93
  test "POST /api/v1/repos/:owner/:repo/pulls requires a bearer token", %{conn: conn} do
94 94
    repository = repository_fixture()
95 95
96 96
    conn =
97
      post(conn, "/api/v3/repos/#{repository.owner}/#{repository.name}/pulls", %{
97
      post(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}/pulls", %{
98 98
        title: "Unauthenticated pull request"
99 99
      })
100 100
101 101
    assert %{"error" => "invalid_api_token"} = json_response(conn, 401)
102 102
  end
103 103
104
  test "GET /api/v3/repos/:owner/:repo/pulls lists pull requests", %{conn: conn} do
104
  test "GET /api/v1/repos/:owner/:repo/pulls lists pull requests", %{conn: conn} do
105 105
    repository = repository_fixture()
106 106
107 107
    conn =
108
      get(conn, "/api/v3/repos/#{repository.owner}/#{repository.name}/pulls")
108
      get(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}/pulls")
109 109
110 110
    assert [] == json_response(conn, 200)
111 111
  end
112 112
113
  test "PATCH /api/v3/repos/:owner/:repo updates the owner pull request setting", %{conn: conn} do
113
  test "PATCH /api/v1/repos/:owner/:repo updates the owner pull request setting", %{conn: conn} do
114 114
    repository = repository_fixture()
115 115
    conn = put_forge_api_token(conn, "pull-request-settings", repository)
116 116
117 117
    conn =
118
      patch(conn, "/api/v3/repos/#{repository.owner}/#{repository.name}", %{
118
      patch(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}", %{
119 119
        pull_requests_enabled: false
120 120
      })
121 121
122 122
    assert %{"pull_requests_enabled" => false} = json_response(conn, 200)
123 123
  end
124 124
125
  test "PATCH /api/v3/repos/:owner/:repo requires an owner", %{conn: conn} do
125
  test "PATCH /api/v1/repos/:owner/:repo requires an owner", %{conn: conn} do
126 126
    repository = repository_fixture()
127 127
    conn = put_forge_api_token(conn, "pull-request-nonowner")
128 128
129 129
    conn =
130
      patch(conn, "/api/v3/repos/#{repository.owner}/#{repository.name}", %{
130
      patch(conn, "/api/v1/repos/#{repository.owner}/#{repository.name}", %{
131 131
        pull_requests_enabled: false
132 132
      })
133 133
test/openagents_web/controllers/push_receipt_controller_test.exs modified +7 -7

@@ -69,7 +69,7 @@ defmodule OpenAgentsWeb.PushReceiptControllerTest do

69 69
70 70
      body =
71 71
        conn
72
        |> get(~p"/api/v3/repos/receipt-owner/open-work/pushes")
72
        |> get(~p"/api/v1/repos/receipt-owner/open-work/pushes")
73 73
        |> json_response(200)
74 74
75 75
      assert body["repo"] == "receipt-owner/open-work"

@@ -96,7 +96,7 @@ defmodule OpenAgentsWeb.PushReceiptControllerTest do

96 96
97 97
      body =
98 98
        conn
99
        |> get(~p"/api/v3/repos/receipt-owner/open-work/pushes/1")
99
        |> get(~p"/api/v1/repos/receipt-owner/open-work/pushes/1")
100 100
        |> json_response(200)
101 101
102 102
      assert body["wal_seq"] == 1

@@ -113,7 +113,7 @@ defmodule OpenAgentsWeb.PushReceiptControllerTest do

113 113
114 114
      body =
115 115
        conn
116
        |> get(~p"/api/v3/repos/receipt-owner/open-work/pushes/1")
116
        |> get(~p"/api/v1/repos/receipt-owner/open-work/pushes/1")
117 117
        |> json_response(200)
118 118
119 119
      assert body["link"] == WAL.entry_link(head)

@@ -136,7 +136,7 @@ defmodule OpenAgentsWeb.PushReceiptControllerTest do

136 136
137 137
      body =
138 138
        conn
139
        |> get(~p"/api/v3/repos/receipt-owner/open-work/pushes")
139
        |> get(~p"/api/v1/repos/receipt-owner/open-work/pushes")
140 140
        |> json_response(200)
141 141
142 142
      assert body["pushes"] == []

@@ -147,7 +147,7 @@ defmodule OpenAgentsWeb.PushReceiptControllerTest do

147 147
      seed_wal!(repository.storage_key, 1)
148 148
149 149
      assert conn
150
             |> get(~p"/api/v3/repos/receipt-owner/open-work/pushes/9")
150
             |> get(~p"/api/v1/repos/receipt-owner/open-work/pushes/9")
151 151
             |> json_response(404)
152 152
    end
153 153
  end

@@ -157,7 +157,7 @@ defmodule OpenAgentsWeb.PushReceiptControllerTest do

157 157
      seed_wal!(repository.storage_key, 1)
158 158
159 159
      assert conn
160
             |> get(~p"/api/v3/repos/receipt-owner/closed-work/pushes")
160
             |> get(~p"/api/v1/repos/receipt-owner/closed-work/pushes")
161 161
             |> json_response(404)
162 162
    end
163 163

@@ -169,7 +169,7 @@ defmodule OpenAgentsWeb.PushReceiptControllerTest do

169 169
170 170
      body =
171 171
        conn
172
        |> get(~p"/api/v3/repos/receipt-owner/closed-work/pushes")
172
        |> get(~p"/api/v1/repos/receipt-owner/closed-work/pushes")
173 173
        |> json_response(200)
174 174
175 175
      assert Enum.map(body["pushes"], & &1["wal_seq"]) == [0]
test/openagents_web/controllers/repository_controller_test.exs modified +22 -22

@@ -5,14 +5,14 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

5 5
  alias OpenAgents.Forge.{Repos, WAL}
6 6
  alias OpenAgents.Repositories
7 7
8
  test "POST /api/v3/user/repos creates in the authenticated GitHub namespace", %{conn: conn} do
8
  test "POST /api/v1/user/repos creates in the authenticated GitHub namespace", %{conn: conn} do
9 9
    user = github_user("repository-api-create", "octavia")
10 10
11 11
    response =
12 12
      conn
13 13
      |> authorize(user)
14 14
      |> put_req_header("idempotency-key", "repo-create-1")
15
      |> post(~p"/api/v3/user/repos", %{
15
      |> post(~p"/api/v1/user/repos", %{
16 16
        name: "My-Project",
17 17
        description: "An API-created repository",
18 18
        private: true,

@@ -43,20 +43,20 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

43 43
    assert %{"code" => "invalid_idempotency_key"} =
44 44
             conn
45 45
             |> authorize(user)
46
             |> post(~p"/api/v3/user/repos", %{name: "missing-key"})
46
             |> post(~p"/api/v1/user/repos", %{name: "missing-key"})
47 47
             |> json_response(400)
48 48
49 49
    first =
50 50
      conn
51 51
      |> authorize(user)
52 52
      |> put_req_header("idempotency-key", "same-key")
53
      |> post(~p"/api/v3/user/repos", %{name: "same-request"})
53
      |> post(~p"/api/v1/user/repos", %{name: "same-request"})
54 54
55 55
    replayed =
56 56
      conn
57 57
      |> authorize(user)
58 58
      |> put_req_header("idempotency-key", "same-key")
59
      |> post(~p"/api/v3/user/repos", %{name: "same-request"})
59
      |> post(~p"/api/v1/user/repos", %{name: "same-request"})
60 60
61 61
    assert json_response(first, 202)["id"] == json_response(replayed, 202)["id"]
62 62

@@ -64,7 +64,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

64 64
             conn
65 65
             |> authorize(user)
66 66
             |> put_req_header("idempotency-key", "same-key")
67
             |> post(~p"/api/v3/user/repos", %{name: "different-request"})
67
             |> post(~p"/api/v1/user/repos", %{name: "different-request"})
68 68
             |> json_response(409)
69 69
  end
70 70

@@ -91,18 +91,18 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

91 91
92 92
    assert %{"id" => public_id, "permissions" => %{"pull" => true, "push" => false}} =
93 93
             conn
94
             |> get(~p"/api/v3/repos/visible-owner/public-repo")
94
             |> get(~p"/api/v1/repos/visible-owner/public-repo")
95 95
             |> json_response(200)
96 96
97 97
    assert public_id == public_repository.id
98 98
99 99
    assert conn
100
           |> get(~p"/api/v3/repos/visible-owner/private-repo")
100
           |> get(~p"/api/v1/repos/visible-owner/private-repo")
101 101
           |> json_response(404)
102 102
103 103
    assert conn
104 104
           |> authorize(viewer)
105
           |> get(~p"/api/v3/repos/visible-owner/private-repo")
105
           |> get(~p"/api/v1/repos/visible-owner/private-repo")
106 106
           |> json_response(404)
107 107
108 108
    {:ok, _membership} = Repositories.add_member(private_repository, viewer, "viewer")

@@ -110,13 +110,13 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

110 110
    assert %{"id" => private_id, "permissions" => %{"pull" => true, "push" => false}} =
111 111
             conn
112 112
             |> authorize(viewer)
113
             |> get(~p"/api/v3/repos/visible-owner/private-repo")
113
             |> get(~p"/api/v1/repos/visible-owner/private-repo")
114 114
             |> json_response(200)
115 115
116 116
    assert private_id == private_repository.id
117 117
  end
118 118
119
  test "GET /api/v3/user/repos returns a bounded visible list", %{conn: conn} do
119
  test "GET /api/v1/user/repos returns a bounded visible list", %{conn: conn} do
120 120
    user = github_user("repository-api-list", "repo-list-owner")
121 121
122 122
    Enum.each(1..3, fn number ->

@@ -131,7 +131,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

131 131
    first =
132 132
      conn
133 133
      |> authorize(user)
134
      |> get(~p"/api/v3/user/repos?per_page=2")
134
      |> get(~p"/api/v1/user/repos?per_page=2")
135 135
      |> json_response(200)
136 136
137 137
    assert length(first["repositories"]) == 2

@@ -140,7 +140,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

140 140
    second =
141 141
      conn
142 142
      |> authorize(user)
143
      |> get(~p"/api/v3/user/repos?per_page=2&after=#{first["next_cursor"]}")
143
      |> get(~p"/api/v1/user/repos?per_page=2&after=#{first["next_cursor"]}")
144 144
      |> json_response(200)
145 145
146 146
    assert MapSet.disjoint?(

@@ -149,7 +149,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

149 149
           )
150 150
  end
151 151
152
  test "GET /api/v3/user/repos filters by GitHub namespace", %{conn: conn} do
152
  test "GET /api/v1/user/repos filters by GitHub namespace", %{conn: conn} do
153 153
    user = github_user("repository-api-list-namespace", "repo-list-filter")
154 154
155 155
    assert {:ok, _repository, :created} =

@@ -162,7 +162,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

162 162
    response =
163 163
      conn
164 164
      |> authorize(user)
165
      |> get(~p"/api/v3/user/repos?namespace=repo-list-filter&per_page=10")
165
      |> get(~p"/api/v1/user/repos?namespace=repo-list-filter&per_page=10")
166 166
      |> json_response(200)
167 167
168 168
    assert Enum.map(response["repositories"], & &1["full_name"]) == [

@@ -172,7 +172,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

172 172
    assert %{"repositories" => []} =
173 173
             conn
174 174
             |> authorize(user)
175
             |> get(~p"/api/v3/user/repos?namespace=another-owner&per_page=10")
175
             |> get(~p"/api/v1/user/repos?namespace=another-owner&per_page=10")
176 176
             |> json_response(200)
177 177
  end
178 178

@@ -191,18 +191,18 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

191 191
    assert conn
192 192
           |> authorize(user)
193 193
           |> put_req_header("idempotency-key", "quota-first")
194
           |> post(~p"/api/v3/user/repos", %{name: "first"})
194
           |> post(~p"/api/v1/user/repos", %{name: "first"})
195 195
           |> json_response(202)
196 196
197 197
    assert %{"code" => "repository_quota_exceeded"} =
198 198
             conn
199 199
             |> authorize(user)
200 200
             |> put_req_header("idempotency-key", "quota-second")
201
             |> post(~p"/api/v3/user/repos", %{name: "second"})
201
             |> post(~p"/api/v1/user/repos", %{name: "second"})
202 202
             |> json_response(422)
203 203
  end
204 204
205
  test "DELETE /api/v3/repos/:owner/:repo removes an owned repository and its storage", %{
205
  test "DELETE /api/v1/repos/:owner/:repo removes an owned repository and its storage", %{
206 206
    conn: conn
207 207
  } do
208 208
    configure_repository_storage("repository-api-delete")

@@ -222,7 +222,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

222 222
    response =
223 223
      conn
224 224
      |> authorize(owner)
225
      |> delete("/api/v3/repos/delete-owner/delete-me")
225
      |> delete("/api/v1/repos/delete-owner/delete-me")
226 226
227 227
    assert response.status == 204
228 228
    assert response.resp_body == ""

@@ -235,7 +235,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

235 235
    refute File.exists?(bare_path)
236 236
  end
237 237
238
  test "DELETE /api/v3/repos/:owner/:repo permits only repository owners", %{conn: conn} do
238
  test "DELETE /api/v1/repos/:owner/:repo permits only repository owners", %{conn: conn} do
239 239
    owner = github_user("repository-api-delete-authorization-owner", "protected-owner")
240 240
    maintainer = github_user("repository-api-delete-authorization-maintainer")
241 241

@@ -252,7 +252,7 @@ defmodule OpenAgentsWeb.RepositoryControllerTest do

252 252
    assert %{"code" => "not_found"} =
253 253
             conn
254 254
             |> authorize(maintainer)
255
             |> delete("/api/v3/repos/protected-owner/protected-repository")
255
             |> delete("/api/v1/repos/protected-owner/protected-repository")
256 256
             |> json_response(404)
257 257
258 258
    assert Repositories.get_by_path!("protected-owner", "protected-repository").id ==
test/openagents_web/controllers/repository_import_controller_test.exs modified +11 -11

@@ -17,7 +17,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

17 17
    :ok
18 18
  end
19 19
20
  test "POST /api/v3/user/repos/imports accepts one frozen GitHub snapshot", %{conn: conn} do
20
  test "POST /api/v1/user/repos/imports accepts one frozen GitHub snapshot", %{conn: conn} do
21 21
    user = github_user("repository-import-api", "octavia")
22 22
    assert {:ok, user} = Accounts.store_github_token(user, "gho_import_fixture")
23 23
    main_sha = String.duplicate("a", 40)

@@ -29,7 +29,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

29 29
      conn
30 30
      |> authorize(user)
31 31
      |> put_req_header("idempotency-key", "import-key-1")
32
      |> post(~p"/api/v3/user/repos/imports", %{
32
      |> post(~p"/api/v1/user/repos/imports", %{
33 33
        source: %{provider: "github", repository: "octavia/source-project"},
34 34
        name: "copied-project",
35 35
        private: true

@@ -60,7 +60,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

60 60
    status =
61 61
      conn
62 62
      |> authorize(user)
63
      |> get(~p"/api/v3/repository-imports/#{import_id}")
63
      |> get(~p"/api/v1/repository-imports/#{import_id}")
64 64
65 65
    assert json_response(status, 200)["import"]["id"] == import_id
66 66

@@ -68,7 +68,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

68 68
69 69
    assert conn
70 70
           |> authorize(other_user)
71
           |> get(~p"/api/v3/repository-imports/#{import_id}")
71
           |> get(~p"/api/v1/repository-imports/#{import_id}")
72 72
           |> json_response(404)
73 73
  end
74 74

@@ -83,7 +83,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

83 83
      conn
84 84
      |> authorize(user)
85 85
      |> put_req_header("idempotency-key", "public-import-key")
86
      |> post(~p"/api/v3/user/repos/imports", %{
86
      |> post(~p"/api/v1/user/repos/imports", %{
87 87
        source: %{provider: "github", repository: "octavia/source-project"}
88 88
      })
89 89

@@ -115,7 +115,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

115 115
      conn
116 116
      |> authorize(user)
117 117
      |> put_req_header("idempotency-key", "org-create-key")
118
      |> post(~p"/api/v3/orgs/acme/repos", %{name: "org-project", private: false})
118
      |> post(~p"/api/v1/orgs/acme/repos", %{name: "org-project", private: false})
119 119
120 120
    assert %{
121 121
             "full_name" => "acme/org-project",

@@ -137,7 +137,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

137 137
        conn
138 138
        |> authorize(user)
139 139
        |> put_req_header("idempotency-key", "foreign-import-key")
140
        |> post(~p"/api/v3/user/repos/imports", %{
140
        |> post(~p"/api/v1/user/repos/imports", %{
141 141
          source: %{provider: "github", repository: "tobi/walgit"}
142 142
        })
143 143

@@ -165,7 +165,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

165 165
        conn
166 166
        |> authorize(user)
167 167
        |> put_req_header("idempotency-key", "mirror-key")
168
        |> post(~p"/api/v3/user/repos/imports", %{
168
        |> post(~p"/api/v1/user/repos/imports", %{
169 169
          source: %{provider: "github", repository: "tobi/walgit"},
170 170
          mirror: true
171 171
        })

@@ -199,7 +199,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

199 199
        conn
200 200
        |> authorize(user)
201 201
        |> put_req_header("idempotency-key", "unlicensed-mirror-key")
202
        |> post(~p"/api/v3/user/repos/imports", %{
202
        |> post(~p"/api/v1/user/repos/imports", %{
203 203
          source: %{provider: "github", repository: "tobi/walgit"},
204 204
          mirror: true
205 205
        })

@@ -217,7 +217,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

217 217
        conn
218 218
        |> authorize(user)
219 219
        |> put_req_header("idempotency-key", "private-mirror-key")
220
        |> post(~p"/api/v3/user/repos/imports", %{
220
        |> post(~p"/api/v1/user/repos/imports", %{
221 221
          source: %{provider: "github", repository: "tobi/walgit"},
222 222
          mirror: true
223 223
        })

@@ -232,7 +232,7 @@ defmodule OpenAgentsWeb.RepositoryImportControllerTest do

232 232
        conn
233 233
        |> authorize(user)
234 234
        |> put_req_header("idempotency-key", "owned-projection-key")
235
        |> post(~p"/api/v3/user/repos", %{name: "mine", private: false})
235
        |> post(~p"/api/v1/user/repos", %{name: "mine", private: false})
236 236
237 237
      assert %{"mirror" => false, "upstream" => nil} = json_response(response, 202)
238 238
    end
test/openagents_web/controllers/repository_isolation_controller_test.exs modified +7 -7

@@ -37,11 +37,11 @@ defmodule OpenAgentsWeb.RepositoryIsolationControllerTest do

37 37
  } do
38 38
    assert initial_issue.number == second_issue.number
39 39
40
    assert get(conn, ~p"/api/v3/repos/OpenAgentsInc/openagents.com/issues/1")
40
    assert get(conn, ~p"/api/v1/repos/OpenAgentsInc/openagents.com/issues/1")
41 41
           |> json_response(200)
42 42
           |> Map.fetch!("title") == "Initial issue"
43 43
44
    assert get(recycle(conn), ~p"/api/v3/repos/SecondOrg/second-repo/issues/1")
44
    assert get(recycle(conn), ~p"/api/v1/repos/SecondOrg/second-repo/issues/1")
45 45
           |> json_response(200)
46 46
           |> Map.fetch!("title") == "Second issue"
47 47
  end

@@ -50,7 +50,7 @@ defmodule OpenAgentsWeb.RepositoryIsolationControllerTest do

50 50
    conn: conn,
51 51
    second_issue: second_issue
52 52
  } do
53
    assert patch(conn, ~p"/api/v3/repos/SecondOrg/second-repo/issues/1", %{title: "Crossed"})
53
    assert patch(conn, ~p"/api/v1/repos/SecondOrg/second-repo/issues/1", %{title: "Crossed"})
54 54
           |> api_error_code(404) == "not_found"
55 55
56 56
    assert Issues.get_issue_by_path!("SecondOrg", "second-repo", 1).title == second_issue.title

@@ -66,17 +66,17 @@ defmodule OpenAgentsWeb.RepositoryIsolationControllerTest do

66 66
    {:ok, _initial_milestone} = Milestones.create_milestone(initial, %{title: "Initial M"})
67 67
    {:ok, _second_milestone} = Milestones.create_milestone(second, %{title: "Second M"})
68 68
69
    assert get(conn, ~p"/api/v3/repos/SecondOrg/second-repo/labels/priority")
69
    assert get(conn, ~p"/api/v1/repos/SecondOrg/second-repo/labels/priority")
70 70
           |> json_response(200)
71 71
           |> Map.fetch!("color") == "222222"
72 72
73
    assert get(recycle(conn), ~p"/api/v3/repos/SecondOrg/second-repo/milestones/1")
73
    assert get(recycle(conn), ~p"/api/v1/repos/SecondOrg/second-repo/milestones/1")
74 74
           |> json_response(200)
75 75
           |> Map.fetch!("title") == "Second M"
76 76
  end
77 77
78 78
  test "a resource cannot be read through an unknown repository path", %{conn: conn} do
79
    assert get(conn, ~p"/api/v3/repos/Unknown/nope/issues/1")
79
    assert get(conn, ~p"/api/v1/repos/Unknown/nope/issues/1")
80 80
           |> api_error_code(404) == "not_found"
81 81
  end
82 82

@@ -91,7 +91,7 @@ defmodule OpenAgentsWeb.RepositoryIsolationControllerTest do

91 91
92 92
    assert {:ok, _issue} = Issues.create_issue(private_repository, %{title: "Private"})
93 93
94
    assert get(conn, ~p"/api/v3/repos/PrivateOrg/private-repo/issues/1")
94
    assert get(conn, ~p"/api/v1/repos/PrivateOrg/private-repo/issues/1")
95 95
           |> api_error_code(404) == "not_found"
96 96
  end
97 97
end
test/openagents_web/controllers/reputation_controller_test.exs modified +8 -8

@@ -28,14 +28,14 @@ defmodule OpenAgentsWeb.ReputationControllerTest do

28 28
      |> Map.fetch!("attestations")
29 29
      |> hd()
30 30
31
    keys = build_conn() |> get("/api/v3/reputation/keys") |> json_response(200)
31
    keys = build_conn() |> get("/api/v1/reputation/keys") |> json_response(200)
32 32
    key = Enum.find(keys["keys"], &(&1["key_id"] == published["issuer_key_id"]))
33 33
34 34
    assert published["claim_digest"] == attestation.claim_digest
35 35
    assert Claim.valid_signature?(published["claim"], published["signature"], key["public_key"])
36 36
    assert published["revocation"]["revoked"] == false
37 37
38
    policy = build_conn() |> get("/api/v3/reputation/policy") |> json_response(200)
38
    policy = build_conn() |> get("/api/v1/reputation/policy") |> json_response(200)
39 39
    version = hd(policy["versions"])
40 40
41 41
    assert policy["score"] == nil

@@ -84,7 +84,7 @@ defmodule OpenAgentsWeb.ReputationControllerTest do

84 84
85 85
  test "scoped evidence never publishes a score", context do
86 86
    {:ok, _attestation} = attest(context)
87
    path = "/api/v3/repos/#{path(context)}/reputation/subjects/actor:builder"
87
    path = "/api/v1/repos/#{path(context)}/reputation/subjects/actor:builder"
88 88
89 89
    body = build_conn() |> get(path) |> json_response(200)
90 90

@@ -119,7 +119,7 @@ defmodule OpenAgentsWeb.ReputationControllerTest do

119 119
        ]
120 120
      })
121 121
122
    path = "/api/v3/repos/#{repository.owner}/#{repository.name}/attestations/#{attestation.id}"
122
    path = "/api/v1/repos/#{repository.owner}/#{repository.name}/attestations/#{attestation.id}"
123 123
124 124
    assert build_conn() |> get(path) |> json_response(404)
125 125

@@ -132,11 +132,11 @@ defmodule OpenAgentsWeb.ReputationControllerTest do

132 132
133 133
  test "an unknown attestation is not found", context do
134 134
    assert build_conn()
135
           |> get("/api/v3/repos/#{path(context)}/attestations/#{Ecto.UUID.generate()}")
135
           |> get("/api/v1/repos/#{path(context)}/attestations/#{Ecto.UUID.generate()}")
136 136
           |> json_response(404)
137 137
138 138
    assert build_conn()
139
           |> get("/api/v3/repos/#{path(context)}/attestations/not-a-uuid")
139
           |> get("/api/v1/repos/#{path(context)}/attestations/not-a-uuid")
140 140
           |> json_response(404)
141 141
  end
142 142

@@ -165,10 +165,10 @@ defmodule OpenAgentsWeb.ReputationControllerTest do

165 165
  end
166 166
167 167
  defp issue_path(context),
168
    do: "/api/v3/repos/#{path(context)}/issues/#{context.issue.number}/attestations"
168
    do: "/api/v1/repos/#{path(context)}/issues/#{context.issue.number}/attestations"
169 169
170 170
  defp attestation_path(context, attestation),
171
    do: "/api/v3/repos/#{path(context)}/attestations/#{attestation.id}"
171
    do: "/api/v1/repos/#{path(context)}/attestations/#{attestation.id}"
172 172
173 173
  defp path(context), do: "#{context.repository.owner}/#{context.repository.name}"
174 174
test/openagents_web/controllers/reputation_subject_claim_controller_test.exs modified +12 -12

@@ -17,7 +17,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

17 17
18 18
    created =
19 19
      authed
20
      |> post(~p"/api/v3/reputation/subject-claims", %{
20
      |> post(~p"/api/v1/reputation/subject-claims", %{
21 21
        "subject_kind" => "account",
22 22
        "subject_id" => "user:" <> user.id
23 23
      })

@@ -30,7 +30,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

30 30
    listed =
31 31
      build_conn()
32 32
      |> put_forge_api_token("reputation-claim")
33
      |> get(~p"/api/v3/reputation/subject-claims")
33
      |> get(~p"/api/v1/reputation/subject-claims")
34 34
35 35
    assert [claim] = json_response(listed, 200)["claims"]
36 36
    assert claim["id"] == created["id"]

@@ -40,7 +40,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

40 40
    body =
41 41
      conn
42 42
      |> put_forge_api_token("reputation-claim-wrong")
43
      |> post(~p"/api/v3/reputation/subject-claims", %{
43
      |> post(~p"/api/v1/reputation/subject-claims", %{
44 44
        "subject_kind" => "account",
45 45
        "subject_id" => "user:" <> Ecto.UUID.generate()
46 46
      })

@@ -53,7 +53,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

53 53
    body =
54 54
      conn
55 55
      |> put_forge_api_token("reputation-claim-kind")
56
      |> post(~p"/api/v3/reputation/subject-claims", %{
56
      |> post(~p"/api/v1/reputation/subject-claims", %{
57 57
        "subject_kind" => "solver",
58 58
        "subject_id" => "actor:whoever"
59 59
      })

@@ -68,7 +68,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

68 68
    created =
69 69
      conn
70 70
      |> put_forge_api_token("reputation-claim-review")
71
      |> post(~p"/api/v3/reputation/subject-claims", %{
71
      |> post(~p"/api/v1/reputation/subject-claims", %{
72 72
        "subject_kind" => "account",
73 73
        "subject_id" => "user:" <> user.id
74 74
      })

@@ -78,12 +78,12 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

78 78
79 79
    assert build_conn()
80 80
           |> put_forge_api_token("reputation-claim-review")
81
           |> get(~p"/api/v3/reputation/subject-claims/pending")
81
           |> get(~p"/api/v1/reputation/subject-claims/pending")
82 82
           |> json_response(403)
83 83
84 84
    assert build_conn()
85 85
           |> put_forge_api_token("reputation-claim-review")
86
           |> patch(~p"/api/v3/reputation/subject-claims/#{created["id"]}", %{
86
           |> patch(~p"/api/v1/reputation/subject-claims/#{created["id"]}", %{
87 87
             "status" => "linked"
88 88
           })
89 89
           |> json_response(403)

@@ -95,7 +95,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

95 95
    pending =
96 96
      build_conn()
97 97
      |> put_forge_api_token("reputation-claim-operator")
98
      |> get(~p"/api/v3/reputation/subject-claims/pending")
98
      |> get(~p"/api/v1/reputation/subject-claims/pending")
99 99
      |> json_response(200)
100 100
101 101
    assert Enum.any?(pending["claims"], &(&1["id"] == created["id"]))

@@ -103,7 +103,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

103 103
    linked =
104 104
      build_conn()
105 105
      |> put_forge_api_token("reputation-claim-operator")
106
      |> patch(~p"/api/v3/reputation/subject-claims/#{created["id"]}", %{"status" => "linked"})
106
      |> patch(~p"/api/v1/reputation/subject-claims/#{created["id"]}", %{"status" => "linked"})
107 107
      |> json_response(200)
108 108
109 109
    assert linked["status"] == "linked"

@@ -111,7 +111,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

111 111
112 112
    assert build_conn()
113 113
           |> put_forge_api_token("reputation-claim-operator")
114
           |> patch(~p"/api/v3/reputation/subject-claims/#{created["id"]}", %{
114
           |> patch(~p"/api/v1/reputation/subject-claims/#{created["id"]}", %{
115 115
             "status" => "linked"
116 116
           })
117 117
           |> json_response(409)

@@ -122,7 +122,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

122 122
123 123
    conn
124 124
    |> put_forge_api_token("reputation-claim-first")
125
    |> post(~p"/api/v3/reputation/subject-claims", %{
125
    |> post(~p"/api/v1/reputation/subject-claims", %{
126 126
      "subject_kind" => "account",
127 127
      "subject_id" => "user:" <> first.id
128 128
    })

@@ -131,7 +131,7 @@ defmodule OpenAgentsWeb.ReputationSubjectClaimControllerTest do

131 131
    body =
132 132
      build_conn()
133 133
      |> put_forge_api_token("reputation-claim-second")
134
      |> post(~p"/api/v3/reputation/subject-claims", %{
134
      |> post(~p"/api/v1/reputation/subject-claims", %{
135 135
        "subject_kind" => "account",
136 136
        "subject_id" => "user:" <> first.id
137 137
      })
test/openagents_web/controllers/stack_controller_test.exs modified +11 -11

@@ -32,7 +32,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

32 32
    :ok
33 33
  end
34 34
35
  describe "POST /api/v3/repos/:owner/:repo/stacks" do
35
  describe "POST /api/v1/repos/:owner/:repo/stacks" do
36 36
    test "creates a stack, reads it back, and replays the idempotency key", %{conn: conn} do
37 37
      repository = repository_fixture()
38 38
      oids = seed_chain(repository, ["layer-1", "layer-2", "layer-3"])

@@ -231,7 +231,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

231 231
    end
232 232
  end
233 233
234
  describe "POST /api/v3/repos/:owner/:repo/stacks/:stack_number/append" do
234
  describe "POST /api/v1/repos/:owner/:repo/stacks/:stack_number/append" do
235 235
    test "appends to the top, bumps the version, and replays retries", %{conn: conn} do
236 236
      repository = repository_fixture()
237 237
      oids = seed_chain(repository, ["layer-1", "layer-2"])

@@ -342,7 +342,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

342 342
    end
343 343
  end
344 344
345
  describe "POST /api/v3/repos/:owner/:repo/stacks/:stack_number/unstack" do
345
  describe "POST /api/v1/repos/:owner/:repo/stacks/:stack_number/unstack" do
346 346
    test "removes the top layer, records the event, and replays retries", %{conn: conn} do
347 347
      repository = repository_fixture()
348 348
      oids = seed_chain(repository, ["layer-1", "layer-2"])

@@ -446,7 +446,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

446 446
    end
447 447
  end
448 448
449
  describe "POST /api/v3/repos/:owner/:repo/stacks/:stack_number/dissolve" do
449
  describe "POST /api/v1/repos/:owner/:repo/stacks/:stack_number/dissolve" do
450 450
    test "releases every layer, dissolves the stack, and replays retries", %{conn: conn} do
451 451
      repository = repository_fixture()
452 452
      oids = seed_chain(repository, ["layer-1", "layer-2"])

@@ -525,7 +525,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

525 525
    end
526 526
  end
527 527
528
  describe "POST /api/v3/repos/:owner/:repo/stacks/:stack_number/rebase" do
528
  describe "POST /api/v1/repos/:owner/:repo/stacks/:stack_number/rebase" do
529 529
    test "accepts a rebase, exposes the operation, and replays retries", %{conn: conn} do
530 530
      repository = repository_fixture()
531 531
      oids = seed_chain(repository, ["layer-1"])

@@ -625,7 +625,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

625 625
    end
626 626
  end
627 627
628
  describe "POST /api/v3/repos/:owner/:repo/stacks/:stack_number/merge" do
628
  describe "POST /api/v1/repos/:owner/:repo/stacks/:stack_number/merge" do
629 629
    test "accepts a merge, exposes the operation, and replays retries", %{conn: conn} do
630 630
      repository = repository_fixture()
631 631
      oids = seed_chain(repository, ["layer-1"])

@@ -728,7 +728,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

728 728
    end
729 729
  end
730 730
731
  describe "PUT /api/v3/repos/:owner/:repo/pulls/:pull_number/merge-async" do
731
  describe "PUT /api/v1/repos/:owner/:repo/pulls/:pull_number/merge-async" do
732 732
    test "accepts a submission, polls it, replays retries, and reports the active conflict",
733 733
         %{conn: conn} do
734 734
      repository = repository_fixture()

@@ -855,7 +855,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

855 855
    end
856 856
  end
857 857
858
  describe "GET /api/v3/repos/:owner/:repo/stacks" do
858
  describe "GET /api/v1/repos/:owner/:repo/stacks" do
859 859
    test "reads are public for a public repository", %{conn: conn} do
860 860
      repository = repository_fixture()
861 861

@@ -879,7 +879,7 @@ defmodule OpenAgentsWeb.StackControllerTest do

879 879
880 880
      assert %{"number" => stack_number} = json_response(create_conn, 201)
881 881
882
      pulls = "/api/v3/repos/#{repository.owner}/#{repository.name}/pulls"
882
      pulls = "/api/v1/repos/#{repository.owner}/#{repository.name}/pulls"
883 883
884 884
      show_conn = get(conn, "#{pulls}/#{pr_2}")
885 885
      main_oid = oids["main"]

@@ -904,10 +904,10 @@ defmodule OpenAgentsWeb.StackControllerTest do

904 904
    end
905 905
  end
906 906
907
  defp path(repository), do: "/api/v3/repos/#{repository.owner}/#{repository.name}/stacks"
907
  defp path(repository), do: "/api/v1/repos/#{repository.owner}/#{repository.name}/stacks"
908 908
909 909
  defp pulls_path(repository),
910
    do: "/api/v3/repos/#{repository.owner}/#{repository.name}/pulls"
910
    do: "/api/v1/repos/#{repository.owner}/#{repository.name}/pulls"
911 911
912 912
  defp seed_chain(repository, branches) do
913 913
    path = Repos.ensure_repo!(repository.storage_key, repository.default_branch)
test/openagents_web/controllers/thread_controller_test.exs modified +71 -72

@@ -14,12 +14,12 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

14 14
  alias OpenAgents.Repo
15 15
  alias OpenAgents.Threads
16 16
17
  describe "POST /api/v3/threads" do
17
  describe "POST /api/v1/threads" do
18 18
    test "opens a thread and returns a grant that names it", %{conn: conn} do
19 19
      body =
20 20
        conn
21 21
        |> put_chat_api_token("thread-open")
22
        |> post(~p"/api/v3/threads", %{"objective" => "Rename the fence."})
22
        |> post(~p"/api/v1/threads", %{"objective" => "Rename the fence."})
23 23
        |> json_response(201)
24 24
25 25
      assert %{"thread" => thread, "grant" => grant} = body

@@ -45,7 +45,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

45 45
      grant =
46 46
        conn
47 47
        |> put_chat_api_token("thread-ceilings")
48
        |> post(~p"/api/v3/threads", %{"objective" => "Measure the budget."})
48
        |> post(~p"/api/v1/threads", %{"objective" => "Measure the budget."})
49 49
        |> json_response(201)
50 50
        |> Map.fetch!("grant")
51 51

@@ -64,7 +64,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

64 64
      thread =
65 65
        conn
66 66
        |> put_chat_api_token("thread-shape")
67
        |> post(~p"/api/v3/threads", %{
67
        |> post(~p"/api/v1/threads", %{
68 68
          "objective" => "Edit the file.",
69 69
          "reasoning" => "low",
70 70
          "permission_profile" => "workspace_write"

@@ -80,7 +80,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

80 80
      thread =
81 81
        conn
82 82
        |> put_chat_api_token("thread-repository")
83
        |> post(~p"/api/v3/threads", %{
83
        |> post(~p"/api/v1/threads", %{
84 84
          "objective" => "openagents coder in OpenAgentsInc/openagents.com on main",
85 85
          "repository" => "OpenAgentsInc/openagents.com"
86 86
        })

@@ -98,7 +98,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

98 98
      thread =
99 99
        conn
100 100
        |> put_chat_api_token("thread-no-repository")
101
        |> post(~p"/api/v3/threads", %{"objective" => "No repository named."})
101
        |> post(~p"/api/v1/threads", %{"objective" => "No repository named."})
102 102
        |> json_response(201)
103 103
        |> Map.fetch!("thread")
104 104

@@ -109,7 +109,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

109 109
      body =
110 110
        conn
111 111
        |> put_chat_api_token("thread-blank-repository")
112
        |> post(~p"/api/v3/threads", %{"objective" => "Blank it.", "repository" => "   "})
112
        |> post(~p"/api/v1/threads", %{"objective" => "Blank it.", "repository" => "   "})
113 113
        |> json_response(422)
114 114
115 115
      assert body["code"] == "validation_failed"

@@ -120,7 +120,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

120 120
      body =
121 121
        conn
122 122
        |> put_chat_api_token("thread-long-repository")
123
        |> post(~p"/api/v3/threads", %{
123
        |> post(~p"/api/v1/threads", %{
124 124
          "objective" => "Bound it.",
125 125
          "repository" => String.duplicate("a", 201)
126 126
        })

@@ -133,7 +133,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

133 133
      body =
134 134
        conn
135 135
        |> put_chat_api_token("thread-ox-alpha")
136
        |> post(~p"/api/v3/threads", %{
136
        |> post(~p"/api/v1/threads", %{
137 137
          "objective" => "Delegate the edit.",
138 138
          "model" => "ox-alpha"
139 139
        })

@@ -146,7 +146,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

146 146
      body =
147 147
        conn
148 148
        |> put_chat_api_token("thread-default-model")
149
        |> post(~p"/api/v3/threads", %{"objective" => "Take the default."})
149
        |> post(~p"/api/v1/threads", %{"objective" => "Take the default."})
150 150
        |> json_response(201)
151 151
152 152
      assert body["grant"]["model"] == OpenAgents.Inference.Models.default_id()

@@ -167,7 +167,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

167 167
      body =
168 168
        conn
169 169
        |> put_chat_api_token("thread-unavailable-model")
170
        |> post(~p"/api/v3/threads", %{
170
        |> post(~p"/api/v1/threads", %{
171 171
          "objective" => "Ask for the unconfigured lane.",
172 172
          "model" => "ox-alpha"
173 173
        })

@@ -191,7 +191,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

191 191
      body =
192 192
        conn
193 193
        |> put_chat_api_token("thread-bad-model")
194
        |> post(~p"/api/v3/threads", %{
194
        |> post(~p"/api/v1/threads", %{
195 195
          "objective" => "Ask for the impossible.",
196 196
          "model" => "attacker/gpt-9-ultra"
197 197
        })

@@ -205,7 +205,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

205 205
      body =
206 206
        conn
207 207
        |> put_chat_api_token("thread-no-objective")
208
        |> post(~p"/api/v3/threads", %{})
208
        |> post(~p"/api/v1/threads", %{})
209 209
        |> json_response(422)
210 210
211 211
      assert body["code"] == "validation_failed"

@@ -216,7 +216,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

216 216
      body =
217 217
        conn
218 218
        |> put_chat_api_token("thread-bad-reasoning")
219
        |> post(~p"/api/v3/threads", %{
219
        |> post(~p"/api/v1/threads", %{
220 220
          "objective" => "Ask for the impossible.",
221 221
          "reasoning" => "not-a-legal-value"
222 222
        })

@@ -230,7 +230,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

230 230
      body =
231 231
        conn
232 232
        |> put_chat_api_token("thread-bad-profile")
233
        |> post(~p"/api/v3/threads", %{
233
        |> post(~p"/api/v1/threads", %{
234 234
          "objective" => "Ask for the impossible.",
235 235
          "permission_profile" => "root"
236 236
        })

@@ -252,13 +252,13 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

252 252
253 253
      for index <- 1..limit do
254 254
        assert authenticated
255
               |> post(~p"/api/v3/threads", %{"objective" => "Concurrent #{index}."})
255
               |> post(~p"/api/v1/threads", %{"objective" => "Concurrent #{index}."})
256 256
               |> json_response(201)
257 257
      end
258 258
259 259
      body =
260 260
        authenticated
261
        |> post(~p"/api/v3/threads", %{"objective" => "One too many."})
261
        |> post(~p"/api/v1/threads", %{"objective" => "One too many."})
262 262
        |> json_response(429)
263 263
264 264
      assert body["code"] == "thread_quota_reached"

@@ -275,7 +275,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

275 275
276 276
      opened =
277 277
        authenticated
278
        |> post(~p"/api/v3/threads", %{"objective" => "Spend it all."})
278
        |> post(~p"/api/v1/threads", %{"objective" => "Spend it all."})
279 279
        |> json_response(201)
280 280
281 281
      grant = Repo.get_by!(Grant, thread_id: opened["thread"]["id"])

@@ -289,7 +289,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

289 289
290 290
      body =
291 291
        authenticated
292
        |> post(~p"/api/v3/threads", %{"objective" => "One more, on empty."})
292
        |> post(~p"/api/v1/threads", %{"objective" => "One more, on empty."})
293 293
        |> json_response(402)
294 294
295 295
      assert body["code"] == "credit_exhausted"

@@ -308,37 +308,37 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

308 308
309 309
      assert conn
310 310
             |> put_chat_api_token("thread-cap-mine")
311
             |> post(~p"/api/v3/threads", %{"objective" => "Mine."})
311
             |> post(~p"/api/v1/threads", %{"objective" => "Mine."})
312 312
             |> json_response(201)
313 313
314 314
      assert conn
315 315
             |> put_chat_api_token("thread-cap-yours")
316
             |> post(~p"/api/v3/threads", %{"objective" => "Yours."})
316
             |> post(~p"/api/v1/threads", %{"objective" => "Yours."})
317 317
             |> json_response(201)
318 318
    end
319 319
320 320
    test "an anonymous caller is refused with the envelope", %{conn: conn} do
321
      body = conn |> post(~p"/api/v3/threads", %{"objective" => "No."}) |> json_response(401)
321
      body = conn |> post(~p"/api/v1/threads", %{"objective" => "No."}) |> json_response(401)
322 322
323 323
      assert body["code"] == "unauthenticated"
324 324
      assert is_map(body["errors"])
325 325
    end
326 326
  end
327 327
328
  describe "GET /api/v3/threads/:thread_id" do
328
  describe "GET /api/v1/threads/:thread_id" do
329 329
    test "reports usage, and reports no remainder where there is no ceiling", %{conn: conn} do
330 330
      authenticated = put_chat_api_token(conn, "thread-read")
331 331
332 332
      created =
333 333
        authenticated
334
        |> post(~p"/api/v3/threads", %{"objective" => "Report on me."})
334
        |> post(~p"/api/v1/threads", %{"objective" => "Report on me."})
335 335
        |> json_response(201)
336 336
337 337
      id = created["thread"]["id"]
338 338
      grant = Repo.get_by!(Grant, thread_id: id)
339 339
      {:ok, _spent} = Inference.record_usage(grant, %{"input_tokens" => 10, "output_tokens" => 5})
340 340
341
      body = authenticated |> get(~p"/api/v3/threads/#{id}") |> json_response(200)
341
      body = authenticated |> get(~p"/api/v1/threads/#{id}") |> json_response(200)
342 342
343 343
      assert body["thread"]["status"] == "open"
344 344
      assert body["grant"]["status"] == "active"

@@ -365,13 +365,13 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

365 365
      created =
366 366
        conn
367 367
        |> put_chat_api_token("thread-owner")
368
        |> post(~p"/api/v3/threads", %{"objective" => "Private work."})
368
        |> post(~p"/api/v1/threads", %{"objective" => "Private work."})
369 369
        |> json_response(201)
370 370
371 371
      body =
372 372
        conn
373 373
        |> put_chat_api_token("thread-stranger")
374
        |> get(~p"/api/v3/threads/#{created["thread"]["id"]}")
374
        |> get(~p"/api/v1/threads/#{created["thread"]["id"]}")
375 375
        |> json_response(404)
376 376
377 377
      assert body["code"] == "not_found"

@@ -382,17 +382,17 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

382 382
      created =
383 383
        conn
384 384
        |> put_chat_api_token("thread-owner-two")
385
        |> post(~p"/api/v3/threads", %{"objective" => "Private work."})
385
        |> post(~p"/api/v1/threads", %{"objective" => "Private work."})
386 386
        |> json_response(201)
387 387
388 388
      stranger = put_chat_api_token(conn, "thread-stranger-two")
389 389
390 390
      theirs =
391
        stranger |> get(~p"/api/v3/threads/#{created["thread"]["id"]}") |> json_response(404)
391
        stranger |> get(~p"/api/v1/threads/#{created["thread"]["id"]}") |> json_response(404)
392 392
393 393
      absent =
394 394
        stranger
395
        |> get(~p"/api/v3/threads/00000000-0000-4000-8000-000000000001")
395
        |> get(~p"/api/v1/threads/00000000-0000-4000-8000-000000000001")
396 396
        |> json_response(404)
397 397
398 398
      assert Map.drop(theirs, ["request_id"]) == Map.drop(absent, ["request_id"])

@@ -408,12 +408,12 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

408 408
409 409
      created =
410 410
        authenticated
411
        |> post(~p"/api/v3/threads", %{"objective" => "Outlive me."})
411
        |> post(~p"/api/v1/threads", %{"objective" => "Outlive me."})
412 412
        |> json_response(201)
413 413
414 414
      body =
415 415
        authenticated
416
        |> get(~p"/api/v3/threads/#{created["thread"]["id"]}")
416
        |> get(~p"/api/v1/threads/#{created["thread"]["id"]}")
417 417
        |> json_response(200)
418 418
419 419
      assert body["grant"]["status"] == "active"

@@ -434,12 +434,12 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

434 434
435 435
      created =
436 436
        authenticated
437
        |> post(~p"/api/v3/threads", %{"objective" => "Outlive me."})
437
        |> post(~p"/api/v1/threads", %{"objective" => "Outlive me."})
438 438
        |> json_response(201)
439 439
440 440
      body =
441 441
        authenticated
442
        |> get(~p"/api/v3/threads/#{created["thread"]["id"]}")
442
        |> get(~p"/api/v1/threads/#{created["thread"]["id"]}")
443 443
        |> json_response(200)
444 444
445 445
      assert body["grant"]["status"] == "expired"

@@ -454,7 +454,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

454 454
455 455
      first =
456 456
        authenticated
457
        |> post(~p"/api/v3/threads", %{"objective" => "First."})
457
        |> post(~p"/api/v1/threads", %{"objective" => "First."})
458 458
        |> json_response(201)
459 459
460 460
      assert Threads.open_count(github_user("api-token-thread-expiry-cap")) == 1

@@ -462,37 +462,37 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

462 462
      # Waiting does not free it. There is no clock to wait out.
463 463
      refused =
464 464
        authenticated
465
        |> post(~p"/api/v3/threads", %{"objective" => "Second."})
465
        |> post(~p"/api/v1/threads", %{"objective" => "Second."})
466 466
        |> json_response(429)
467 467
468 468
      assert refused["code"] == "thread_quota_reached"
469 469
470 470
      # Saying so does.
471
      _deleted = authenticated |> delete(~p"/api/v3/threads/#{first["thread"]["id"]}")
471
      _deleted = authenticated |> delete(~p"/api/v1/threads/#{first["thread"]["id"]}")
472 472
473 473
      second =
474 474
        authenticated
475
        |> post(~p"/api/v3/threads", %{"objective" => "Second."})
475
        |> post(~p"/api/v1/threads", %{"objective" => "Second."})
476 476
        |> json_response(201)
477 477
478 478
      assert second["grant"]["token"] != first["grant"]["token"]
479 479
    end
480 480
  end
481 481
482
  describe "DELETE /api/v3/threads/:thread_id" do
482
  describe "DELETE /api/v1/threads/:thread_id" do
483 483
    test "revokes the grant immediately", %{conn: conn} do
484 484
      authenticated = put_chat_api_token(conn, "thread-revoke")
485 485
486 486
      created =
487 487
        authenticated
488
        |> post(~p"/api/v3/threads", %{"objective" => "Stop me."})
488
        |> post(~p"/api/v1/threads", %{"objective" => "Stop me."})
489 489
        |> json_response(201)
490 490
491 491
      id = created["thread"]["id"]
492 492
      token = created["grant"]["token"]
493 493
      assert {:ok, _usable} = Inference.resolve(token)
494 494
495
      body = authenticated |> delete(~p"/api/v3/threads/#{id}") |> json_response(200)
495
      body = authenticated |> delete(~p"/api/v1/threads/#{id}") |> json_response(200)
496 496
497 497
      assert body["thread"]["status"] == "cancelled"
498 498
      assert body["grant"]["status"] == "revoked"

@@ -505,12 +505,12 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

505 505
506 506
      created =
507 507
        authenticated
508
        |> post(~p"/api/v3/threads", %{"objective" => "Stop me twice."})
508
        |> post(~p"/api/v1/threads", %{"objective" => "Stop me twice."})
509 509
        |> json_response(201)
510 510
511 511
      id = created["thread"]["id"]
512
      assert authenticated |> delete(~p"/api/v3/threads/#{id}") |> json_response(200)
513
      body = authenticated |> delete(~p"/api/v3/threads/#{id}") |> json_response(200)
512
      assert authenticated |> delete(~p"/api/v1/threads/#{id}") |> json_response(200)
513
      body = authenticated |> delete(~p"/api/v1/threads/#{id}") |> json_response(200)
514 514
515 515
      assert body["thread"]["status"] == "cancelled"
516 516
      assert body["grant"]["status"] == "revoked"

@@ -520,13 +520,13 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

520 520
      created =
521 521
        conn
522 522
        |> put_chat_api_token("thread-revoke-owner")
523
        |> post(~p"/api/v3/threads", %{"objective" => "Mine alone."})
523
        |> post(~p"/api/v1/threads", %{"objective" => "Mine alone."})
524 524
        |> json_response(201)
525 525
526 526
      body =
527 527
        conn
528 528
        |> put_chat_api_token("thread-revoke-stranger")
529
        |> delete(~p"/api/v3/threads/#{created["thread"]["id"]}")
529
        |> delete(~p"/api/v1/threads/#{created["thread"]["id"]}")
530 530
        |> json_response(404)
531 531
532 532
      assert body["code"] == "not_found"

@@ -540,7 +540,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

540 540
541 541
      created =
542 542
        authenticated
543
        |> post(~p"/api/v3/threads", %{"objective" => "Answer one question."})
543
        |> post(~p"/api/v1/threads", %{"objective" => "Answer one question."})
544 544
        |> json_response(201)
545 545
546 546
      proxied =

@@ -558,7 +558,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

558 558
559 559
      assert proxied.status == 200
560 560
561
      body = authenticated |> get(~p"/api/v3/threads/#{created["thread"]["id"]}")
561
      body = authenticated |> get(~p"/api/v1/threads/#{created["thread"]["id"]}")
562 562
      grant = json_response(body, 200)["grant"]
563 563
564 564
      assert grant["call_count"] == 1

@@ -570,11 +570,11 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

570 570
571 571
      created =
572 572
        authenticated
573
        |> post(~p"/api/v3/threads", %{"objective" => "Answer, then stop."})
573
        |> post(~p"/api/v1/threads", %{"objective" => "Answer, then stop."})
574 574
        |> json_response(201)
575 575
576 576
      assert authenticated
577
             |> delete(~p"/api/v3/threads/#{created["thread"]["id"]}")
577
             |> delete(~p"/api/v1/threads/#{created["thread"]["id"]}")
578 578
             |> json_response(200)
579 579
580 580
      proxied =

@@ -619,18 +619,18 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

619 619
620 620
      created =
621 621
        authenticated
622
        |> post(~p"/api/v3/threads", %{"objective" => "Remember me."})
622
        |> post(~p"/api/v1/threads", %{"objective" => "Remember me."})
623 623
        |> json_response(201)
624 624
625 625
      %{authenticated: authenticated, id: created["thread"]["id"]}
626 626
    end
627 627
628 628
    test "records an event and advances the count", %{authenticated: conn, id: id} do
629
      before = conn |> get(~p"/api/v3/threads/#{id}") |> json_response(200)
629
      before = conn |> get(~p"/api/v1/threads/#{id}") |> json_response(200)
630 630
631 631
      body =
632 632
        conn
633
        |> post(~p"/api/v3/threads/#{id}/events", %{
633
        |> post(~p"/api/v1/threads/#{id}/events", %{
634 634
          "event_type" => "turn.user",
635 635
          "payload" => %{"text" => "list the open issues"}
636 636
        })

@@ -644,14 +644,14 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

644 644
    test "reads the transcript back, oldest first", %{authenticated: conn, id: id} do
645 645
      for text <- ["first", "second", "third"] do
646 646
        conn
647
        |> post(~p"/api/v3/threads/#{id}/events", %{
647
        |> post(~p"/api/v1/threads/#{id}/events", %{
648 648
          "event_type" => "turn.user",
649 649
          "payload" => %{"text" => text}
650 650
        })
651 651
        |> json_response(201)
652 652
      end
653 653
654
      body = conn |> get(~p"/api/v3/threads/#{id}/events") |> json_response(200)
654
      body = conn |> get(~p"/api/v1/threads/#{id}/events") |> json_response(200)
655 655
656 656
      # The server's copy is the only copy: a client reads this back rather than
657 657
      # keeping its own, so two machines on one thread see one transcript.

@@ -667,17 +667,17 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

667 667
    test "continues from an event already read", %{authenticated: conn, id: id} do
668 668
      for text <- ["one", "two", "three"] do
669 669
        conn
670
        |> post(~p"/api/v3/threads/#{id}/events", %{
670
        |> post(~p"/api/v1/threads/#{id}/events", %{
671 671
          "event_type" => "turn.user",
672 672
          "payload" => %{"text" => text}
673 673
        })
674 674
        |> json_response(201)
675 675
      end
676 676
677
      first = conn |> get(~p"/api/v3/threads/#{id}/events?limit=2") |> json_response(200)
677
      first = conn |> get(~p"/api/v1/threads/#{id}/events?limit=2") |> json_response(200)
678 678
      cursor = List.last(first["events"])["id"]
679 679
680
      rest = conn |> get(~p"/api/v3/threads/#{id}/events?after=#{cursor}") |> json_response(200)
680
      rest = conn |> get(~p"/api/v1/threads/#{id}/events?after=#{cursor}") |> json_response(200)
681 681
682 682
      # A working session records a turn and every tool it ran, which passes
683 683
      # the listing cap inside an hour. Without a cursor its history could not

@@ -695,13 +695,13 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

695 695
      assert byte_size(reasoning) > 16_384
696 696
697 697
      conn
698
      |> post(~p"/api/v3/threads/#{id}/events", %{
698
      |> post(~p"/api/v1/threads/#{id}/events", %{
699 699
        "event_type" => "turn.reasoning",
700 700
        "payload" => %{"text" => reasoning}
701 701
      })
702 702
      |> json_response(201)
703 703
704
      body = conn |> get(~p"/api/v3/threads/#{id}/events") |> json_response(200)
704
      body = conn |> get(~p"/api/v1/threads/#{id}/events") |> json_response(200)
705 705
706 706
      stored =
707 707
        body["events"]

@@ -719,7 +719,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

719 719
      # column holds valid JSON, not that the JSON is interesting.
720 720
      body =
721 721
        conn
722
        |> post(~p"/api/v3/threads/#{id}/events", %{"event_type" => "turn.started"})
722
        |> post(~p"/api/v1/threads/#{id}/events", %{"event_type" => "turn.started"})
723 723
        |> json_response(201)
724 724
725 725
      assert body["thread"]["event_count"] > 0

@@ -728,7 +728,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

728 728
    test "returns the created event, whose id is the cursor", %{authenticated: conn, id: id} do
729 729
      body =
730 730
        conn
731
        |> post(~p"/api/v3/threads/#{id}/events", %{
731
        |> post(~p"/api/v1/threads/#{id}/events", %{
732 732
          "event_type" => "turn.user",
733 733
          "payload" => %{"text" => "echo me back"}
734 734
        })

@@ -742,14 +742,14 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

742 742
      assert body["event"]["payload"] == %{"text" => "echo me back"}
743 743
      assert is_binary(body["event"]["inserted_at"])
744 744
745
      read = conn |> get(~p"/api/v3/threads/#{id}/events") |> json_response(200)
745
      read = conn |> get(~p"/api/v1/threads/#{id}/events") |> json_response(200)
746 746
      assert List.last(read["events"])["id"] == body["event"]["id"]
747 747
    end
748 748
749 749
    test "refuses an event with no type", %{authenticated: conn, id: id} do
750 750
      body =
751 751
        conn
752
        |> post(~p"/api/v3/threads/#{id}/events", %{"payload" => %{"text" => "x"}})
752
        |> post(~p"/api/v1/threads/#{id}/events", %{"payload" => %{"text" => "x"}})
753 753
        |> json_response(422)
754 754
755 755
      # The code is the machine's half of the refusal, symmetric with

@@ -759,11 +759,11 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

759 759
    end
760 760
761 761
    test "refuses to append to a revoked thread", %{authenticated: conn, id: id} do
762
      conn |> delete(~p"/api/v3/threads/#{id}") |> json_response(200)
762
      conn |> delete(~p"/api/v1/threads/#{id}") |> json_response(200)
763 763
764 764
      body =
765 765
        conn
766
        |> post(~p"/api/v3/threads/#{id}/events", %{"event_type" => "turn.user"})
766
        |> post(~p"/api/v1/threads/#{id}/events", %{"event_type" => "turn.user"})
767 767
        |> json_response(422)
768 768
769 769
      # A transcript that keeps growing after the report was written is not the

@@ -775,11 +775,11 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

775 775
      authenticated: conn,
776 776
      id: id
777 777
    } do
778
      before = conn |> get(~p"/api/v3/threads/#{id}") |> json_response(200)
778
      before = conn |> get(~p"/api/v1/threads/#{id}") |> json_response(200)
779 779
780 780
      body =
781 781
        conn
782
        |> post(~p"/api/v3/threads/#{id}/events", %{
782
        |> post(~p"/api/v1/threads/#{id}/events", %{
783 783
          "events" => [
784 784
            %{"event_type" => "turn.user", "payload" => %{"text" => "first"}},
785 785
            %{"event_type" => "tool.ran", "payload" => %{"tool" => "bash"}},

@@ -799,19 +799,19 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

799 799
      assert Enum.all?(body["events"], &is_binary(&1["inserted_at"]))
800 800
      assert body["thread"]["event_count"] == before["thread"]["event_count"] + 3
801 801
802
      read = conn |> get(~p"/api/v3/threads/#{id}/events") |> json_response(200)
802
      read = conn |> get(~p"/api/v1/threads/#{id}/events") |> json_response(200)
803 803
      assert Enum.take(read["events"], -3) |> Enum.map(& &1["id"]) == ids
804 804
    end
805 805
806 806
    test "a batch with one invalid event records nothing", %{authenticated: conn, id: id} do
807
      before = conn |> get(~p"/api/v3/threads/#{id}") |> json_response(200)
807
      before = conn |> get(~p"/api/v1/threads/#{id}") |> json_response(200)
808 808
809 809
      # The second entry passes the route's parse — its type is non-blank — and
810 810
      # is refused by the database's 80-character ceiling, so the refusal
811 811
      # proves the transaction rolled the first entry back with it.
812 812
      body =
813 813
        conn
814
        |> post(~p"/api/v3/threads/#{id}/events", %{
814
        |> post(~p"/api/v1/threads/#{id}/events", %{
815 815
          "events" => [
816 816
            %{"event_type" => "turn.user", "payload" => %{"text" => "landed?"}},
817 817
            %{"event_type" => String.duplicate("x", 81), "payload" => %{}}

@@ -822,7 +822,7 @@ defmodule OpenAgentsWeb.ThreadControllerTest do

822 822
      assert body["code"] == "event_invalid"
823 823
      assert body["errors"]["events[1].event_type"] != nil
824 824
825
      after_refusal = conn |> get(~p"/api/v3/threads/#{id}") |> json_response(200)

This page updates live while a promote is in flight · changelog