Skip to content

feat(cli): resolve acting org from local git remotes - #55

Merged
theCodeDrift merged 5 commits into
mainfrom
jakob/org-uuid
Jul 10, 2026
Merged

theCodeDrift merged 5 commits into
mainfrom
jakob/org-uuid

Conversation

@theCodeDrift

@theCodeDrift theCodeDrift commented Jul 7, 2026 •

Copy link
Copy Markdown
Member

Gives the CLI a correct, per-repository sense of which Taskless organization it is acting as, and sends that identity on write calls — the 0.10.x org-identity contract on the client side.

Why. A Taskless user can belong to multiple orgs, but the CLI previously only knew the org from the token's numeric orgId claim, so a multi-org user was routed to whichever org their token happened to pin. The agreed design has whoami return every org with a canonical owner url, and makes it the client's job to pick the acting org by matching that url against the repo's remotes; the server authorizes the chosen org per request.

What's here

Resolution layer.

  • canonicalOwnerUrl reduces a git remote to its canonical owner url (https://github.com/{owner}). It's a verbatim port of the server's shared @taskless/shared/github implementation, so the two compare by exact string equality across SSH, ssh:///git://, port, www., .git, and mixed-case forms.
  • listRemoteOwnerUrls enumerates git remotes in origin → upstream → rest precedence (keeping every url of a multi-url remote), canonicalizes + dedupes, and drops non-GitHub owners.
  • selectOrgForOwners / resolveCurrentOrg / resolveOrgSubject pick the acting org: the first owner url that matches a whoami org wins; the org's Taskless UUID is the subject, falling back to the token's deprecated numeric claim when nothing matches.

Send side.

  • resolveIdentity now returns orgSubject (string | number); submitRule / iterateRule / reconcile send it as orgId (widened to string | number), and check sends it on reconcile.
  • The server returns the same 404 organization_not_found for both "not your org" and "install doesn't cover this repo" (deliberately, to not leak org existence), so the message now names the coverage cause first, not just re-auth.
  • Handles two new generation states: classifying (transient) and unsupported (terminal — the org's plan lacks the requested capability, e.g. runtime rules), surfaced with the new RULE_UNSUPPORTED code and documented in the create/improve recipes.

Types. Regenerated src/generated/api.d.ts from the live schema (which now carries the contract) and derived WhoamiOrg from it rather than hand-typing; email is now optional.

Compatibility

Additive. The token keeps its numeric orgId claim, so single-org behavior is unchanged and older clients are unaffected. End-to-end multi-org routing depends on the server-side stack (taskless#82–#85) reaching production; the whoami + reconcile pieces are already live.

Refs TSKL-245

@theCodeDrift theCodeDrift added skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) and removed skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) labels Jul 7, 2026
@theCodeDrift
theCodeDrift requested a review from Copilot July 9, 2026 03:38

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.

Pull request overview

Introduces a client-side “acting org” resolution layer for the CLI by matching the current repo’s git remotes to whoami orgs’ canonical owner URLs, and then uses that resolved org subject (UUID preferred, numeric claim fallback) on write-ish calls.

Changes:

  • Add GitHub-remote → canonical owner URL normalization + remote enumeration with origin/upstream precedence, used to select the acting org from whoami.
  • Update identity/write flows (rule create/improve and reconcile in check) to send orgSubject and handle new server statuses/errors (classifying, unsupported, RULE_UNSUPPORTED, improved org-not-found messaging).
  • Add/expand Vitest coverage for org selection and remote canonicalization; update generated API types for whoami and rule status.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/cli/test/org.test.ts Adds tests for selecting an acting org and resolving the org subject fallback chain.
packages/cli/test/git-remote.test.ts Adds tests for canonical owner URL normalization and remote owner URL ordering/deduping.
packages/cli/src/util/git-remote.ts Implements canonicalOwnerUrl + remote owner enumeration (origin→upstream→rest).
packages/cli/src/types/errors.ts Adds RULE_UNSUPPORTED error code.
packages/cli/src/help/rule-improve.txt Documents RULE_UNSUPPORTED in JSON error output.
packages/cli/src/help/rule-create.txt Documents RULE_UNSUPPORTED in JSON error output.
packages/cli/src/generated/api.d.ts Updates OpenAPI-derived types (whoami org UUID/url/source; new rule statuses; reconcile endpoint).
packages/cli/src/commands/rules.ts Sends identity.orgSubject; handles classifying + unsupported terminal state.
packages/cli/src/commands/info.ts Aligns local type with whoami.email now being optional.
packages/cli/src/commands/check.ts Resolves and sends org subject on reconcile requests.
packages/cli/src/auth/org.ts Adds org selection + org-subject resolution (whoami match, claim fallback).
packages/cli/src/auth/identity.ts Replaces orgId with orgSubject in resolved identity.
packages/cli/src/api/rules.ts Accepts `orgId: string
packages/cli/src/api/reconcile.ts Updates reconcile request typing/docs to accept optional org subject.
.changeset/org-subject-send.md Adds a changeset describing org-subject sending + new statuses/errors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/util/git-remote.ts
Comment thread .changeset/org-subject-send.md
theCodeDrift added a commit that referenced this pull request Jul 9, 2026
listRemoteOwnerUrls used find() to pull origin/upstream, taking only the
first remote.<name>.url. Git allows several urls per remote (e.g. `git
remote set-url --add`), so a second origin/upstream url was dropped and
could miss a matching org owner. Collect all urls per precedence remote
with flatMap; dedup already handles repeats.

Reported by Copilot review on #55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@theCodeDrift
theCodeDrift marked this pull request as ready for review July 9, 2026 05:13
theCodeDrift and others added 5 commits July 9, 2026 23:42
Add the client-side org identity layer for the 0.10.x whoami contract:
the CLI now maps the repo's local git remotes to a Taskless org UUID
instead of relying on the token's numeric orgId claim.

- canonicalOwnerUrl reduces any GitHub remote (SSH/HTTPS, .git, www.,
  userinfo, mixed case, trailing slash) to the canonical owner url
  (https://github.com/{owner}), matching the server's orgs[].url
  normalization exactly.
- listRemoteOwnerUrls enumerates git remotes, orders them
  origin -> upstream -> rest, canonicalizes + dedupes, and skips
  non-GitHub remotes. Returns [] when git is unavailable or the repo has
  no remotes, degrading cleanly to no current-org context.
- selectOrgForOwners / resolveCurrentOrg pick the acting org: the first
  owner url that matches a github-sourced whoami org wins.

This is the resolution layer only. Sending the org id UUID as the
subject on write calls is deferred until the API team finalizes the wire
field. The token keeps carrying the numeric orgId claim, so the change
is additive and older clients are unaffected.

Refs TSKL-245
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client canonicalizer was two hardcoded github.com regexes; the server
builds whoami's per-org url from a URL-parse-based, host-agnostic
canonicalOwnerUrl in @taskless/shared/github. The two are matched with
string equality, so any divergence silently drops an org match.

The regex version threw on ssh://, git://, and port-bearing github
remotes the server accepts and reduces normally. That's a graceful miss
(no subject sent, server falls back to the deprecated token claim) rather
than a misroute, but such a repo loses the new multi-org routing.

Port the shared algorithm verbatim so the two sides are byte-for-byte
identical by construction: accept bare login / repo URL / scp- or
URL-form SSH, default the host to github.com, strip www./userinfo/port/
.git/trailing slash, lowercase host + owner. It no longer throws; the
github.com-only filter now lives in listRemoteOwnerUrls, which drops
non-github owners (they can't match a github-sourced org anyway).

Adds parity fixtures for the previously-throwing forms and a
listRemoteOwnerUrls regression test for an ssh:// remote.

Refs TSKL-245
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live OpenAPI schema now carries the 0.10.x org-identity contract, so
regenerate src/generated/api.d.ts from it (openapi-typescript, prettier
formatted) and drop the hand-typed WhoamiOrg in favour of the generated
type — per the styleguide, generated types over custom definitions.

Schema changes now reflected:
- whoami orgs gain `id` (Taskless UUID), `source` ("github"), and the
  canonical owner `url`; `email` is now optional.
- /cli/api/reconcile is documented and its body carries the org subject:
  `orgId?: string | number` — the Taskless UUID (preferred) or numeric
  GitHub org id, falling back to the deprecated token claim. This is the
  wire field the send-side will populate.
- /cli/api/rule-hash-vectors is documented; rule status gains
  `classifying` and `unsupported`.

WhoamiOrg is now `paths[...whoami...]["orgs"][number]`. `email` becoming
optional flows through to info.ts (local auth type) and the org.ts test
helper casts `source` since the schema pins it to the literal "github".

Refs TSKL-245
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the org-identity resolution built earlier into the write paths. The
CLI now resolves the acting org's Taskless UUID (via whoami + the repo's
git remotes) and sends it as the `orgId` subject on rule create, rule
iterate, and reconcile. When no remote matches a known org it falls back
to the token's deprecated numeric claim, so single-org behaviour is
unchanged; multi-org users are no longer misrouted to a pinned org.

- resolveOrgSubject(cwd, token): whoami-matched UUID, else the numeric
  claim, else undefined. resolveIdentity now returns `orgSubject`
  (string | number) instead of a numeric `orgId`.
- submitRule / iterateRule / reconcile request types widen `orgId` to
  `string | number`; check.ts resolves and sends the subject too.
- The server returns the same 404 `organization_not_found` for both
  "not your org" and "install doesn't cover this repo", so the message
  now names the coverage cause first, not just re-auth.
- Handle two new generation states: `classifying` (transient, keep
  polling) and `unsupported` (terminal — the org's plan lacks the
  requested capability, e.g. runtime rules), surfaced with the new
  RULE_UNSUPPORTED code and documented in the create/improve recipes.

Refs TSKL-245
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
listRemoteOwnerUrls used find() to pull origin/upstream, taking only the
first remote.<name>.url. Git allows several urls per remote (e.g. `git
remote set-url --add`), so a second origin/upstream url was dropped and
could miss a matching org owner. Collect all urls per precedence remote
with flatMap; dedup already handles repeats.

Reported by Copilot review on #55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@theCodeDrift
theCodeDrift merged commit a3b702f into main Jul 10, 2026
4 checks passed
@theCodeDrift
theCodeDrift deleted the jakob/org-uuid branch July 10, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants