Repository navigation
feat(cli): resolve acting org from local git remotes - #55
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
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 sendorgSubjectand 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
whoamiand 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.
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
marked this pull request as ready for review
July 9, 2026 05:13
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
force-pushed
the
jakob/org-uuid
branch
from
July 10, 2026 06:42
1767e82 to
453efcf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
orgIdclaim, so a multi-org user was routed to whichever org their token happened to pin. The agreed design haswhoamireturn every org with a canonical ownerurl, 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.
canonicalOwnerUrlreduces a git remote to its canonical owner url (https://github.com/{owner}). It's a verbatim port of the server's shared@taskless/shared/githubimplementation, so the two compare by exact string equality across SSH,ssh:///git://, port,www.,.git, and mixed-case forms.listRemoteOwnerUrlsenumerates git remotes inorigin → upstream → restprecedence (keeping every url of a multi-url remote), canonicalizes + dedupes, and drops non-GitHub owners.selectOrgForOwners/resolveCurrentOrg/resolveOrgSubjectpick the acting org: the first owner url that matches awhoamiorg wins; the org's Taskless UUID is the subject, falling back to the token's deprecated numeric claim when nothing matches.Send side.
resolveIdentitynow returnsorgSubject(string | number);submitRule/iterateRule/reconcilesend it asorgId(widened tostring | number), andchecksends it on reconcile.404 organization_not_foundfor 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.classifying(transient) andunsupported(terminal — the org's plan lacks the requested capability, e.g. runtime rules), surfaced with the newRULE_UNSUPPORTEDcode and documented in the create/improve recipes.Types. Regenerated
src/generated/api.d.tsfrom the live schema (which now carries the contract) and derivedWhoamiOrgfrom it rather than hand-typing;emailis now optional.Compatibility
Additive. The token keeps its numeric
orgIdclaim, 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