flag-set-role
Flips one role-segment's enablement on a runtime feature flag in Flagsmith, or manages per-org segment membership. The skill is the only approved path for mutating Flagsmith segment overrides and per-feature org-segment rules — direct UI/curl edits break the fallback-fidelity contract documented in ADR-038 v2 §"Fallback semantics".
Per-org targeting uses a per-feature segment <flag>-orgs (ADR-043 §"Per-feature segment scoping", 2026-05-29) — each org-targetable flag gets its own segment, so the org set for one flag is independent of another's. (The legacy shared org-targeted segment is retained until team-workspace-invite is migrated off it; new per-org scoping uses <flag>-orgs.)
When to use
- Promoting a feature from dev cohort to everyone:
... <flag> prd on. - Disabling a feature for everyone:
... <flag> prd off. - Enabling a feature for dev cohort only:
... <flag> dev on. - Adding an org to the per-org segment:
... <flag> prd on --org <orgId>. - Removing an org from the per-org segment:
... <flag> prd off --org <orgId>. - Migrating a feature off the legacy shared
org-targetedsegment onto its own<flag>-orgssegment:... <flag> prd on --detach-shared --org <memberId>.
When NOT to use
- Creating a brand-new flag → use
soleur:flag-create. - Promoting a user to dev → use
soleur:user-set-role.
Arguments
<arguments> #$ARGUMENTS </arguments>
Required positional args: <flag-name> <role> <on|off>.
<flag-name>: must be a key inapps/web-platform/lib/feature-flags/server.tsRUNTIME_FLAGS(and in the script'sFLAG_ENV_VARSmap).<role>:prdordev.<on|off>: target enablement.
Flag --org <orgId> switches to per-org targeting mode. When provided, the script provisions the feature's own <flag>-orgs segment (creates it + ensures an ON feature-state override in both envs) and adds/removes an EQUAL orgId <uuid> condition in its ANY rule, instead of flipping a role-segment override. The orgId must be a valid UUID. Flag --control-org <orgId> (per-org mode only) sets the control org for the eval-layer re-verify (the org asserted to be NOT enabled — proves no leak). Defaults to a synthetic non-member UUID; pass a real sibling org (e.g. one sharing the legacy org-targeted segment) for a stronger leak check. Flag --detach-shared (migration verb; requires --org <memberId> and value on) removes the feature's override on the legacy shared org-targeted segment in BOTH envs by publishing a version with segment_ids_to_delete_overrides:[<org-targeted id>] (resolved by name, never hard-coded), then eval-verifies the feature STILL resolves enabled=true for the member org (served by its own <flag>-orgs segment now) and enabled=false for the control org. Provision <flag>-orgs via the --org path FIRST — detach removes the shared override, it does not create the per-feature one. Idempotent: a no-op for any env with no override, and a clean no-op if org-targeted is already gone. Flag --dry-run runs detect/diff/validate steps (no writes). Flag --confirmed skips the interactive read -p prompt (for agent-driven use; the agent must obtain operator ack via AskUserQuestion before passing this flag).
Prerequisites
dopplerCLI authenticated with access tosoleurproject, configsdev,prd,cli_ops.curlon PATH.python3on PATH (used for JSON parsing).
Procedure
Invoke the script:
# Dry-run to see the matrix:
bash "${CLAUDE_PLUGIN_ROOT}/skills/flag-set-role/scripts/flip.sh" <flag> <role> <on|off> --dry-run
# After AskUserQuestion confirmation:
bash "${CLAUDE_PLUGIN_ROOT}/skills/flag-set-role/scripts/flip.sh" <flag> <role> <on|off> --confirmed
Agent-driven flow (recommended):
- Run with
--dry-runto get the pre/post matrix (exits 0, no writes, no prompt). - Present the matrix output to the operator via AskUserQuestion with options: "Yes, apply" / "Cancel".
- On confirmation, re-run with
--confirmed(skips theread -pprompt, proceeds to write). - On cancel, abort.
Important: The agent must always pass --dry-run (for preview) or --confirmed (for apply) — never invoke without one of these flags, or the interactive prompt will hang the agent shell.
The script (full procedure in [scripts/flip.sh](./scripts/flip.sh)):
Role targeting (default):
- Validate args. Flag must be in the known-set; role in
{prd,dev}; value in{on,off}. - Resolve IDs. Look up
feature_idfrom Flagsmith project39082by feature name (cached per session). Look upsegment_idby name (role-prdorrole-dev). - Read current state. For each env (dev
90722, prd90721), fetch the live version's feature-states + per-segment override. - Apply fallback-fidelity rule. If proposed =
dev offAND currentprd onin either env, abort with exit 1 + clear message. (See ADR-038 v2 §"Fallback semantics" — the env-var fallback can only mirror one state;dev off / prd oncannot be represented and would silently re-enable the dev cohort on outage.) - Print pre/post matrix. Show current (env × segment) enablement table and the proposed delta.
- Operator ack. If
--confirmedis passed, skip (the agent already obtained ack via AskUserQuestion). Otherwise, wait for literalyesat the terminal prompt (perhr-menu-option-ack-not-prod-write-auth). Anything else aborts. - Flip Flagsmith. For each env, POST to
/api/v1/environments/{env_id}/features/{feature_id}/versions/withfeature_states_to_create(first-time) ORfeature_states_to_update(subsequent),publish_immediately: true. - Mirror Doppler (only on
role=prdflip). Rundoppler secrets set FLAG_<X>=<0|1> -p soleur -c devAND-c prdvia stdin-piped 0600 temp file (no CLI-arg leak). - Re-verify. Re-fetch state in both envs and assert matches proposed.
Org targeting (when --org <orgId> is provided):
- Validate args. Same as role targeting, plus UUID format validation on
--organd--control-org(and they must differ). - Read current membership. Resolve the feature's own
<flag>-orgssegment (may not exist yet → empty membership) and extract its orgIds from therules[0].rules[0].conditions[]array (each condition isEQUAL orgId <uuid>inside anANYrule). - Compute new membership + display. Add (on) or remove (off) the target org. Print current membership, proposed action, control org, and the new membership. No "already present" early-exit — the override may still be missing, so provisioning + eval-verify always run.
- Dry-run / operator ack.
--dry-runprints the plan and exits 0 with no writes. Otherwise wait foryes(or--confirmed). - Audit trail. WORM audit entry with
target: org:<orgId>BEFORE any Flagsmith mutation (append-before-flip). - Provision
<flag>-orgs. Idempotently create the segment (ALL→ANY/EQUAL-orgId envelope) and ensure an ON feature-state override for the flag in BOTH envs (provision_feature_segment). - Write membership. Re-read the segment immediately before the PUT (shrinks the read-modify-write window), then PUT the conditions array rebuilt from the fresh read (one
EQUALcondition per org). - Eval-layer re-verify (the load-bearing check). Evaluate the flag for a transient identity carrying the
orgIdtrait (the productiongetIdentityFlags("org:<orgId>:<role>", {role, orgId}, transient)path, againstedge.api.flagsmith.com): assert the target org resolvesenabled == (on)AND the control org resolvesenabled == false. Membership-set equality alone is NOT sufficient — a missing override, or an override present in only one env, leaves the flag OFF while the org is "in" the segment. Eval propagation is polled (eventual, completes within seconds).
No Doppler mirror runs for org-targeting (segment membership is not reflected in env vars per ADR-038/ADR-043 — a per-org-only flag falls back OFF on a