Document repository creation and OpenAgents CLI

a06751195cef · AtlantisPleb · · parent 6d3f45b00ba3

Document repository creation and OpenAgents CLI

Deploy story

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

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added docs/repository-creation-and-openagents-cli-spec.md

Diff

1 file changed, +985 -0

docs/repository-creation-and-openagents-cli-spec.md added +985

@@ -0,0 +1,985 @@

1
# Repository creation and OpenAgents CLI specification
2
3
Date: 2026-08-20
4
5
Status: Proposed; repository creation is the first delivery slice
6
7
## Purpose
8
9
Allow an authenticated person or agent to create an OpenAgents-hosted Git
10
repository, push and pull with standard Git, and manage the repository through
11
the browser or a first-party CLI.
12
13
This specification uses Cursor Origin as product research. It adopts the useful
14
interaction patterns without treating Origin's API, implementation, pricing,
15
team model, or terminology as an OpenAgents contract.
16
17
## Implementation homes
18
19
| Concern | Owning repository |
20
| --- | --- |
21
| Repository, namespace, membership, and provisioning authority | `openagents.com` Phoenix application |
22
| REST API, browser authorization, Git HTTP, and web interface | `openagents.com` Phoenix application |
23
| CLI source, release tooling, and terminal tests | `openagents` Effect monorepo |
24
| Public API contract | Authored and tested by `openagents.com`; consumed as a pinned client contract by the CLI |
25
26
The Phoenix application remains the server authority. The CLI must not create a
27
second repository database or infer authorization from local Git state.
28
29
## Related documents
30
31
- [OpenAgents architecture](architecture.md)
32
- [GitHub-shaped Issues and Projects API assessment](github-api-issues-projects-assessment.md)
33
- [Issues and Projects UI roadmap](issues-projects-ui-roadmap.md)
34
- [API authentication](api-authentication.md)
35
- [GitHub authentication and token lifecycle](github-auth-plan.md)
36
- [ADR 0007: Cut over to forge-canonical source control after proof](decisions/0007-cut-over-to-forge-canonical-source-control-after-proof.md)
37
- [Integration hardening and staging readiness recommendations](2026-08-20-integration-hardening-and-staging-readiness-recommendations.md)
38
39
## Current state
40
41
The application already has several parts of the required domain:
42
43
- `OpenAgents.Repositories.Repository` stores a UUID, owner, name, visibility,
44
  and default branch.
45
- `OpenAgents.Repositories.Membership` grants repository roles.
46
- Issues, labels, milestones, comments, projects, and project items carry a
47
  repository ID and enforce cross-repository isolation.
48
- `OpenAgents.Repositories.create_repository/1` creates a database row for
49
  tests and internal callers.
50
- The forge serves Git smart HTTP, stores a durable write-ahead log, maintains
51
  reconstructable bare-repository caches, and exposes bounded code-browsing
52
  projections.
53
- The first-party `oa_pat_...` token supports the `forge:write` scope for
54
  GitHub-shaped API mutations.
55
56
The current implementation does not provide a user repository lifecycle:
57
58
- No browser route or public API creates a repository.
59
- No API lists every repository the current user can access.
60
- The signed-in dashboard loads only `OpenAgentsInc/openagents.com`.
61
- Git repository admission comes from `OPENAGENTS_FORGE_REPOSITORIES`, not the
62
  repository database.
63
- Forge storage and visibility use repository names from runtime configuration,
64
  while Issues and Projects use stable repository UUIDs from PostgreSQL.
65
- Git HTTP paths contain a repository name without an owner namespace.
66
- Git HTTP accepts an operator credential or paired-machine credential without
67
  checking an ordinary user's repository membership.
68
- The public code route has a literal `OpenAgentsInc` owner segment.
69
70
Creating a database row alone therefore does not create a usable hosted Git
71
repository.
72
73
## Product decisions
74
75
The first release follows these decisions:
76
77
- A repository belongs to one stable namespace.
78
- The first namespace type is a personal namespace. Team and organization
79
  namespaces follow later.
80
- Repository visibility is `private` or `public`. The `internal` visibility
81
  from Origin requires a team model and is out of scope.
82
- New repositories default to `private` and use `main` as the default branch.
83
- PostgreSQL owns repository identity, lifecycle, membership, and policy.
84
- The forge write-ahead log owns durable Git ref and object history.
85
- A local bare repository is a cache that the forge can reconstruct.
86
- Repository creation never grants build, promotion, hot-load, deployment, or
87
  operator authority.
88
- Standard Git remains the data-plane client. The CLI orchestrates API and Git
89
  commands but does not replace Git.
90
- The CLI uses the server-provided clone URL instead of constructing one from a
91
  hard-coded host.
92
- GitHub mirroring, pull requests, rulesets, SSH keys, apps, and team sharing
93
  remain future slices.
94
95
## Goals
96
97
The repository-creation slice must support these outcomes:
98
99
- You can claim or receive a stable personal namespace.
100
- You can create an empty repository from the browser.
101
- You can create the same repository with `openagents repo create`.
102
- You can list and view repositories that you can access.
103
- You can clone, fetch, push, and pull with standard Git.
104
- A public repository supports anonymous read access.
105
- A private repository never reveals its existence to an unauthorized caller.
106
- An agent can complete the flow without parsing prose or answering an
107
  interactive prompt when it already has an environment credential.
108
- Repeated create requests do not create duplicate database, storage, or
109
  membership records.
110
- Provisioning failures remain durable, inspectable, retryable, and bounded.
111
112
## Non-goals for the first slice
113
114
The first slice does not include:
115
116
- GitHub repository mirroring or bidirectional synchronization.
117
- Pull requests, reviews, merge queues, rulesets, or branch protection.
118
- Team namespaces, organization administration, or `internal` visibility.
119
- Repository transfer, rename, archive, restore, or deletion.
120
- SSH Git transport or SSH-key management.
121
- Code search across repositories.
122
- Third-party application installation.
123
- Billing or plan enforcement.
124
- Automatic deployment of code pushed to a user repository.
125
- Complete GitHub or Origin API compatibility.
126
127
These omissions reserve future command groups without making them current
128
product promises.
129
130
## Origin pattern adaptation
131
132
| Origin pattern | OpenAgents disposition |
133
| --- | --- |
134
| Browser-assisted CLI login | Adopt with an OpenAgents-owned device authorization flow |
135
| Git credential-helper setup | Adopt for the exact admitted OpenAgents host |
136
| `repo create`, `list`, `view`, and `clone` | Adopt in the first release |
137
| Repository inference from the `origin` remote | Adopt with strict host and path validation |
138
| Human and JSON output | Adopt as separate output contracts |
139
| Endpoint override | Adopt for local development and staging |
140
| Repository deletion | Defer until recovery and retention semantics exist |
141
| GitHub mirror creation | Defer until per-repository source authority and synchronization receipts exist |
142
| Pull requests and rulesets | Defer until their server domains exist |
143
| SSH keys | Defer; use HTTPS and the Git credential helper first |
144
| Generic authenticated API command | Reserve until endpoint and secret-redaction behavior is specified |
145
| Self-update and shell completion | Defer until CLI packaging and release channels are admitted |
146
147
Origin uses team-owned codebases and `internal` visibility. OpenAgents starts
148
with stable personal namespaces and `public` or `private` repositories because
149
those concepts already match the application domain.
150
151
## Namespace model
152
153
A repository owner must be a durable namespace, not a mutable GitHub login
154
copied into a string field.
155
156
Add a `namespaces` table with at least these fields:
157
158
| Field | Meaning |
159
| --- | --- |
160
| `id` | Stable UUID |
161
| `slug` | Displayed, URL-safe owner name |
162
| `slug_key` | Case-insensitive uniqueness key |
163
| `kind` | `personal` initially; reserve `team` |
164
| `owner_user_id` | Owning user for a personal namespace |
165
| `state` | `active`, `suspended`, or `retired` |
166
| `inserted_at`, `updated_at` | Audit timestamps |
167
168
The repository row gains a required `namespace_id`. Keep `owner` as a derived
169
API projection during migration only if compatibility requires it. New policy
170
checks must join through `namespace_id` and memberships instead of trusting the
171
projected owner string.
172
173
### Personal namespace enrollment
174
175
Use this first-release flow:
176
177
1. Derive a suggested slug from the authenticated user's current GitHub login.
178
2. Normalize and validate the suggestion.
179
3. Claim the slug transactionally on the user's first repository creation.
180
4. Keep the claimed slug stable when the GitHub login later changes.
181
5. If another namespace already owns the slug, return `namespace_required` and
182
   direct the user to an authenticated namespace-selection page.
183
184
Do not silently append an unstable number to resolve a collision. The user must
185
know the namespace that appears in clone URLs.
186
187
Namespace rename and transfer are out of scope. The UI must say that the chosen
188
namespace cannot be changed during the initial release.
189
190
### Repository name rules
191
192
Use one validation rule in the browser, API, CLI, database, and Git transport:
193
194
- Normalize names to lowercase.
195
- Accept 1 through 64 ASCII characters.
196
- Require an ASCII letter or digit as the first character.
197
- Permit lowercase letters, digits, hyphens, underscores, and a dot followed by
198
  a letter or digit.
199
- Reserve platform route names and names required by Git internals.
200
- Enforce case-insensitive uniqueness within a namespace.
201
202
The existing forge name expression is the starting contract:
203
204
```text
205
^[a-z0-9](?:[a-z0-9_-]|\.(?=[a-z0-9])){0,63}$
206
```
207
208
The API returns the normalized name. The CLI reports normalization before it
209
changes local Git configuration.
210
211
## Repository lifecycle
212
213
Add a lifecycle state to each repository:
214
215
```text
216
provisioning -> ready
217
provisioning -> failed -> provisioning
218
ready -> suspended
219
suspended -> ready
220
```
221
222
Reserve `deleting` and `deleted` for the later deletion slice.
223
224
The repository row needs these additional attributes:
225
226
| Field | Meaning |
227
| --- | --- |
228
| `namespace_id` | Stable owner namespace |
229
| `created_by_user_id` | Audited creator |
230
| `description` | Optional bounded description |
231
| `lifecycle_state` | Provisioning and availability state |
232
| `provision_error_code` | Bounded operational code without provider prose |
233
| `storage_key` | Stable UUID-derived forge storage key |
234
| `ready_at` | Time when Git operations became available |
235
236
Do not put a credential, filesystem path, bucket URL, or raw provisioning error
237
in the repository row.
238
239
### Authority split
240
241
The lifecycle preserves these authorities:
242
243
- PostgreSQL decides whether a repository exists, who can access it, and which
244
  lifecycle state it occupies.
245
- The forge write-ahead log durably records Git objects and refs.
246
- The bare Git directory is a node-local cache.
247
- Runtime configuration sets service limits and the separate deployment
248
  allowlist. It does not enumerate every hosted repository.
249
250
Split the current `forge_repos` concept into two concerns:
251
252
- A PostgreSQL-backed hosted-repository inventory for Git and product routes.
253
- An operator-owned deployable-repository allowlist for build, promotion,
254
  direct loading, relup, and rolling replacement.
255
256
A new user repository enters only the hosted-repository inventory. Repository
257
creation must never add it to the deployable-repository allowlist.
258
259
## Provisioning contract
260
261
Repository creation crosses PostgreSQL and durable Git storage, so one database
262
transaction cannot complete the entire operation. Use a transactional outbox
263
and an idempotent provisioner.
264
265
1. Validate the authenticated principal, namespace, name, visibility, default
266
   branch, quota, and idempotency key.
267
2. In one PostgreSQL transaction, create the repository, add the creator as an
268
   `owner` member, and insert a provisioning outbox record.
269
3. Commit before any filesystem or object-store operation begins.
270
4. Initialize an empty durable WAL namespace using `repository.storage_key`.
271
5. Materialize or initialize the bare-repository cache with the requested
272
   symbolic default branch.
273
6. Verify that upload-pack and receive-pack resolve the same repository UUID.
274
7. Mark the repository `ready` and set `ready_at`.
275
8. On failure, record a bounded `provision_error_code`, mark the repository
276
   `failed`, and retain the outbox attempt history.
277
278
The provisioner must tolerate a crash after every step. A retry must converge
279
on the same repository and storage namespace without deleting accepted Git
280
objects.
281
282
The create API can wait for a bounded synchronous attempt. Return `201 Created`
283
when provisioning completes during that window. Return `202 Accepted` with
284
`lifecycle_state: "provisioning"` when work continues asynchronously. The CLI
285
polls the repository resource until it reaches `ready`, reaches `failed`, or
286
exceeds its client timeout.
287
288
## API contract
289
290
The new surface extends the bounded GitHub-shaped API under `/api/v3`.
291
292
### Endpoints
293
294
| Method and path | Authority | First release behavior |
295
| --- | --- | --- |
296
| `POST /api/v3/user/repos` | Authenticated API with `forge:write` | Create in the caller's personal namespace |
297
| `GET /api/v3/user/repos` | Authenticated API | List repositories visible to the caller, including private repositories |
298
| `GET /api/v3/repos/{owner}/{repo}` | Optional API principal | Return a public repository or a repository visible to the principal |
299
| Git smart HTTP under `/git/{owner}/{repo}.git` | Public read or authenticated Git principal | Clone, fetch, push, and pull |
300
301
Reserve `POST /api/v3/orgs/{org}/repos` for a future team namespace. Do not add
302
an endpoint that accepts an arbitrary `owner` string and then creates a personal
303
repository there.
304
305
The current route classifier treats all `/api/v3` `GET` requests as public.
306
`GET /api/v3/user/repos` is an authenticated exception and needs an explicit
307
route-authority declaration and test.
308
309
### Create request
310
311
```http
312
POST /api/v3/user/repos
313
Authorization: Bearer oa_pat_...
314
Content-Type: application/json
315
Idempotency-Key: 3ec9fce0-45dd-45b3-93f0-1d1ed3bd4efa
316
```
317
318
```json
319
{
320
  "name": "my-project",
321
  "description": "An optional description",
322
  "private": true,
323
  "default_branch": "main"
324
}
325
```
326
327
Rules:
328
329
- `name` is required.
330
- `description` is optional and bounded to 350 Unicode scalar values.
331
- `private` defaults to `true`.
332
- `default_branch` defaults to `main` and follows Git ref-name validation.
333
- The server derives the personal namespace from the authenticated principal.
334
- `Idempotency-Key` is required for the CLI and recommended for every client.
335
336
The same principal, idempotency key, and normalized request returns the original
337
result. Reusing the key with a different normalized request returns `409
338
Conflict`.
339
340
### Repository response
341
342
```json
343
{
344
  "id": "31fb2eb8-c6f9-4dad-80bd-2e532da9ad7f",
345
  "name": "my-project",
346
  "full_name": "octavia/my-project",
347
  "owner": {
348
    "login": "octavia",
349
    "type": "User"
350
  },
351
  "private": true,
352
  "visibility": "private",
353
  "description": "An optional description",
354
  "default_branch": "main",
355
  "lifecycle_state": "ready",
356
  "clone_url": "https://openagents.com/git/octavia/my-project.git",
357
  "html_url": "https://openagents.com/octavia/my-project",
358
  "permissions": {
359
    "admin": true,
360
    "push": true,
361
    "pull": true
362
  },
363
  "created_at": "2026-08-20T18:00:00Z",
364
  "updated_at": "2026-08-20T18:00:00Z"
365
}
366
```
367
368
The server may add fields. The CLI must ignore unknown fields and fail when a
369
required field has the wrong type.
370
371
### Status and error behavior
372
373
| Status | Meaning |
374
| --- | --- |
375
| `201 Created` | Repository is ready |
376
| `202 Accepted` | Repository exists and provisioning continues |
377
| `400 Bad Request` | Malformed JSON or header |
378
| `401 Unauthorized` | Missing, invalid, expired, or revoked token |
379
| `403 Forbidden` | Authenticated principal lacks the required authority |
380
| `404 Not Found` | Repository is absent or hidden from the principal |
381
| `409 Conflict` | Name or idempotency conflict |
382
| `422 Unprocessable Entity` | Valid JSON violates repository rules |
383
| `503 Service Unavailable` | Provisioning cannot currently start |
384
385
Use one bounded error envelope:
386
387
```json
388
{
389
  "message": "Repository name is unavailable",
390
  "code": "repository_name_conflict",
391
  "field": "name",
392
  "request_id": "req_..."
393
}
394
```
395
396
`code` is the automation contract. `message` is user-facing text and can
397
change. Do not include database, filesystem, Git, object-store, or provider
398
error prose.
399
400
### Pagination
401
402
`GET /api/v3/user/repos` uses a bounded opaque cursor:
403
404
- `per_page` defaults to 30 and permits 1 through 100.
405
- `after` carries an opaque server cursor.
406
- The response contains `repositories` and `next_cursor`.
407
- Ordering is stable by normalized namespace, normalized name, and UUID.
408
409
Do not use an unbounded list because the first browser dashboard happens to
410
contain one repository today.
411
412
## API and Git authentication
413
414
### Existing personal API tokens
415
416
Keep `oa_pat_...` as the first CLI bearer format. Repository creation requires
417
`forge:write`, and the server still applies repository or namespace policy after
418
token authentication. A scope never grants access to every repository.
419
420
Existing issue and project clients retain their current behavior. Add scope or
421
token-format changes only through a separately documented migration.
422
423
### Browser-assisted CLI login
424
425
Add a device-style browser flow so a terminal never handles the GitHub OAuth
426
grant:
427
428
1. `openagents auth login` creates a short-lived CLI authorization request.
429
2. The server returns a secret device code, a short user code, a verification
430
   URL, an expiry, and a polling interval.
431
3. The CLI opens the verification URL when the platform supports it and prints
432
   the URL and user code as a fallback.
433
4. The user signs in through the existing GitHub flow and reviews the requested
434
   `forge:write` scope and token lifetime.
435
5. The authenticated, CSRF-protected browser approves or denies the request.
436
6. The CLI polls with the secret device code.
437
7. On approval, the server returns one `oa_pat_...` plaintext exactly once and
438
   stores only its digest.
439
8. The CLI stores the token in an admitted operating-system credential store.
440
441
Store device codes as digests, expire them within 10 minutes, allow one terminal
442
claim, rate-limit polling, and return the same refusal for unknown, expired,
443
claimed, or denied codes where enumeration would reveal state.
444
445
### Headless and agent authentication
446
447
An agent or noninteractive process supplies `OPENAGENTS_TOKEN`. The CLI reads
448
the variable at execution time and does not persist it.
449
450
Support `openagents auth login --token-stdin` for attended automation that
451
needs to store an existing token without placing it in argv or shell history.
452
Do not add a `--token <plaintext>` option. Command-line arguments can appear in
453
process listings and diagnostic output.
454
455
### Git credential helper
456
457
`openagents auth login` offers to configure the Git credential helper. The
458
helper:
459
460
- Implements Git's credential-helper stdin and stdout protocol.
461
- Returns a credential only for an exact configured HTTPS host.
462
- Uses the operating-system credential store or `OPENAGENTS_TOKEN`.
463
- Never returns credentials for plain HTTP except an explicit loopback
464
  development endpoint.
465
- Never logs the request, response, token, or complete credential-helper input.
466
- Erases the stored token on `openagents auth logout`.
467
468
`openagents auth setup-git --local` changes only the current repository.
469
`openagents auth setup-git --global` requires explicit confirmation in an
470
interactive terminal.
471
472
### Git authorization
473
474
Resolve Git paths through the repository database:
475
476
- Anonymous `upload-pack` is allowed only for a `ready`, public repository.
477
- Authenticated `upload-pack` requires pull access to the resolved repository.
478
- `receive-pack` requires an active user, `forge:write`, and a writable
479
  repository membership.
480
- Machine credentials require an explicit repository grant and operation
481
  scope. Pairing a machine does not grant access to every repository.
482
- The operator credential remains an operational recovery path. It must not be
483
  the normal CLI credential.
484
- A private, missing, suspended, failed, or unauthorized repository returns an
485
  indistinguishable refusal where the transport permits it.
486
487
Change the canonical Git path from a name-only route to
488
`/git/{owner}/{repo}.git`. Keep a tested compatibility alias for the existing
489
`/git/openagents.com.git` remote until the canonical repository cutover plan
490
retires it.
491
492
## Browser experience
493
494
### Repository list
495
496
Replace the signed-in dashboard's single hard-coded repository card with a
497
bounded list of repositories the user can access.
498
499
The list includes:
500
501
- Namespace and repository name.
502
- Public or private visibility.
503
- Description when present.
504
- Updated time.
505
- Open issue count when available without an unbounded query.
506
- A **New repository** action.
507
508
Use a LiveView stream and separate count and empty-state assigns. Add search and
509
pagination after the base list works.
510
511
### New repository page
512
513
Add an authenticated `/repositories/new` route with:
514
515
- A read-only or selectable namespace field, depending on enrollment state.
516
- A repository name input.
517
- An optional description.
518
- A `private` or `public` visibility choice, with `private` selected initially.
519
- A default-branch input set to `main`.
520
- A **Create repository** button with pending and disabled states.
521
522
Use `OpenAgentsWeb.UI` components, `to_form/2`, stable DOM IDs, and the
523
authenticated LiveView session. The browser calls the same context operation as
524
the API and never invokes forge filesystem code directly.
525
526
### Empty repository page
527
528
After creation, the repository page shows:
529
530
- Provisioning progress until the repository reaches `ready`.
531
- The HTTPS clone URL from the repository projection.
532
- Commands for cloning an empty repository.
533
- Commands for adding the repository as a remote to an existing checkout.
534
- A copy control with an accessible name.
535
- Links to Issues and Projects for the same repository.
536
537
Do not show push instructions until the repository is `ready`.
538
539
### Repository route
540
541
The preferred repository URL remains `/{owner}/{repo}` so code, Issues, and
542
Projects share one GitHub-shaped root. Place the dynamic repository-home route
543
after every reserved first-segment route and maintain an executable reserved
544
segment inventory. Add route tests proving that `/api`, `/auth`, `/admin`,
545
`/chat`, `/docs`, `/settings`, `/status`, and future declared product routes
546
cannot be interpreted as namespaces.
547
548
If that route contract cannot be proven without fragile ordering, use
549
`/code/{owner}/{repo}` and keep the Issue and Project routes unchanged. Resolve
550
this route decision before implementation begins.
551
552
## CLI product contract
553
554
The proposed binary name is `openagents`. Reserve `oa` as a possible later
555
alias; do not make scripts depend on it in the first release.
556
557
This CLI is the first-party repository-hosting client. It does not replace the
558
Pylon contributor runtime or absorb Pylon's agent-execution commands.
559
560
### Global behavior
561
562
```text
563
openagents [--endpoint <url>] [--json] [--no-color] <command>
564
```
565
566
- `--endpoint` defaults to `https://openagents.com`.
567
- `OPENAGENTS_ENDPOINT` provides the same setting for development and staging.
568
- `OPENAGENTS_TOKEN` provides a nonpersistent bearer token.
569
- `NO_COLOR` and `--no-color` disable ANSI output.
570
- `--json` emits one documented JSON value to stdout.
571
- Human progress goes to stderr when stdout carries machine-readable output.
572
- `--help` works at the root, group, and command levels.
573
- `--version` prints the CLI version and exits.
574
575
The CLI must not send telemetry in the first release.
576
577
### Authentication commands
578
579
```text
580
openagents auth login
581
openagents auth login --token-stdin
582
openagents auth status
583
openagents auth setup-git --local
584
openagents auth setup-git --global
585
openagents auth logout
586
```
587
588
`auth status --json` reports the endpoint, authentication source, account login,
589
namespace, token expiry, and Git-helper state. It never reports a token or token
590
digest.
591
592
### Repository commands
593
594
```text
595
openagents repo create <name>
596
openagents repo create <owner>/<name>
597
openagents repo list
598
openagents repo view [<owner>/<name>]
599
openagents repo clone <owner>/<name> [<directory>]
600
```
601
602
`repo create <name>` targets the authenticated user's personal namespace.
603
`repo create <owner>/<name>` succeeds only when the authenticated user can
604
create repositories in that namespace. Team namespace creation remains
605
unavailable until the server supports it.
606
607
Create options:
608
609
```text
610
--public
611
--private
612
--description <text>
613
--default-branch <branch>
614
--remote <name>
615
--source <directory>
616
```
617
618
Rules:
619
620
- `--public` and `--private` are mutually exclusive. Omission means private.
621
- Without `--source`, the command creates only the remote repository.
622
- With `--source`, the command verifies that the directory is a Git worktree,
623
  creates or updates the named remote after server creation succeeds, and
624
  prints the next push command.
625
- The first release does not push automatically. A future `--push` flag needs a
626
  separate confirmation and branch-selection contract.
627
- `--remote` defaults to `origin` only when that remote is absent. If `origin`
628
  already points elsewhere, the command refuses to overwrite it.
629
630
`repo list` supports `--namespace`, `--limit`, `--after`, and `--json`.
631
`repo view` and later repository-scoped commands infer the repository from the
632
`origin` remote when no argument is present. `-R, --repo <owner>/<name>`
633
overrides inference.
634
635
Remote inference accepts only clone URLs returned by an admitted OpenAgents
636
endpoint. It must not treat an arbitrary path that resembles `owner/name` as an
637
authenticated OpenAgents repository.
638
639
### Future command reservations
640
641
Reserve these names without shipping placeholder commands:
642
643
```text
644
openagents repo delete
645
openagents repo mirror
646
openagents pr ...
647
openagents ruleset ...
648
openagents ssh-key ...
649
openagents api ...
650
openagents update
651
openagents completion
652
```
653
654
A help page must describe only commands that work in the installed version.
655
656
### Noninteractive behavior
657
658
When stdin or stdout is not a terminal:
659
660
- Never open a browser or prompt for confirmation.
661
- Require `OPENAGENTS_TOKEN` or an existing admitted credential-store entry.
662
- Require all ambiguous values as flags or arguments.
663
- Return stable exit codes.
664
- Keep stdout machine-readable when `--json` is present.
665
- Cancel in-flight HTTP requests and child Git processes on `SIGINT` and
666
  `SIGTERM`.
667
668
Use these initial exit-code classes:
669
670
| Code | Meaning |
671
| --- | --- |
672
| `0` | Success |
673
| `1` | Unclassified operational failure |
674
| `2` | Usage or validation error |
675
| `3` | Authentication or authorization failure |
676
| `4` | Repository not found or hidden |
677
| `5` | Conflict |
678
| `6` | Network or service unavailable |
679
| `7` | Repository provisioning failed or timed out |
680
| `130` | Interrupted by the user |
681
682
The JSON error shape contains `code`, `message`, `exit_code`, and `request_id`
683
when the server supplies one.
684
685
## CLI implementation proposal
686
687
Create the CLI in the `openagents` monorepo at
688
`packages/openagents-cli/` with:
689
690
- Package name `@openagentsinc/openagents-cli`.
691
- Binary name `openagents`.
692
- Node 24, pnpm, and Vite Plus, matching the monorepo contract.
693
- Effect and Effect Schema for services, configuration, response decoding,
694
  typed failures, resource scopes, retries, interruption, and tests.
695
- A public package boundary only if the release process intends npm
696
  distribution. Keep it private during the first implementation slice if the
697
  release format remains undecided.
698
699
The monorepo requires Effect for new TypeScript service and CLI logic. The
700
remaining decision is the command-parser package, not whether business logic
701
uses Effect. Do not add `@effect/cli` or another parser until a small spike
702
proves compatibility with the pinned Effect version, help output, completion,
703
and packaged binaries.
704
705
### Effect service boundaries
706
707
Keep command handlers thin and compose these services:
708
709
| Service | Responsibility |
710
| --- | --- |
711
| `CliConfiguration` | Endpoint, output mode, timeouts, and environment inputs |
712
| `CredentialStore` | Secure token presence, read, write, and erase |
713
| `AuthClient` | Device authorization, status, and logout |
714
| `ForgeApiClient` | Authenticated repository API calls and schema decoding |
715
| `RepositoryResolver` | Parse `-R`, environment, and admitted Git remotes |
716
| `GitClient` | Run argv-only Git commands with bounded output and cancellation |
717
| `GitCredentialHelper` | Implement the exact Git credential protocol |
718
| `BrowserLauncher` | Open the verification URL in an attended session |
719
| `ConsoleOutput` | Human and JSON rendering without secret leakage |
720
721
Model expected failures as tagged errors and map them to the exit-code table in
722
one place. Preserve server error codes instead of parsing user-facing messages.
723
724
The monorepo contains `@openagentsinc/local-secret-store`, including platform
725
adapter contracts and some owner-attended adapters. Run a focused suitability
726
review before reuse. A CLI credential store must work for ordinary attended
727
users and fail closed in headless environments; it must not require a hidden
728
developer-only acknowledgment.
729
730
The monorepo also contains `@openagentsinc/forge-protocol`. Its README labels it
731
a historical coordination contract and says the hosted `openagents.com` service
732
owns current API authority. Do not make it the repository-creation authority.
733
Reuse a type only if it matches a new Phoenix-owned public API contract and no
734
historical D1, R2, Nostr, or standalone-service assumption crosses the boundary.
735
736
The CLI targets the Phoenix endpoints in this specification. It does not target
737
`apps/forge-git-service` as a separate API authority. Moving the Git data plane
738
behind that service would require a separate architecture and deployment
739
decision while preserving the Phoenix policy authority.
740
741
### Client contract synchronization
742
743
The server repository owns the HTTP contract. Keep the two repositories in
744
sync through a versioned artifact:
745
746
1. Add repository endpoint controller tests and JSON fixtures in
747
   `openagents.com`.
748
2. Publish a bounded OpenAPI document or equivalent JSON Schema artifact for
749
   the repository endpoints.
750
3. Record its version and SHA-256 digest in the CLI source.
751
4. Generate or hand-author Effect Schema decoders against that artifact.
752
5. Run CLI contract tests against the server-owned fixtures.
753
6. Fail the release gate when a required field or error code changes without a
754
   contract-version update.
755
756
The OpenAPI or JSON Schema artifact describes the client contract. Phoenix
757
controller tests remain the executable server truth.
758
759
### Distribution
760
761
The desired user experience is a single installer followed by
762
`openagents --version`, but the packaging mechanism needs a spike.
763
764
Evaluate these release steps in order:
765
766
1. Run the CLI from the monorepo with Node 24 during development.
767
2. Publish an npm development preview if it improves dogfooding.
768
3. Produce checksum-verified standalone artifacts for macOS, Linux, and WSL.
769
4. Serve a versioned installer from `openagents.com` only after artifact signing,
770
   rollback, and update-channel behavior pass release tests.
771
5. Add `openagents update` only after the updater verifies a signed manifest and
772
   artifact digest before replacement.
773
774
Do not publish a `curl | sh` instruction before the script pins and verifies the
775
downloaded artifact.
776
777
## Security and privacy requirements
778
779
- Derive every namespace and repository mutation from an authenticated
780
  principal.
781
- Check membership and lifecycle state in the same query that resolves a
782
  repository for API or Git access.
783
- Store API tokens, device codes, and poll secrets only as digests on the
784
  server.
785
- Show a CLI token once and never include it in export, logs, telemetry,
786
  receipts, exception messages, or JSON output.
787
- Do not put credentials in clone URLs.
788
- Bound request bodies, Git diagnostics, descriptions, names, list sizes,
789
  retries, polling, and total command duration.
790
- Treat repository descriptions, README files, and other repository content as
791
  untrusted input.
792
- Apply the existing Markdown sanitization boundary to rendered repository
793
  content.
794
- Return `404 Not Found` for a private repository when the caller must not learn
795
  that it exists.
796
- Audit repository creation, provisioning transitions, membership creation,
797
  token creation, and Git writes without recording repository content.
798
- Prevent repository creation from changing runtime configuration or deployment
799
  targets.
800
801
## Test plan
802
803
### Phoenix domain and API tests
804
805
Add focused tests for:
806
807
- Namespace claim, normalization, collision, and immutability.
808
- Repository name and default-branch validation.
809
- Atomic repository, owner-membership, and outbox creation.
810
- Idempotent repeat requests and mismatched idempotency keys.
811
- Personal namespace authority and refusal of arbitrary owner creation.
812
- Public, private, member, nonmember, banned-user, suspended, failed, and
813
  missing repository reads.
814
- `201`, `202`, `401`, `403`, `404`, `409`, `422`, and `503` behavior.
815
- Pagination order and cursor bounds.
816
- Route-authority classification for authenticated `GET /api/v3/user/repos`.
817
- Cross-repository isolation for the new endpoints.
818
819
Follow the repository's endpoint-first test-driven workflow: start each route
820
with a failing `OpenAgentsWeb.ConnCase` test before adding the route or
821
controller action.
822
823
### Provisioning and Git tests
824
825
Add tests for:
826
827
- A crash after each provisioning transition followed by convergence.
828
- Duplicate outbox delivery.
829
- WAL initialization before `ready`.
830
- Bare-cache deletion followed by reconstruction.
831
- Two repositories with the same name in different namespaces.
832
- Anonymous clone of a public repository.
833
- Hidden private clone without a credential.
834
- Member clone, contributor push, reader push refusal, and nonmember refusal.
835
- Token expiry and revocation during a Git session.
836
- A paired machine with and without an explicit repository grant.
837
- Compatibility of the existing `openagents.com` Git remote.
838
- Proof that a user repository cannot enter build, promotion, or deployment
839
  paths.
840
841
### LiveView tests
842
843
Add tests for stable DOM IDs and outcomes:
844
845
- Repository list, empty state, and pagination.
846
- Namespace enrollment state.
847
- Create form validation and submission.
848
- Private visibility as the default.
849
- Provisioning, ready, and failed states.
850
- Clone instructions only after readiness.
851
- Navigation to Issues and Projects under the new repository path.
852
853
### CLI tests
854
855
Use deterministic Effect layers and fake clocks for:
856
857
- Command parsing and generated help.
858
- Environment, stored credential, and endpoint precedence.
859
- Device authorization approval, denial, expiry, rate limiting, and
860
  interruption.
861
- Token redaction from human output, JSON, errors, logs, and snapshots.
862
- Repository create `201` and `202` flows.
863
- Idempotent retry after a connection failure.
864
- Remote inference and refusal of unadmitted hosts.
865
- Refusal to overwrite an existing `origin` remote.
866
- Git credential-helper protocol transcripts.
867
- TTY and non-TTY behavior.
868
- Stable exit-code and JSON error mappings.
869
- Unknown response fields and invalid required response fields.
870
871
Run an end-to-end suite against a disposable Phoenix server, PostgreSQL
872
database, forge storage directory, and Git checkout. The suite must create a
873
repository through the CLI, push a commit with standard Git, clone it into a
874
second directory, and verify the exact commit SHA.
875
876
## Delivery sequence
877
878
### Phase 0: Close design decisions
879
880
1. Approve the CLI binary and package names.
881
2. Approve the personal namespace enrollment behavior.
882
3. Approve the canonical browser and Git URL shapes.
883
4. Select the initial CLI distribution format.
884
5. Record the repository lifecycle and authority split in `INVARIANTS.md`.
885
886
### Phase 1: Add namespace and repository authority
887
888
1. Generate migrations with `mix ecto.gen.migration`.
889
2. Add namespace ownership and repository lifecycle schemas.
890
3. Backfill `OpenAgentsInc/openagents.com` into an explicit namespace.
891
4. Add transactionally created owner membership and provisioning outbox rows.
892
5. Rehearse the migration down and up against populated fixtures.
893
894
### Phase 2: Add create, list, and view APIs
895
896
1. Add failing controller tests for `POST /api/v3/user/repos`.
897
2. Implement the context operation, controller, JSON projection, and route.
898
3. Add authenticated repository list and optional-auth repository view.
899
4. Add idempotency, cursor, error-envelope, and route-authority tests.
900
5. Publish the initial client contract artifact.
901
902
### Phase 3: Provision Git repositories
903
904
1. Make the hosted repository inventory database-backed.
905
2. Change storage keys and Git paths to include stable repository identity.
906
3. Add the idempotent provisioning worker and recovery scan.
907
4. Scope upload-pack and receive-pack through repository policy.
908
5. Preserve the deployment allowlist as a separate operator control.
909
6. Prove create, push, cache loss, reconstruction, fetch, and clone locally.
910
911
### Phase 4: Add CLI authentication
912
913
1. Add device authorization records and endpoints.
914
2. Add the authenticated browser approval page.
915
3. Add one-time PAT delivery and expiry behavior.
916
4. Implement the CLI credential store and Git credential helper.
917
5. Pass secret-handling and headless-agent tests.
918
919
### Phase 5: Build the Effect CLI
920
921
1. Create `packages/openagents-cli` in the monorepo.
922
2. Pin the Phoenix-owned contract artifact.
923
3. Implement `auth` and `repo` command groups through Effect services.
924
4. Add human output, JSON output, exit-code, signal, and redaction tests.
925
5. Run the cross-repository disposable end-to-end suite.
926
927
### Phase 6: Add the browser interface
928
929
1. Replace the hard-coded dashboard repository card with the scoped list.
930
2. Add namespace enrollment and repository creation pages.
931
3. Add provisioning and empty-repository states.
932
4. Run accessibility, keyboard, responsive, and browser checks.
933
934
### Phase 7: Stage and release
935
936
1. Run `mix precommit` and the owned exact-SHA gate in `openagents.com`.
937
2. Run `pnpm run check` in the CLI monorepo.
938
3. Deploy to an isolated staging environment only after Gate 12 permits it.
939
4. Create, push, clone, revoke, retry, and recover on one staging candidate.
940
5. Scan the complete log window for tokens, clone credentials, repository
941
   content, and private paths.
942
6. Publish the CLI only after the server candidate and client contract digest
943
   match.
944
945
## Acceptance criteria
946
947
The first repository-creation slice is complete when:
948
949
- An authenticated user has one stable personal namespace.
950
- The browser and CLI create the same repository resource through the same
951
  context policy.
952
- A successful command returns only after the repository is ready, or reports a
953
  durable provisioning failure with a stable code.
954
- The creator receives an `owner` membership in the same database transaction
955
  as the repository row.
956
- A repeat request with the same idempotency key cannot create a duplicate.
957
- A public repository clones anonymously.
958
- A private repository is hidden from a nonmember.
959
- A permitted user can push and a read-only or unrelated user cannot.
960
- Deleting the node-local bare cache does not lose accepted Git history.
961
- Creating a repository cannot make it deployable.
962
- The CLI works in attended and noninteractive modes without placing a token in
963
  argv, logs, JSON, or a clone URL.
964
- `openagents repo create`, standard `git push`, and `openagents repo clone`
965
  complete an exact-SHA end-to-end test.
966
- Both repositories pass their required local gates at the exact delivered
967
  revisions.
968
969
## Decisions required before implementation
970
971
The owner must resolve these items before Phase 1 begins:
972
973
1. **Binary name:** Use `openagents`, `oa`, or both.
974
2. **Namespace enrollment:** Claim the suggested GitHub-login slug
975
   automatically, or require a one-time browser confirmation for every user.
976
3. **Repository home route:** Use `/{owner}/{repo}` with a reserved-route
977
   contract, or use `/code/{owner}/{repo}`.
978
4. **Distribution:** Start with npm, standalone artifacts, or both.
979
5. **Effect command parser:** Use a parser compatible with the pinned Effect
980
   version, or keep parsing local while all effects remain in Effect services.
981
982
This document recommends `openagents`, automatic claim with an explicit
983
collision flow, `/{owner}/{repo}`, a short npm dogfood phase followed by signed
984
standalone artifacts, and Effect TypeScript with a parser selected by a bounded
985
spike.

This page updates live while a promote is in flight · changelog