fix(users): filter removed members from tw users by default#245
fix(users): filter removed members from tw users by default#245lmjabreu wants to merge 2 commits into
tw users by default#245Conversation
The Twist v4 workspace_users/get endpoint returns both active and removed members, with a `removed: boolean` field that the CLI was ignoring. In workspace 1585 this meant `tw users` listed 554 entries when only 146 were actual members, and `resolveUserRefs` would happily match a removed user by name/email, leading to confusing HTTP 403 "User N is not part of workspace" errors from downstream mutations like `tw channel add`. Filter removed users in `getWorkspaceUsers` by default, and add an `--include-removed` flag on `tw users` for the rare audit case where you actually want to see them. When that flag is set, the human-readable output appends a red [removed] chip; `removed` is exposed in the curated JSON essentials so callers can distinguish the two states. Follow-up to side-discovery #3 in #244. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
doistbot
left a comment
There was a problem hiding this comment.
This PR thoughtfully addresses a significant issue with reference resolution by ensuring tw users filters out removed members by default, while introducing an --include-removed flag to access tombstoned users when needed. These changes greatly improve the reliability of workspace user management and correctly utilize the v4 API schema. A few minor adjustments are needed to document the new flag in the AI agent skill content, realign a misplaced test block, and strengthen the test suite to properly verify both the default filtering behavior and the updated JSON payload.
scottlovegrove
left a comment
There was a problem hiding this comment.
I'm wondering whether this should be the default of the SDK rather than this. The MCP probably has the same issue :/
I think I would prefer this to be the default of the SDK
## Summary - `getWorkspaceUsers` now excludes users with `removed: true` by default; pass `includeRemoved: true` to include them. - The Twist API endpoint `/workspace_users/get` (server: `workspace_users.py` `get(id)`) calls `workspaces.get_users(ws.id)` without the model's `with_removed` arg, so it's hardcoded to `with_removed=True` and exposes no override param. Filtering therefore happens **client-side** in the SDK — matching what twist-web (Redux selectors) and twist-cli already do. - Applied to **both** execution paths: the awaited call filters in `.then()`; the batch path uses a new optional `transform?: (data: unknown) => T` hook on `BatchRequestDescriptor`, applied after schema parse in `batch-builder.ts`. The batch descriptor now also carries `z.array(WorkspaceUserSchema)`, so batch results are validated too. Prompted by [twist-cli#245 review](Doist/twist-cli#245 (review)) — the default belongs in the SDK so cli/MCP can drop their own filtering. ## Test plan - [x] `getWorkspaceUsers` excludes removed users by default (awaited + batch) - [x] `includeRemoved: true` returns all users (awaited + batch) - [x] No `include_removed`/`with_removed` query param is sent (proves client-side filtering) - [x] Full suite green (245 tests), `type-check`, `oxlint`, `oxfmt` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## [2.8.0](v2.7.1...v2.8.0) (2026-05-22) ### Features * **workspace-users:** exclude removed users by default ([#138](#138)) ([6e4b93f](6e4b93f)), closes [twist-cli#245](Doist/twist-cli#245)
This has been done and is available in 2.8.1 of the SDK. @lmjabreu ask your agent to pull in that newer SDK, and then we can tidy up this PR. The PR should still make the flag available, but it doesn't need to do any of the filtering. |
Address PR feedback:
- Scott: twist-sdk 2.8.1 now filters removed users by default and
exposes `includeRemoved` on `getWorkspaceUsers`. Drop the CLI-side
`.filter(u => !u.removed)` and just forward `includeRemoved`
through. Keeps the fix in one place so the MCP and any other
SDK consumer benefit too.
- doistbot: extend `SKILL_CONTENT` with `--include-removed` so the
AI agent skill stays in sync (re-ran `npm run sync:skill`).
- doistbot: move the new tests out of `describe('user --json')` into
their own block, and strengthen assertions: text output verifies
the chip lands on the removed row only; new test pins curated
`--json --include-removed` output exposes `removed` without
needing `--full`.
- doistbot: add `src/lib/api.test.ts` that mocks the SDK client and
asserts `getWorkspaceUsers` forwards `includeRemoved` to the
SDK — guards the contract the filtering now relies on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@scottlovegrove done — bumped to |
Summary
tw userswas listing users who had been removed from the workspace. InDoist(ws:1585) this meant 554 entries returned for what is actually a 146-member workspace, pollutingtw users [--json]output and silently breakingresolveUserRefs— any command that takes a name/email user ref (tw channel add,tw groups add-user, etc.) could resolve to a tombstoned user and then 403 from the backend withUser N is not part of workspace.Root cause: Twist's v4
workspace_users/get?id=<wsid>endpoint returns both active and removed members with aremoved: booleanflag. The SDK'sWorkspaceUserSchemaalready exposes that field — the CLI just wasn't reading it. The curated--jsonformatter also stripped it, so the staleness was invisible.Reproducer (before the fix):
After the fix:
Changes
src/lib/api.tsgetWorkspaceUsers(workspaceId, { includeRemoved? })filtersremoved === trueby default. One filter, fixestw users,resolveUserRefs(src/lib/refs.ts), andresolveNotifyIds(src/commands/thread/helpers.ts) in one shot.src/commands/user.ts--include-removedflag. Human output appends a red[removed]chip.src/lib/output.tsremovedtoUSER_ESSENTIAL_FIELDSso callers can distinguish the two states without--full.src/commands/user.test.tsgetUserById-based callers (e.g.tw groups view) were already correct because they only fetch ids that the group'suserIdslist references — removed users aren't there.Why a flag, not always filter
Audit/recovery cases occasionally do want the historical roster (e.g. "who used to be in here?").
--include-removedcovers that without making the common case noisy.Side discoveries
None. Scope was kept tight to the diagnosed bug.
Checks
npm run type-checknpm run lint:checknpm test— 653/653 (2 new)Follow-up to side-discovery #3 in #244.