Skip to content

feat: report team attachment in setup, status, and doctor - #776

Open
IAM-marco wants to merge 6 commits into
mainfrom
feat/614-claim-nudges
Open

feat: report team attachment in setup, status, and doctor#776
IAM-marco wants to merge 6 commits into
mainfrom
feat/614-claim-nudges

Conversation

@IAM-marco

Copy link
Copy Markdown
Contributor

Summary

Closes #614. Part of #96.

  • setup, status, and doctor now say whether a project is attached to a team, so a developer meets the temporary nature of a fresh project without first knowing zitadel claim exists.
  • State comes from .zitadel/secret alone, which G1 (feat: add the zitadel claim command #754) already writes. There is no ambient claim-state endpoint to call: getClaimStatus answers for one live challenge and rejects a secret that did not mint it, and claim attributes were deliberately kept off GET /projects/{id} because they belong to the grant, not the project (breakdown §3.5). Reading locally is also what keeps status and doctor fast and working offline.
  • Advisory, never a failure. An unattached project works exactly like an attached one, so doctor warns and still exits 0. Failing would have broken every scripted zitadel doctor run against a project nobody had claimed yet.
  • Cloud only. Local and self-hosted projects have no team to attach to, and zitadel setup --server local is the documented dev loop, so an ungated nudge would advertise an impossible action for the life of every local project. The gate reads the server in zitadel.json, not the server a command is pointed at: doctor pins its source to the local runtime URL and status --server local overrides it.
  • Copy shows the team id, not a name: teams have no name column yet (Resource model - Teams #598), and the claim responses carry only team_id. It frames projects as temporary without promising deletion, since the 14-day lifetime is unenforced.

The ticket was written before G1 shipped and asked for an API call that does not exist and a team name that cannot be resolved; it has been updated to match, and #774 tracks the two follow-ups.

Two judgement calls worth a look

An existing test's expectation changed. status.test.ts asserted next_actions is empty when the platform is unreachable. The claim nudge legitimately fires there, because it is a local read, which is the point of reading locally. The assertion now distinguishes probe-dependent journey guidance (still withheld) from the claim nudge (still shown), rather than suppressing the nudge to keep the old line green.

The setup summary renders every row with a green , so the nudge reads as ✓ Ownership temporary until you attach it to a team. The value is dimmed. If that checkmark reads wrong, the alternative is dropping the row and keeping only the action line below the box.

Validation

moon run cli:test        # 1011 passed (109 files)
moon run cli:lint        # clean
moon run cli:typecheck   # clean (KNOWN-VACUOUS per moon.yml, so weak signal)

Smoke-tested by hand against the built CLI, covering every state:

  • detached cloud project: doctor warns and exits 0, status and the setup box carry the nudge
  • attached: [ok] claim: Project is attached to team team-abc, no nudge anywhere
  • project whose zitadel.json says http://localhost:8080: silent even when invoked with --server https://api.zitadel.cloud, confirming the gate reads the project's recorded server rather than the flag
  • doctor --fix on a detached project: warning persists, .zitadel/secret byte-identical

Notes

moon run cli:readme produces a ~130-line diff that is pure terminal-width reflow, unrelated to this change, so it is not included. Worth knowing that task is not width-stable.

Release notes / changeset

  • .changeset/cli-claim-nudges.md — minor bump for @zitadel/cli.

🤖 Generated with Claude Code

IAM-marco and others added 5 commits August 6, 2026 18:59
Adds the single place that answers "is this project attached to a team?",
so the surfaces that nudge toward `zitadel claim` cannot drift from each
other or from the command itself.

The answer comes from `.zitadel/secret` alone, which `zitadel claim`
already writes. There is no ambient claim-state endpoint to call:
`getClaimStatus` answers for one live challenge and rejects a secret that
did not mint it, and claim attributes were deliberately kept off
`GET /projects/{id}` because they belong to the grant rather than the
project. Reading locally is what keeps `status` and `doctor` fast and
working offline.

Claiming is a cloud concept, so the helper reports `not-applicable` for
local and self-hosted servers rather than nudging toward an action that
cannot happen there. The gate reads the `server` recorded in
`zitadel.json` via the new `readProjectServer`, not the server a command
happens to be pointed at: `doctor` pins its own source to the local
runtime URL and `status --server local` overrides it, so either would
answer the wrong question.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`setup` is where a developer first meets their project, so it is where
the temporary nature of it has to be visible. Adds an ownership row to
the summary box and the claim nudge to both the box actions and the
envelope's `next_actions`/`next_commands`, so humans and agents get the
same signal.

The nudge lands after the install and verify actions: attaching a team is
the step after the app demonstrably works, not a precondition for trying
it. A project this command just created is never attached yet, so the
only branch here is whether attaching applies to this server at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carries the attachment state on `data.project.claim` (`detached`, or
`attached` with `team_id` and `claimed_at`) and adds the nudge to the
guidance when no team is attached. The field is omitted entirely off the
cloud, where it would only invite agents to act on it.

`projectStatus` already read both `zitadel.json` and `.zitadel/secret`,
so this costs no extra I/O and no platform call.

The nudge is additive to the existing journey staging rather than a stage
of its own, because attaching a team is orthogonal to whether login works
yet. That also means it survives an unreachable platform: the journey
guidance is staged on the user-presence probe and is withheld when the
probe cannot answer, but the claim state is a local read, so going
offline no longer hides it. The test covering that case now says so
explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reports whether the project is attached to a team, and surfaces
`zitadel claim` in the advisory when it is not.

Always a warning, never a failure. An unattached project works exactly
like an attached one, same issuer, same users, same applications, so the
only thing missing is durability and that is the developer's call rather
than a defect. It matters mechanically too: `doctor` turns any `fail`
into a thrown `E_VALIDATION`, so failing here would break every scripted
`zitadel doctor` run against a project nobody had claimed yet.

Implements `SanityCheck` directly rather than extending the abstract
base, for the same reason `ManagedFilesCheck` does: the base's
verify-throws contract can only express pass or fail. That also means
nothing wraps a throw from `run`, so an unreadable secret or config is
caught and reported as a skip. Otherwise a missing `.zitadel/secret`
would crash the whole battery on a nudge and hide the `secret` check that
actually reports the problem, with its repair path.

`fix` stays the inherited no-op. `doctor --fix` calls it on every
non-passing check, but a claim needs a human to sign in through a
browser, so there is nothing safe to automate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the new envelope fields in the CLI's agent contract so an agent
can branch on attachment state, and adds the changeset for the release
notes.

Both stay on the vocabulary `zitadel claim` established. The CLI's
vocabulary gate bans "unclaimed" in README.md and SKILLS.md, and keeping
it out of the strings themselves is what makes that gate easy to keep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 17:00
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nextgen Ready Ready Preview Aug 6, 2026 5:01pm
nextgen-docs Ready Ready Preview Aug 6, 2026 5:01pm
nextgen-mock-zitadel Ready Ready Preview Aug 6, 2026 5:01pm

Request Review

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

Note

Copilot was unable to run its full agentic suite in this review.

Adds local “claim/ownership” awareness to the CLI so new cloud projects are clearly labeled as temporary until attached to a team, without requiring a platform call.

Changes:

  • Introduces claim-state helpers to compute claim status from .zitadel/secret and generate user-facing nudges.
  • Surfaces claim state and nudges in setup, status, and doctor (including a new ClaimCheck).
  • Adds/updates unit tests and docs to cover detached/attached/off-cloud behavior.

Reviewed changes

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

Show a summary per file
File Description
apps/cli/src/lib/claim-state.ts New claim state model + copy/nudge helpers
apps/cli/src/lib/project.ts Adds readProjectServer for “where the project lives” semantics
apps/cli/src/commands/status.ts Emits claim state + appends claim guidance/commands for detached cloud projects
apps/cli/src/commands/setup/index.ts Adds ownership summary row + claim nudges for new cloud projects
apps/cli/src/commands/doctor/index.ts Adds top-level claim guidance when claim check warns
apps/cli/src/commands/doctor/checks/index.ts Registers new ClaimCheck
apps/cli/src/commands/doctor/checks/claim.ts New check that warns on detached cloud projects using local secret/config
apps/cli/tests/unit/lib/claim-state.test.ts Covers claim-state classification and copy constraints
apps/cli/tests/unit/commands/status.test.ts Covers status claim field + nudges/commands behavior
apps/cli/tests/unit/commands/setup.test.ts Covers setup nudges on cloud vs silence off-cloud
apps/cli/tests/unit/commands/doctor/checks.test.ts Adds unit coverage for ClaimCheck behavior
apps/cli/tests/unit/commands/doctor.test.ts Ensures doctor warns (not fails) and suggests claim appropriately
apps/cli/SKILLS.md Documents new claim reporting behavior across commands
.changeset/cli-claim-nudges.md Releases feature as a minor change

Comment on lines +12 to +15
export type ClaimState =
| { kind: "attached"; team_id: string; claimed_at?: string }
| { kind: "detached" }
| { kind: "not-applicable" };
Comment on lines +47 to +49
if (claimed_at && team_id) {
return { kind: "attached", team_id, claimed_at };
}
@@ -0,0 +1,81 @@
import { claimState, type ClaimState } from "../../../lib/claim-state";
Comment on lines +26 to +34
let state;
try {
state = claimState({
secret: await readZitadelSecret(ctx.cwd),
// From `zitadel.json`, not the command's own source: `doctor` pins its
// source to the local runtime URL, so asking it where the project lives
// would report every project as local.
server: readProjectServer(await readZitadelConfig(ctx.cwd)),
});
Comment on lines +53 to +55
message:
"This project is temporary until you attach it to a team. Run `zitadel claim` " +
"to make it permanent; nothing about the project changes.",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping this one as-is, deliberately. Check messages in this battery quote the bare command: schema.ts:37 says "New projects scaffold editable defaults via zitadel setup". The publicCliCommand form belongs to next_actions/next_commands, which is exactly where this check's advisory already puts it via claimAction(cliVersion)/claimCommand(cliVersion) (doctor/index.ts:263-266), so a doctor run already prints the runnable npx @zitadel/cli@<version> claim right below the warning.

Inlining the versioned npx string into the check message too would duplicate it on every run and make the one-line warning considerably longer, without telling the reader anything the advisory does not.

Comment on lines +263 to +266
if (warnings.some((check) => check.name === "claim")) {
nextActions.push(claimAction(cliVersion));
nextCommands.push(claimCommand(cliVersion));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already handled: advisoryForWarnings ends with return { nextActions: unique(nextActions), nextCommands: unique(nextCommands) } (doctor/index.ts:279, helper at :490), so every branch including this one is de-duplicated before returning. That is also why the existing docker-cli and managed-runtime-processes branches push unconditionally. No change needed.

Comment thread apps/cli/SKILLS.md
Comment on lines +148 to +152
`setup`, `status`, and `doctor` report whether a team is attached, reading
`claimed_at`/`team_id` from `.zitadel/secret` (no platform call). `status`
carries `data.project.claim` as `{"kind": "detached"}` or
`{"kind": "attached", "team_id", "claimed_at"}`, and `doctor` reports a
`claim` check. A project with no team is only ever a **warning**, never a
Comment on lines +119 to +130
const json = parseJson(res.stdout) as {
data: {
project: { claim?: { kind: string; team_id?: string } };
next_actions: string[];
next_commands: string[];
};
};
expect(json.data.project.claim).toEqual({
kind: "attached",
team_id: "team-001",
claimed_at: "2026-01-02T00:00:00.000Z",
});
Review follow-ups on the nudges.

`ClaimState`'s `attached` variant made `claimed_at` optional even though
`claimState` only ever produces that variant when both `claimed_at` and
`team_id` are present, so the optionality described a state that cannot
occur. Making it required lets the type say what the constructor
guarantees, and the test that built an `attached` value without a
timestamp now has to supply one.

The doctor check declared `state` untyped, leaving it an evolving `any`
and its `ClaimState` import unused. It is annotated now.

The `attached` example in the agent contract was not valid JSON: it
listed the key names with no values, which is the one thing a document
aimed at agents should not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: 933f086

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@zitadel/cli Minor
@zitadel/testing Minor
@zitadel/server Minor
@zitadel/server-linux-x64 Minor
@zitadel/server-linux-arm64 Minor
@zitadel/server-darwin-x64 Minor
@zitadel/server-darwin-arm64 Minor
@zitadel/server-win32-x64 Minor
@zitadel/api Minor
@zitadel/config Minor
@zitadel/components Minor
@zitadel/sdk-core Minor
@zitadel/sdk-next Minor
@zitadel/sdk-nuxt Minor
@zitadel/sdk-react Minor
@zitadel/sdk-vue Minor
@zitadel/sdk-angular Minor
@zitadel/sdk-solid Minor
@zitadel/sdk-svelte Minor
@zitadel/sdk-qwik Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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.

Claim G2: CLI team-attachment nudges

2 participants