Skip to content

feat(people): add people clients add/remove/invite/enable/disable - #689

Merged
jeremy merged 6 commits into
mainfrom
feat/project-clients
Sep 9, 2026
Merged

feat(people): add people clients add/remove/invite/enable/disable#689
jeremy merged 6 commits into
mainfrom
feat/project-clients

Conversation

@jeremy

@jeremy jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member

What

Surfaces the client-admission API basecamp/bc3#13098 added — the client-side
counterpart of people add/people remove — as a people clients group:

basecamp people clients enable  --in <project>
basecamp people clients list    --in <project>
basecamp people clients add     <id|email|name>... --in <project>
basecamp people clients invite  <invitee>...       --in <project> [--company NAME] [--title TITLE]
basecamp people clients invite  -                  --in <project>   # one invitee per line on stdin
basecamp people clients remove  <id|email|name>... --in <project>
basecamp people clients disable --in <project>
  • add / remove grant and revoke existing client users through
    PUT /projects/:id/people/client_users.json, resolving ids, emails, or names
    the way people add does. The endpoint silently drops an ineligible id (a
    team member's, or someone already on the project), so the ids it did not
    echo back in granted/revoked are reported as a diagnostic notice rather
    than lost.
  • invite creates new clients by email through the same endpoint's
    create list. An invitee is a bare address or "Name <email>" (RFC 5322
    mailbox form via net/mail, so a quoted display name works); - alone reads
    one invitee per line from stdin. --company applies to every invitee;
    --title names one person's role and so takes exactly one invitee.
  • enable / disable drive POST/DELETE /projects/:id/client_enablement.json.
  • list is the project roster narrowed to client: true; people list
    now also carries each person's client flag.

A separate group rather than a --client flag on people add: bc3 keeps the
two kinds of access apart on the wire (the team endpoint drops a client's id,
the client endpoint rejects a team member's — neither cross-grades), and the
CLI mirrors that split instead of hiding it behind a mode flag. The verbs
honor the configured default project (.basecamp/config.json) as SKILL.md
promises; people add/remove still read only the flag, unchanged here.

Error mapping

The endpoint's refusals become verdicts, structured in --json/--agent
output:

Server CLI
403 (clients off, or no permission — same empty body) forbidden (4). The project is read back once: with clients_enabled: false the message says so and the hint is basecamp people clients enable --in <project>; otherwise it is a permission message.
422 {errors:[{email_address, messages}]} validation (9), each rejected row named in the hint (the SDK branch folds the row-keyed body into FieldErrors by address); nobody was invited.
429 (new addresses exceed the account's user limit) limit_exceeded (10), retryable: false. This 429 carries no Retry-After and waiting cannot change it, so it is not reported as throttling; a 429 that does name a Retry-After still converts as a rate limit. The SDK branch declares the operation retry_on: [503], so it surfaces on the first attempt.
403 on disable while clients remain forbidden, with the roster read back so the hint is the exact remove command.

Dependency — pinned to the SDK release

Depends on basecamp/basecamp-sdk#847, which shipped in
basecamp-sdk v0.17.0.
go.mod pins github.com/basecamp/basecamp-sdk/go v0.17.0 (tag go/v0.17.0,
6e73a06f4262) via make bump-sdk REF=v0.17.0, the mechanism #677 used; the
vendored MCP model is re-synced from that tag (scripts/sync-mcp-model.sh,
PROVENANCE.json ref go/v0.17.0) and the Nix vendorHash recomputed and
build-verified (make update-nix-hash). No replace directive.

The branch is rebased onto main (#681, #688, #686). The only conflicts were
the pin-generated files — go.mod, go.sum, PROVENANCE.json,
sdk-provenance.json, nix/package.nix — resolved by taking main's side and
re-running the flip; .surface regenerated to main + the people clients
additions (no removals against main).

Tests

  • people_clients_test.go: request shapes for grant/revoke/invite (resolved
    ids, the create rows, --company/--title placement, name omitted when
    blank), stdin invitees, the unaffected-id notice, and each error mapping
    (403 both ways, per-row 422, bare 429 vs Retry-After 429, disable while
    clients remain).
  • e2e/people.bats: offline usage errors (missing project, non-address
    invitee, - with an empty pipe, - mixed with other invitees, --title
    with several invitees).
  • Catalog, .surface, SKILL.md, API-COVERAGE, and the smoke-coverage entries
    updated. Every bin/ci target run locally: fmt, vet, lint, lint-actions,
    e2e (458 bats), naming, surface, skill drift, bare groups, lint lockstep,
    smoke coverage, SDK provenance, and go mod tidy green; the MCP catalog
    (provenance, snapshot, served count 256 → 259) and the Nix flake
    (vendorHash recomputed and build-verified) too. The Go unit run is green
    except TestBareBasecampNeverReportsASetupError,
    TestExplicitSetupStillRefuses,
    TestDeleteConfirmableFollowsTheAudienceNotTheDevice and the
    TestIsInteractive* cases in appctx/cli, which fail identically on
    pristine main in this non-TTY shell (as feat(bubble-up): add bubble-up add/remove commands #677 noted).

I'm babysitting the review loop on this PR through to convergence.

Release note: Features (enhancement).

Copilot AI balanced review requested due to automatic review settings September 9, 2026 00:29
@github-actions github-actions Bot added commands CLI command implementations sdk SDK wrapper and provenance tests Tests (unit and e2e) skills Agent skills docs deps labels Sep 9, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T22:20:32.438513Z 09de5e5 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The seat-limit response incorrectly activates the global rate-limit gate, and the required released SDK repin remains outstanding.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds project client management under people clients, backed by new SDK operations and integrated across CLI documentation, tests, MCP metadata, and API coverage.

Changes:

  • Adds client listing, invitation, membership, and enablement commands.
  • Maps client-specific validation, permission, and seat-limit errors.
  • Updates SDK provenance, command surfaces, documentation, and tests.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File summaries
File Description
skills/basecamp/SKILL.md Documents client-management commands.
nix/package.nix Refreshes the SDK vendor hash.
internal/version/sdk-provenance.json Records the pinned SDK revision.
internal/mcpserver/testdata/catalog_snapshot.txt Adds three MCP actions.
internal/mcpserver/model/PROVENANCE.json Updates model provenance.
internal/mcpserver/model/openapi.json Adds client API schemas and operations.
internal/mcpserver/model/behavior-model.json Adds retry and redaction metadata.
internal/mcpserver/catalog_test.go Updates the operation count.
internal/commands/people.go Implements the client command group.
internal/commands/people_clients_test.go Tests client command behavior.
internal/commands/commands.go Registers the client action group.
go.sum Updates SDK checksums.
go.mod Pins the SDK feature revision.
e2e/smoke/smoke_projects.bats Smoke-tests client listing.
e2e/smoke/smoke_lifecycle.bats Classifies mutating commands as out of scope.
e2e/people.bats Tests offline usage validation.
API-COVERAGE.md Records three newly covered endpoints.
.surface Updates the CLI surface snapshot.
Review details
  • Files reviewed: 17/18 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread go.mod Outdated
Comment thread internal/commands/people.go
Comment thread internal/commands/people.go Outdated
Comment thread internal/commands/people.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e9d040858

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/commands/people.go Outdated
Comment thread internal/commands/people.go
Comment thread internal/commands/people.go Outdated
Copilot AI review requested due to automatic review settings September 9, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several commands accept ignored positional arguments, the disable hint is not executable, and invitation error documentation conflicts with actual behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

Previously missed (3) — in code that hasn't changed since the last review.

internal/commands/people.go:948

  • This no-argument command does not install an Args validator, so Cobra accepts and silently ignores positional values. With a configured default project, people clients list 123 therefore lists the default project instead of rejecting the unsupported positional project, which can mislead callers. Add Args: cobra.NoArgs.

This issue also appears in the following locations of the same file:

  • line 1433
  • line 1460
    internal/commands/people.go:1289
  • The command rejects malformed mailbox input locally as a usage error in parseClientInvitees; only rows accepted by that parser and subsequently rejected by Basecamp produce validation (9). This help text currently promises the wrong exit contract for inputs such as "Annie Bryan", which the new e2e test explicitly expects to be usage.
    skills/basecamp/SKILL.md:1267
  • This repeats an incorrect exit-code contract: malformed addresses are rejected by parseClientInvitees as usage, while validation (9) is reserved for a server-side 422 after local parsing succeeds. Distinguish those cases so agents do not branch on the wrong verdict.

internal/commands/people.go:1433

  • This no-argument mutating command silently accepts positional values because Args is unset. For example, people clients enable 123 can ignore 123 and enable clients on the configured default project instead. Reject extra arguments with cobra.NoArgs before performing the mutation.
		RunE: func(cmd *cobra.Command, args []string) error {

internal/commands/people.go:1460

  • This no-argument destructive command silently accepts positional values because Args is unset. For example, people clients disable 123 can ignore 123 and disable clients on the configured default project instead. Reject extra arguments with cobra.NoArgs before performing the mutation.
		RunE: func(cmd *cobra.Command, args []string) error {
  • Files reviewed: 19/20 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread internal/commands/people.go Outdated
Copilot AI review requested due to automatic review settings September 9, 2026 00:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Documentation and test gaps remain, and the SDK branch pin must be replaced by a released tag before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

skills/basecamp/SKILL.md:1267

  • The skill currently tells agents that malformed addresses produce validation, while the command deliberately rejects malformed mailbox syntax locally as usage; only a server-side 422 maps to validation. Documenting that distinction prevents agents from relying on the wrong exit code and multi-row hint.
    skills/basecamp/SKILL.md:1269
  • This parenthetical describes the add omission reason for both verbs. For remove, the implementation correctly reports an ID as unchanged when it is not on the project, so the skill should preserve that distinction.
  • Files reviewed: 19/20 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread internal/commands/people.go
Comment thread internal/commands/people.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7210528d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/commands/people.go Outdated
Copilot AI review requested due to automatic review settings September 9, 2026 00:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The required SDK release is still open and blocked, so the pseudo-version must be replaced with the final tagged release before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 19/20 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

The client-admission operations (UpdateProjectClientAccess,
EnableProjectClients, DisableProjectClients) live on basecamp-sdk's
feat/project-client-users branch (basecamp/basecamp-sdk#847) ahead of a
release, so go.mod pins its head as a pseudo-version, the way the
bubble-up pin did. Re-pin to the tagged go/vX.Y.Z release once it is
cut, and re-sync the vendored MCP model and the Nix vendorHash from it.

The vendored MCP model is synced from that head: three People operations
join the catalog (served count 256 -> 259) and the seat-limit 429 on
UpdateProjectClientAccess is declared non-retryable (retry_on [503]).
The client-side counterpart of people add/remove, over the endpoints
bc3#13098 shipped: PUT /projects/:id/people/client_users.json and
POST/DELETE /projects/:id/client_enablement.json.

A separate group rather than a --client flag: bc3 keeps the two kinds of
access apart on the wire (the team endpoint drops a client's id, the
client endpoint rejects a team member's), so the CLI mirrors the split.
The ids the server silently drops come back as a diagnostic notice.

invite takes a bare address or "Name <email>" (net/mail), or "-" for one
invitee per line on stdin; --company applies to every invitee, --title
to exactly one. Refusals map to verdicts: a 403 reads the project back
to name "clients not enabled" against a permission problem; the
all-or-nothing 422 exits validation naming each rejected row from the
SDK's field errors; the seat-limit 429 exits limit_exceeded, not a
retryable rate limit. disable's 403 reads the roster back so the hint
is the exact remove command.

people list now carries each person's client flag.
…3 honestly

The resilience hooks persisted a 60-second block for every headerless
429, so the client seat-limit verdict would have refused unrelated
commands for a minute. OnOperationStart now names the operation in the
context and OnRequestEnd consults the SDK's declared retry set: an
operation that does not retry on 429 (UpdateProjectClientAccess) is one
the server answers 429 as a verdict, and it sets no block. A 429 that
names a Retry-After is honored as before.

disable's 403 names the remaining clients only when the roster shows
some; otherwise it is a plain denial, since the same 403 answers a
caller without permission. The seat-limit message no longer states a
count: existing addresses take no seat and repeats count once, and only
the server knows which rows those are.
…ect-only verbs

The SDK branch head decodes the row-error selectors independently and
reports client access as project_access; the pin, provenance, vendored
MCP model, and Nix vendorHash follow it.

list, enable, and disable take no positional, so a stray one is refused
instead of discarded: with a default project configured, "disable 123"
would otherwise have acted on the configured project. The remove hint
disable offers separates ids with spaces so it pastes as a command.
A token that is not an address is refused before any request as a usage
error; the help and skill text had promised the server's validation
exit for it. The local check now names every malformed token in one
error instead of stopping at the first, and the docs distinguish the
local refusal (usage, 2) from the server's all-or-nothing rejection
(validation, 9) and seat shortfall (limit_exceeded, 10).

people list's client flag is pinned by a test.
Copilot AI review requested due to automatic review settings September 9, 2026 22:16
@jeremy
jeremy force-pushed the feat/project-clients branch from b2171c1 to 09de5e5 Compare September 9, 2026 22:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation matches the SDK contract and includes comprehensive unit, integration, surface, and documentation updates.

Review details
  • Files reviewed: 19/20 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@jeremy
jeremy merged commit 96822ba into main Sep 9, 2026
35 checks passed
@jeremy
jeremy deleted the feat/project-clients branch September 9, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands CLI command implementations deps docs sdk SDK wrapper and provenance skills Agent skills tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants