Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .commitrail/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ for current product capability.
|---|---|---|
| [WS-DB-002](initiatives/WS-DB-002/OVERVIEW.md) | Complete | Shared UUIDv7 record generation, native-UUID relationships and fresh v0.1 baseline; natural-owner retry custody and aligned CI/local setup |
| [WS-MCP-002](initiatives/WS-MCP-002/OVERVIEW.md) | Planned | Nine tools through WS-MCP-002-03: self-service and administrative reads; 18 tools remain and WS-MCP-002-04 administrative grant mutations are next |
| [WS-CLI-001](initiatives/WS-CLI-001/OVERVIEW.md) | Planned | Public Go CLI self-service, project inspection, manager browsing, contributor discovery, claim/start and governing context/intake requirements delivered through WS-CLI-001-07; hidden submission is not exposed, further public journeys and binary distribution remain |
| [WS-CLI-001](initiatives/WS-CLI-001/OVERVIEW.md) | Planned | Public Go CLI self-service, project inspection, manager browsing, contributor discovery, claim/start, governing context/intake requirements and draft project creation delivered through WS-CLI-001-08; hidden submission is not exposed, guide setup, further public journeys and binary distribution remain |
| [WS-ARCH-001](initiatives/WS-ARCH-001/OVERVIEW.md) | Planned | Source storage, inert AUTH contracts, TASK request reservation, hidden exact AUTH preparation and the REV-04C hidden FinalAcceptance/TASK/CON participant are delivered; TASK-before-CHECKERS reservation/current-read custody and ordered admission INSERTs are delivered by 04E1B-B1; 04E1B-B2 adds exact source preparation without publication; 04E1B-B3 retains inspected ZIP metadata and B4 binds Submission text to checked packet custody; initial Submission/dispatch composition is next under the [first-layer sequence](initiatives/WS-ARCH-001/planning/PLAN.md#first-complete-contributor-milestone), ending in a public contributor drill before live human review/revision. True admission remains independent of shared acceptance; false activation still requires exact AUTH receipt custody, database complete-set enforcement, currentness race proof and atomic routing activation. |
| [WS-ART-001](initiatives/WS-ART-001/OVERVIEW.md) | Planned | Exact checker input/output custody and packet foundations are delivered; routing integration, remediation and public intake remain. |
| [WS-AUTH-001](initiatives/WS-AUTH-001/OVERVIEW.md) | Planned | AUTH-19A commitments, TASK request reservation, ARCH-04E2-A hidden strict PREP matching and the REV-04C hidden FinalAcceptance/TASK/CON participant are delivered; the action remains unavailable, with mandatory exact AUTH receipt input, database closure, audit/outbox and scoped activation still required for the automated path. |
Expand Down
13 changes: 10 additions & 3 deletions .commitrail/initiatives/WS-CLI-001/OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
[WS-CLI-001-04](WS-CLI-001-04.md), public manager task pagination and detail;
[WS-CLI-001-05](WS-CLI-001-05.md), contributor ready-task discovery and instructions;
[WS-CLI-001-06](WS-CLI-001-06.md), public contributor claim/start with explicit retry keys;
[WS-CLI-001-07](WS-CLI-001-07.md), contributor governing context and locked intake requirements.
[WS-CLI-001-07](WS-CLI-001-07.md), contributor governing context and locked intake requirements;
[WS-CLI-001-08](WS-CLI-001-08.md), draft project shell creation with explicit caller-owned replay.

## Current boundary

Expand All @@ -35,6 +36,9 @@ Exact caller keys support manual replay; uncertainty never triggers automatic re
`task context TASK_ID` and `task requirements TASK_ID` read governing guide,
policy selectors, server action hints and locked intake rules. Hints are not
authority; these reads do not expose the hidden submission intake.
`project create --name TEXT --slug TEXT --idempotency-key UUID` creates a draft
shell through the public POST. The API owns system-scoped creation authority
and committed recovery; guide upload, approval and activation are not added.
All have text/JSON
output and built-binary integration proof. Mutations preserve omitted/null
semantics and explicitly report uncertain outcomes without automatic retries.
Expand Down Expand Up @@ -85,11 +89,14 @@ CLIs. Keep the package independent of backend and MCP runtime dependencies.
7. **WS-CLI-001-07:** Inspect contributor work context and exact submission
requirements through public reads. Preserve historical selectors, server
hints and contributor disclosure without fetching files or evaluating policy.
8. **Later governed-work commands:** Add project setup, submission,
8. **WS-CLI-001-08:** Create a draft project shell through the public POST with
explicit caller-supplied keys, exact 201/full-response validation and unknown
outcomes. Reuse project inspection parsing/output; no guide activation.
9. **Later governed-work commands:** Add further project setup, submission,
review, revision, and contribution reads/writes only as their actual public
contracts and authority boundaries become available. Split by user journey,
not one PR per endpoint or one giant catalogue PR.
9. **Optional TUI:** Add a focused public queue/evidence view after its API
10. **Optional TUI:** Add a focused public queue/evidence view after its API
workflow is complete. Never require a TUI for agents or scripts.

## Risks and proof
Expand Down
121 changes: 121 additions & 0 deletions .commitrail/initiatives/WS-CLI-001/WS-CLI-001-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# WS-CLI-001-08 — Create a draft project shell

- Initiative: `WS-CLI-001`
- Durable disposition: `Complete`
- Intended merge outcome: Create a project shell through the existing public
REST operation, preserving explicit replay custody and uncertain outcomes.

## Intent

Inspection and contributor preparation are delivered. Begin the already
proposed project-setup journey with `POST /api/v1/projects`, publicly registered
by `projects/create_router.py`. `ProjectCreateService` owns system-scoped
Project Manager authorization, server-generated identity, transaction and
actor/action/key/body replay. The result is a project shell, not an approved
guide or claimable work. No backend implementation or permission is added.

## Bounded change

Allowed: `cli/internal/api/project_create.go`, `project.go`, `client.go` and
`task_mutations.go`; `cli/internal/command/project_create.go` and `command.go`;
`cli/tests/integration/test_project_create_http.py`, `project_create_journey.py`
and `test_public_self_service.py`; CLI/root README, affected roadmap claims,
this record, overview and index. No backend, MCP, workflow/dependency, hidden
route, guide creation/upload/activation, grants, local authority/policy engine,
credential persistence, preflight, generated key, automatic retry or TUI work.

Add `workstream project create --name TEXT --slug TEXT --idempotency-key UUID
[--description TEXT]`. Require both flags, including explicit empty values
where accepted by the API. Validate UTF-8, NUL exclusion and the backend's
200/120-character name/slug bounds. Preserve supplied text, whitespace and
description omission versus explicit empty. Reuse the existing 8KiB encoded
request bound; description has no additional character ceiling. Send exactly
one fixed POST, with unchanged bearer/key and no request preflight.

Extend the existing private transport with a caller-selected expected success
status: existing operations retain 200, creation requires 201. Never accept an
arbitrary 2xx as success. Preserve one-shot mutation bodies, redirect/proxy
refusal, deadlines, response bounds and credential-safe errors. Reuse the
canonical POST error validator for project creation, naming it for its shared
mutation responsibility rather than copying a task-specific implementation.
Do not alter the profile PATCH error contract.

Share project response parsing/output with `project show`. Creation requires
the complete seven-field administrative ProjectResponse, valid UUID/timestamps,
nullable description and no unknown/duplicate/null required fields. It cannot
bind a returned ID to a nonexistent input selector. Preserve the exact raw JSON
and show all seven fields safely. Do not require every replay to say `draft`:
the backend recovers the current stored project, which may have progressed.

Lost, malformed, oversized, redirected, unexpected-status and noncanonical
responses are unknown mutation outcomes. Only a complete canonical 4xx proves
a known denial. Text recovery guidance retains the same name/slug/description
and key; it must not invent a project ID, promise rollback or suggest a new key.
Existing project-create replay deliberately recovers a committed result after
creation-grant revocation (`test_project_create_exact_replay_and_mismatch_are_atomic`).
Do not impose TASK's different replay semantics in the client or claim that
recovery authorizes another creation. Fresh creation still requires authority.

## Acceptance criteria

- Named built-process HTTP cases prove one POST and exact 201 response, all
request fields, key spellings, omission/empty description, accepted text
boundaries, complete escaped text/raw JSON, and invalid input without network.
- Accept explicit null response description; reject a missing description member
or a non-string/non-null value. Reject minimal GET-only projection, malformed
UUID/time/required field, unknown/duplicate members and unexpected 200/202.
Exercise known canonical 4xx versus malformed envelope, including null
details, dropped response, redirect and oversized reply; stdout stays empty.
- A real TLS/HTTP2 GOAWAY case proves exactly one transmitted creation body
and an unknown outcome. Simulated rewindable-body variant must fail that case.
- Extend the existing one-bootstrap public FastAPI/PostgreSQL journey via
`project_create_journey.py`, not a second parallel fixture. Assert OpenAPI
exposure, persisted creation/direct-GET parity, exact manual replay and body
mismatch; slug collision is a server conflict, not an overwrite. No private
product writes or fake authorization. Prove a project-scoped manager and
absent/Access Administrator-only authority cannot create; use public grants
and fresh-key creation controls to prove revocation and suspension denial.
Use the spare scoped-manager actor: add system authority for a positive create,
revoke only that system grant for exact recovery/new-key denial, restore system
authority and prove another positive create before suspension. Reactivate and
revoke the test grants afterward so the existing journey keeps its baseline.
Preserve existing read/task tests and backend-owned replay semantics.
- Go build/vet/verify/tidy-diff; Ruff/format; complete CLI process/API suite;
unchanged workflow guards; links/stale scans/Commitrail and full hosted
Backend remain required. No coverage/count quotas or weakened selection.

## Risk and review routing

Risk `L1`: shared status handling, duplicate creation after lost responses,
bearer leakage and false authority/recovery claims. Plan feasibility review
before code; three focused assignments for architecture/docs, security and
QA/test-delta. Shared transport proof must retain all existing command behavior.
Lead owns deterministic checks and hosted monitoring, not nine reviewer runs.
Human focus: exact 201, caller-owned key, project shell versus guide readiness,
and replay recovery versus authority for a fresh write.

## Evidence

Exact execution and review results belong in the PR. Tests run the installed
binary, not Go functions. Real PostgreSQL proves stored projects and API
authorization; HTTP peers prove wire, malformed responses and transport replay.

```sh
cd cli
go mod verify
go mod tidy -diff
go vet ./...
go build -trimpath -o /tmp/workstream-cli ./cmd/workstream
cd ../backend
WORKSTREAM_CLI_EXECUTABLE=/tmp/workstream-cli python scripts/run_isolated_tests.py --metadata-json /tmp/workstream-cli-isolation.json --timeout-seconds 240 -- python -m pytest -q ../cli/tests/integration
```

Reject a separate create transport/DTO/output renderer, generic API passthrough,
implicit whoami/access preflight, default retries and local grant inference.
Source availability is not binary distribution or deployed-provider evidence.

Plan correction `PLAN-CLI08-NULL-DESCRIPTION` distinguishes a legitimate null
response description from missing or wrong-type data; only required scalar
fields reject null. Project-create's existing committed recovery returns before
PREP. Suspended/revoked-link replay is an owner safety question to hand off,
not a new client-side guard or a claim of fresh replay authorization.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ explicit caller retry keys and uncertain-outcome reporting without automatic ret
`workstream task context TASK_ID` and `task requirements TASK_ID` inspect the
governing guide/policy selectors, server action hints and locked intake rules.
Hints do not grant authority; requirements do not expose hidden submission upload.
`workstream project create --name TEXT --slug TEXT --idempotency-key UUID`
creates a draft project shell through the public API, with explicit manual
replay and uncertain-outcome handling; it does not approve or activate a guide.
All support human-readable and JSON output, using the caller's Flow token.
The first source package is buildable; further workflow commands and published
binaries remain planned.
Expand Down
45 changes: 44 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

An independent Go client for Workstream's public REST API, for humans and
agents using the terminal. It provides human self-profile reads and editing,
plus exact-project inspection, authority reads, manager task browsing and
plus draft project creation, exact-project inspection, authority reads, manager task browsing and
contributor work discovery, claim/start and governing context/intake requirements:

| Command | Public API |
Expand All @@ -11,6 +11,7 @@ contributor work discovery, claim/start and governing context/intake requirement
| `workstream profile update` | `PATCH /api/v1/actors/me` |
| `workstream project access PROJECT_ID` | `GET /api/v1/actors/me/authorization-context?project_id=PROJECT_ID` |
| `workstream project show PROJECT_ID` | `GET /api/v1/projects/PROJECT_ID` |
| `workstream project create --name TEXT --slug TEXT --idempotency-key UUID` | `POST /api/v1/projects` |
| `workstream project tasks PROJECT_ID` | `GET /api/v1/projects/PROJECT_ID/tasks` |
| `workstream project task PROJECT_ID TASK_ID` | `GET /api/v1/projects/PROJECT_ID/tasks/TASK_ID` |
| `workstream task ready PROJECT_ID` | `GET /api/v1/projects/PROJECT_ID/tasks/ready` |
Expand Down Expand Up @@ -92,6 +93,48 @@ object; text escapes terminal controls. Foreign or revoked authority remains
a server denial with empty stdout, not an empty successful project.
This command does not list projects, edit setup, activate guides or claim tasks.

## Create a draft project shell

```sh
workstream project create --name 'Evaluation project' --slug evaluation --idempotency-key CREATE_UUID --output json
workstream project create --name 'Research project' --slug research --description 'Evaluate the supplied evidence.' --idempotency-key ANOTHER_CREATE_UUID
```

Both `--name` and `--slug` must be supplied; their explicit empty values and
whitespace are preserved because the API accepts them. Name and slug accept
at most 200 and 120 Unicode characters. All text must be valid UTF-8 without
NUL. Description is optional: omission sends no member, while an explicit empty
flag sends an empty string. The complete encoded JSON request is capped at
8 KiB; description has no additional character limit in the backend.

Creation requires system-scoped Project Manager authority. Access Administrator
or project-scoped Project Manager authority alone cannot create a project.
The CLI sends one public POST with the unchanged caller bearer and UUID key;
there is no role preflight, generated key or automatic retry. Only HTTP 201
with the full seven-field ProjectResponse establishes success. Nullable
description is valid, but missing members, malformed identity/timestamps,
unknown or duplicate fields and the minimal contributor GET shape are rejected.
JSON preserves the API response; text shows every field with terminal escaping.

This creates a draft shell, not an uploaded or approved guide, activated project
or claimable task. Project identity is generated by Workstream, not the key.
Retain the key with the exact name, slug and description omission/value.
An exact manual replay recovers the stored project, whose state may since have
changed. Changed fields conflict; a different key with the same slug conflicts
rather than overwriting a project.

Unlike task claim/start, the current project-create API recovers a committed
result before fresh creation authorization, including after the creation grant
is revoked. This recovery is not permission for another creation. Do not assume
every mutation has task replay semantics or that a replay certifies current
authority. Fresh creation is denied after revocation or suspension.

Lost, malformed, oversized, redirected, unexpected-status, server-error and
noncanonical error replies leave an unknown outcome: nonzero exit, empty stdout
and `error.outcome_unknown: true`. Only a complete canonical Workstream 4xx
establishes a known denial. If manually retrying, use the unchanged fields and
key; do not invent a project ID, assume rollback or switch to a fresh key.

## Browse tasks as a project manager

```sh
Expand Down
34 changes: 30 additions & 4 deletions cli/internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,10 @@ func (c *Client) safeMetadata(value string) bool {
}

func (c *Client) request(ctx context.Context, method, path, query string, body []byte) (json.RawMessage, error) {
return c.requestWithKey(ctx, method, path, query, body, "")
return c.requestWithKey(ctx, method, path, query, body, "", http.StatusOK)
}

func (c *Client) requestWithKey(ctx context.Context, method, path, query string, body []byte, key string) (json.RawMessage, error) {
func (c *Client) requestWithKey(ctx context.Context, method, path, query string, body []byte, key string, successStatus int) (json.RawMessage, error) {
mutation := method == http.MethodPatch || method == http.MethodPost
target := c.origin + path
if query != "" {
Expand Down Expand Up @@ -353,7 +353,7 @@ func (c *Client) requestWithKey(ctx context.Context, method, path, query string,
correlation = ""
}
}
if response.StatusCode != http.StatusOK {
if response.StatusCode != successStatus {
code := "api_error"
knownEnvelope := false
if response.StatusCode >= 300 && response.StatusCode < 400 {
Expand All @@ -370,7 +370,7 @@ func (c *Client) requestWithKey(ctx context.Context, method, path, query string,
}
}
if method == http.MethodPost {
knownEnvelope = canonicalTaskError(responseBody, response.Header.Get("Content-Type"))
knownEnvelope = canonicalMutationError(responseBody, response.Header.Get("Content-Type"))
}
// A complete API 4xx envelope is a known denial, independent of code
// spelling/redaction. A gateway reply without it cannot prove rollback.
Expand All @@ -383,3 +383,29 @@ func (c *Client) requestWithKey(ctx context.Context, method, path, query string,
}
return json.RawMessage(responseBody), nil
}

// POST writes need a complete public ApiError, not a gateway's code-shaped
// JSON. Profile PATCH deliberately retains its existing response contract.
func canonicalMutationError(raw []byte, contentType string) bool {
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil || mediaType != "application/json" {
return false
}
var envelope struct {
Error json.RawMessage `json:"error"`
Detail json.RawMessage `json:"detail"`
}
if decode(raw, &envelope, []string{"error"}, nil) != nil {
return false
}
var value struct {
Code *string `json:"code"`
Message *string `json:"message"`
Details map[string]json.RawMessage `json:"details"`
CorrelationID *string `json:"correlation_id"`
Retryable *bool `json:"retryable"`
}
return decode(envelope.Error, &value, []string{"code", "message", "details", "correlation_id", "retryable"}, nil) == nil &&
value.Code != nil && *value.Code != "" && value.Message != nil && value.Details != nil &&
value.CorrelationID != nil && validUUID(*value.CorrelationID) && value.Retryable != nil
}
Loading
Loading