Skip to content

@W-23448423: add user-license-reclamation-apply prompt (HITL-guarded)#518

Merged
Akash-Rastogi merged 7 commits into
mainfrom
akash/3b-license-reclaim-apply
Jul 20, 2026
Merged

@W-23448423: add user-license-reclamation-apply prompt (HITL-guarded)#518
Akash-Rastogi merged 7 commits into
mainfrom
akash/3b-license-reclaim-apply

Conversation

@Akash-Rastogi

@Akash-Rastogi Akash-Rastogi commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

[Akash's agent]

Summary

  • Adds the user-license-reclamation-apply MCP prompt — a HITL-guarded destructive workflow that identifies inactive licensed users, surfaces their owned-content counts, and (only after explicit human approval) downgrades approved users to Unlicensed via the update-user tool.
  • Follows the same two-phase preview→confirm pattern as stale-content-cleanup-apply and extract-optimization-apply. Defaults to dry run.
  • Includes 30 unit tests covering happy path, partial confirm, full reject, adversarial prompt-injection refusal, env var configuration, and lookback-cap clamping.

Key design decisions

  • Aligned with inform prompt findings: Corrected TS Events field names (Actor User Name, Event Date), uses Access events (catches embedded/API usage), includes all compound roles (SiteAdministratorCreator, SiteAdministratorExplorer), caps lookback at 90 days, and sets limit: 10000 on queries.
  • Ownership retained: The prompt explicitly surfaces owned-content counts per user and makes clear that downgrading does NOT delete or reassign content.
  • Token-gated confirm: Uses gateKind: 'token' — the update-user tool returns a per-user confirmationToken from the preview call, verified on confirm.
  • Env var support: Reads LICENSE_RECLAIM_INACTIVE_DAYS and LICENSE_RECLAIM_ROLES from process.env (matching inform prompt pattern).
  • Null-lastLogin handling: Provisioned-but-never-signed-in users are included as candidates with Days Inactive = "Never".
  • Prompt-injection defense: userIds and siteRoles args are regex-constrained to safe character classes. The HITL gate block is rendered from hardcoded strings, never user-controlled values.

Dependencies

All dependencies are now merged to main:

Files changed

File Change
src/prompts/userLicenseReclamation/apply.ts New — prompt implementation
src/prompts/userLicenseReclamation/apply.test.ts New — 30 unit tests
src/prompts/index.ts Registration
docs/docs/prompts/user-license-reclamation-apply.md New — docs page
docs/docs/intro.md Prompt table row
package.json / package-lock.json Version bump → 3.5.0

Test plan

  • npm run build — passes
  • npx tsc --noEmit — passes
  • npm run lint — passes
  • npm test — 2369 tests pass (153 files)
  • E2E test against live Tableau Cloud

🤖 Generated with Claude Code

@Akash-Rastogi Akash-Rastogi left a comment

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.

[Akash's agent] Self-review — 1 finding.

Comment thread src/prompts/userLicenseReclamation/apply.ts Outdated
Akash-Rastogi and others added 3 commits July 20, 2026 07:46
HITL-guarded apply prompt that chains 3A inform output into
update-user downgrades to Unlicensed. Follows the same two-phase
preview→confirm pattern as stale-content and extract-optimization
apply prompts. Defaults to dry run; no mutation without explicit
human approval at the Step 4 gate.

- src/prompts/userLicenseReclamation/apply.ts (prompt impl)
- src/prompts/userLicenseReclamation/apply.test.ts (23 tests incl.
  HITL-refusal adversarial cases)
- src/prompts/index.ts (registration)
- docs/docs/prompts/user-license-reclamation-apply.md
- docs/docs/intro.md (table row)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Self-review finding: Steps 2 (ts-events) and 3 (site-content) told
the model to call query-admin-insights but didn't provide VDS query
objects with exact field captions. The model would have to guess
Tableau-specific field names, causing runtime failure.

Now provides deterministic JSON query blocks (same pattern as
job-optimization-inform) with exact field captions and filters:
- Step 2: ts-events with Login/Login (Embedded) filter, LASTN date
- Step 3: site-content with Workbook/Datasource item-type filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix TS Events field names (Actor User Name, Event Date — not Actor User ID / Event Created At)
- Use Access events instead of Login (catches embedded/API usage)
- Add all compound roles (SiteAdministratorCreator, SiteAdministratorExplorer) to defaults
- Cap rangeN at 90 days (TC standard lookback limit)
- Add limit: 10000 to prevent unbounded result sets
- Add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES env var support
- Handle null-lastLogin users (provisioned but never signed in)
- Add ETL lag and lookback cap caveats to Fixed notes
- Bump version to 3.5.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Akash-Rastogi
Akash-Rastogi force-pushed the akash/3b-license-reclaim-apply branch from b63b29d to 280f8bc Compare July 20, 2026 14:50
@Alon-ST-DATA

Alon-ST-DATA commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

[Alon's agent] ## Review — user-license-reclamation-apply prompt

Reviewed against the repo's published standards and the review-feedback this codebase tends to flag. Verdict: ship-with-fixes — no blockers, but 3 MAJOR items should land before merge (the Owner LUID one especially, since it silently breaks Step 3). Read-only review; no live e2e run yet (that's a separate pass).

Heads-up on the PR description

The body says this is "blocked on 3C / update-user doesn't exist on main yet." That appears to be staleupdate-user is fully registered on main (src/tools/web/toolName.ts, tools.ts, mutationGuard.ts, updateUser.ts) and its confirm contract (server token bound to ${userId}:${siteRole}, single-use, verified+consumed on confirm:true) matches this prompt's call shape exactly. The integration is testable now — please don't gate merge on 3C for that reason.

MAJOR

  1. Owner LUID is not a valid Site Content caption (Step 3). Step 3's ownership query selects and joins on Owner LUID, but the repo's only Site Content schema (getStaleContentReport.ts) exposes Owner Email, not Owner LUID — no Owner LUID caption exists anywhere in src. An invalid caption will error the Step 3 VDS call and ownership counts fail. The "verified against live VDS" comment only covers TS_EVENTS_FIELDS, not the site-content list. Fix: join on Owner Email (already fetched) against the Step 1 email, or live-probe Owner LUID before relying on it. Informational-only step — does not gate the downgrade, so no wrong-downgrade risk, but it silently breaks.

  2. Wrong inactivity rule (Step 2). The text says a user is inactive if they have no Access event "or if they were absent from Step 1 results entirely (never logged in)." Step 1 (list-users) returns all users, so "absent from Step 1" matches nobody — this conflates TS-Events absence with a null lastLogin. Logically inert (can't over-include), but it's the core rule of a destructive flow and should be right. Fix: "…NO Access event in the Step 2 result, or whose lastLogin from Step 1 is null."

  3. Docs omit two in-scope roles (Arguments table). The docs list default siteRoles as "Creator, Explorer, ExplorerCanPublish, Viewer", but LICENSE_RECLAIM_ROLES_DEFAULT also includes SiteAdministratorCreator and SiteAdministratorExplorer. This is safety-relevant — the docs hide that site-admin roles are in the default downgrade scope. The argsSchema .describe() correctly lists all six; only the docs table is wrong. Fix: list all six.

MINOR

  1. inactiveDays arg path isn't clamped: regex ^[1-9]\d{0,3}$ accepts 1–9999, but the description/env say 1–3650. getConfiguredInactiveDays() clamps the env path; the arg path (parseInt) doesn't. Low harm (min(x, 90) downstream), but it mislabels the threshold text — clamp the arg path or tighten the regex.
  2. LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES are read by this prompt, but env-vars.md attributes them only to the inform prompt and documents the inform default (Creator,Explorer). Apply's Configuration section never mentions them, and apply's unset default differs (6 roles). Document both.
  3. Version bumped to 3.5.0 while main is at 3.2.0 (diff base 3.4.0) — this assumes the stacked predecessors (3.3.x/3.4.x) merge first. Please verify the merge-train ordering to avoid a version collision/skip.
  4. Docs Workflow step 2 says ts-events "determine[s] each user's last login date." ts-events returns Access events, not last-login (last-login comes from list-users). Reword.

NIT

  1. The 23 tests are almost all literal toContain string matches — they'd still pass if the tool contract were wrong, and they're brittle to rewording. Only the gate-determinism/ordering tests exercise real risk. Acceptable for a text generator, but nothing currently guards the Step 2 wording or the Owner LUID caption.

Verified clean

  • TS-events query correctly uses Actor User Name + Event Date + Event Type SET['Access'] — avoids the Actor User Id INTEGER-not-LUID trap and does not use Created At.
  • HITL gate is deterministic — no user-controlled value interpolated (proven by the benign-vs-injection gate-slice diff test).
  • Two-phase ordering (gate → preview with confirm omitted, token kept → confirm:true) enforced in text and asserted; gateKind:'token' matches the real update-user server contract.
  • Prompt injection: userIds/siteRoles flow only into backtick-wrapped Scope/roles lines, never into gate/confirm; regex rejects backticks/quotes; confirm still requires a server token.
  • Gating: disabled:!adminToolsEnabled present; not registered when the flag is off (no scope leak); update-user self-enforces admin + preview/confirm server-side.
  • Registration trio + docs page + intro.md row present; dryRun defaults true; all four argsSchema fields reflected in the text.

For the later e2e pass (targets)

  1. Highest priority: live-probe Owner LUID on the Site Content datasource — if invalid (likely), Step 3 errors → switch the join to Owner Email.
  2. dryRun unset → model stops at Step 4 with zero update-user calls, and the surfaced default role scope includes both SiteAdministrator variants.
  3. dryRun:false, approve one user → preview returns a token bound to userId:Unlicensed; a forged/reused token is rejected; a token minted for user A cannot confirm user B.
  4. ts-events Access grouping by Actor User Name matches real usernames/emails on a populated site; 90-day cap + ETL-lag behavior for inactiveDays > 90.

🤖 Review assisted by Claude Code (tmcp-lead + tmcp-reviewer). Findings verified against code on main.

@Alon-ST-DATA Alon-ST-DATA 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.

Inline findings for user-license-reclamation-apply (companion to the summary comment above). Verdict: ship-with-fixes — no blockers, 3 MAJOR / 4 minor / 2 nit. Read-only review; no live e2e yet.

Heads-up: the PR body's "blocked on 3C / update-user doesn't exist on main" looks stale — update-user is fully registered on main and its token confirm contract (${userId}:${siteRole}, single-use) matches this prompt's call shape. Testable now.

Comment thread src/prompts/userLicenseReclamation/apply.ts Outdated
Comment thread src/prompts/userLicenseReclamation/apply.ts Outdated
Comment thread src/prompts/userLicenseReclamation/apply.ts Outdated
Comment thread src/prompts/userLicenseReclamation/apply.ts Outdated
Comment thread docs/docs/prompts/user-license-reclamation-apply.md Outdated
Comment thread docs/docs/prompts/user-license-reclamation-apply.md Outdated
Comment thread docs/docs/prompts/user-license-reclamation-apply.md
Comment thread package.json
Akash-Rastogi and others added 2 commits July 20, 2026 09:56
MAJOR fixes:
- Replace invalid `Owner LUID` caption with `Owner Email` (join by email)
- Fix Step 2 inactivity rule: "whose lastLogin from Step 1 is null" not
  "absent from Step 1"
- Docs: list all 6 license-consuming roles including compound variants

MINOR fixes:
- Clamp arg-path inactiveDays to max 3650 (matches env-path behavior)
- Docs: reword Step 2 from "last login date" to "activity signals"
- Docs: add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Alon-ST-DATA

Copy link
Copy Markdown
Contributor

[Alon's agent] Round-2 review — head 47cacd90

Verdict: ship-with-fixes. Dry-run default (dryRun:true) is safe to merge. Do not run dryRun:false on a real site until the two MAJORs below are fixed.

Round-1 findings are addressed

Round-1 reviewed 280f8bcb; 48f12733 ("address review findings — Owner LUID, inactivity rule, docs") fixed MAJORs #1#4. I've reply-resolved those stale inline threads. Live e2e independently confirmed the Owner-LUID fix: the old query returned Unknown Field: Owner LUID, and Owner Email now resolves correctly.

2 net-new MAJORs (block dryRun:false)

MAJOR-1 — recent-login users can be downgraded (apply.ts:217,220-224,239-242)
Step 1 selects candidates by siteRole + "is licensed" + null-lastLogin only. Inactivity is then decided solely by "no Access event in the ts-events window" (L241-242) — there is no lastLogin < cutoff pre-filter (the inform sibling uses lastLogin:lt:${cutoffIso}). So inactiveDays, advertised at L217 as the "Inactive threshold" and in docs as "Minimum days since last login", only sizes the lookback window. A user who logged in yesterday but triggered no Access event gets flagged inactive and queued for downgrade.
Fix: re-add the lastLogin < cutoff gate, or rename the arg so it no longer promises a login-based threshold.

MAJOR-2 — silent ts-events truncation (apply.ts:97-118)
buildActivityQuery is site-wide (no Actor User Name scope) with hardcoded limit: 10000. The raw-VDS path truncates at the limit with no warning. On a busy site with >10k Access events in the window, truncated-away users look inactive → false-positive downgrade.
Fix: scope the query to the candidate emails, and/or warn when returned rows == limit.

MINORs

  • apply.ts:48 — regex ^[1-9]\d{0,3}$ accepts 1–9999 but the message says 1–3650 (clamped by Math.min(...,3650), so cosmetic).
  • Step-3 ownership query (apply.ts:120-134) has the same limit:10000 silent-truncation risk — informational only (ownership counts), lower impact than MAJOR-2.
  • Docs: "Activity signals" step describes ts-events as returning activity, fine — but inactiveDays doc row (L30) says "Minimum days since last login", which MAJOR-1 shows is not what the code enforces. Reconcile with the MAJOR-1 fix.

Verified clean

Registration trio, admin gating, HITL two-phase token (bound to userId:siteRole, single-use, cross-role-safe — proven live: preview→apply→restore, plus cross-role / no-token / replay all rejected), prompt-injection regex, dryRun default true, Owner Email join + TS Events captions, docs page, version bump.

Version — clean

main is now 3.4.0 (PR #553 merged as 3f089875); this PR's 3.5.0 is one minor above — no collision.

Comment thread src/prompts/userLicenseReclamation/apply.ts Outdated
Comment thread src/prompts/userLicenseReclamation/apply.ts
Comment thread src/prompts/userLicenseReclamation/apply.ts
Comment thread src/prompts/userLicenseReclamation/apply.ts Outdated
- Require lastLogin to be null or older than inactiveDays threshold
  before flagging a user as inactive (prevents false positives from
  ETL lag on recently-active users)
- Warn admin when TS Events or Site Content queries hit the 10k row
  limit (silent truncation could cause false-positive downgrades)
- Tighten inactiveDays regex to reject 3651-9999 at schema level
  (previously only clamped at runtime via Math.min)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@Alon-ST-DATA Alon-ST-DATA 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.

[Alon's agent] Approve — head 8c734cfa.

Re-reviewed the fix commit ("add lastLogin pre-filter, truncation warnings, and tighten regex"). All four round-2 findings addressed:

  • MAJOR-1 (recent-login downgradable): inactivity now requires both lastLogin null-or-older-than-inactiveDays AND no Access event; explicit "users whose lastLogin is within the last N days are NOT candidates, even with no Access event." The false-positive-from-ETL-lag path is closed.
  • MAJOR-2 (silent ts-events truncation): admin is now warned when the result hits the 10k-row limit. Note: the query is still site-wide limit:10000 rather than candidate-scoped, so the mitigation is visibility not elimination — a reasonable prompt-only tradeoff, but candidate-scoping (Actor User Name SET filter) would be a stronger follow-up if this ever runs on very large sites.
  • MINOR (Step-3 truncation): same 10k-row warning in the ownership report.
  • MINOR (regex): tightened to exactly 1–3650 at the schema level.

Verification: 34/34 unit tests pass (was 8 — 26 regression tests added). Diff scoped to the expected files; version 3.5.0 is one minor over main (3.4.0) — clean.

Destructive update-user two-phase token flow was proven live earlier (preview→apply→restore; cross-role / no-token / single-use-replay all rejected) and is unchanged by this commit. Since these changes are prompt-text + regex only, no fresh live e2e was needed for regression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Akash-Rastogi
Akash-Rastogi merged commit cc23ea8 into main Jul 20, 2026
8 checks passed
@Akash-Rastogi
Akash-Rastogi deleted the akash/3b-license-reclaim-apply branch July 20, 2026 20:54
mattcfilbert added a commit that referenced this pull request Jul 21, 2026
…tasource-luid) (#570)

* @W-23202034: Prevent sign-out teardown from masking the real REST error (#470)

useRestApi signs out ephemeral (pat / direct-trust) sessions in a `finally`
block. Because a throw inside `finally` replaces whatever the `try` was
returning or throwing, an un-caught sign-out failure clobbered the callback's
real outcome: a callback that 404s on a missing resource surfaced to the caller
as the sign-out's 401 once the session was torn down. That is the misleading
"delete-workbook returns 401 for a non-existent workbook LUID" report.

Sign-out is best-effort cleanup (the ephemeral session expires on its own), so
isolate it in its own try/catch and swallow-and-log any failure at 'warning'.
The callback's result or error now always reaches the caller intact.

Tests: two unit cases in restApiInstance.test.ts inject a rejecting signOut and
assert the callback's error (and a successful result) survive teardown. Both
fail without the fix. e2e is N/A — the defect only manifests when signOut itself
fails at teardown, which a live run cannot force deterministically.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @W-23202054: Stop get-stale-content-report from silently ignoring invalid projectIds (#473)

* @W-23202054: Stop get-stale-content-report from silently ignoring invalid projectIds

get-stale-content-report accepts a projectIds list to scope the report.
Two resolution paths silently dropped requested IDs:

1. resolveProjectScopeIds dropped IDs outside the server's bounded context
   (INCLUDE_PROJECT_IDS) with no signal.
2. resolveProjectIdsToNames intersected requested IDs against real site
   projects; IDs matching no project vanished.

Worst case: when EVERY requested ID was invalid, the scope collapsed to
empty and buildSiteContentQuery treated it identically to "no scope" —
returning the ENTIRE site instead of nothing. A typo in a project scope
silently widened the report to all content.

Fix:
- Track dropped IDs through both resolution paths (unknown-on-site vs
  not-permitted-by-config) and surface them in mcp.warnings, reusing the
  result convention established by query-datasource.
- Widening guard: when a scope was requested but nothing resolved to a real
  project, return an empty report (0 rows) plus warnings — never fall
  through to the unscoped full-site query.
- Back-compat: no mcp key is emitted when all requested IDs are valid.

Test coverage: partial-invalid (scoped to valid subset + warning),
all-invalid (empty report, asserts no VDS query fires), bounded-context
drop, all-valid back-compat, plus direct unit tests for the new resolver
return shapes and warning assembly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @W-23202054: Branch warning wording on whether a valid scope remains; drop env-var name from output

Addresses PR review (Matt):

1. The ignored-projectIds warning message was unconditional — "The report was
   scoped to the remaining valid projects." fired even in the all-invalid case,
   asserting a valid subset that does not exist. That false premise is exactly
   the silent-widening confusion this fix targets. The message now branches on
   whether any requested project survived: when none do, it states an empty
   report (0 rows) was returned instead of the full site, giving an LLM caller
   a self-correction signal.

2. The not-permitted-by-config message no longer names the INCLUDE_PROJECT_IDS
   env var — "outside this server's configured project scope" carries the same
   signal for a caller without coupling tool output to a config key. (The docs
   page still names the env var; that prose is admin-facing config reference.)

Tests: added buildProjectIdWarnings cases for both message branches and the
env-var-not-leaked assertion; strengthened the all-invalid callback test to
assert the empty-report wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @W-22030892: Add friendly client-name mapping for OAuth client_id values in UIP telemetry (#479)

* @W-22030892: Add friendly client-name mapping for OAuth client_id in tool telemetry

Bearer clients now emit two additive tool_call properties:
oauth_client_id (raw CIMD-URL client_id, canonical) and
oauth_client_display_name (host-keyed static map -> Claude/Cursor/VS Code;
unknown clients fall back to the raw value; absent auth emits '' matching
existing optional-prop style). Telemetry/display only — no auth or
request-handling changes; no network CIMD fetches on the hot path.

Known-host map is best-effort (repo has no observed production client_ids);
fail-safe by construction — wrong/missing entries surface raw, never
corrupt. Single edit point: knownClientDisplayNamesByHost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* @W-22030892: bump version to 2.25.3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* @W-22030892: cover embedded-OAuth client ids + bound telemetry values

Self-review round fixes:
- embedded-OAuth sessions (tableauAuthInfo normalized to X-Tableau-Auth)
  carry the client id on extra.authInfo.clientId — telemetry now derives
  Bearer clientId ?? authInfo.clientId (RED-verified: the path emitted
  blanks before).
- client_id is an attacker-influenceable token claim: new
  sanitizeClientIdForTelemetry bounds it (URL -> origin+pathname, drops
  search/hash/userinfo; 200-char cap) on both emitted fields, following
  the accessTokenValidator claim-truncation precedent (divergence from
  its 256+marker documented in the module header).

150 files / 2,246 tests green, tsc + eslint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* @W-23149256 prerelease-aware npm + Docker publish (hotfix without moving latest) + release runbook (#416)

* @W-23149256 prerelease-aware npm + Docker publish (hotfix without moving latest) + release runbook

Formalizes the hotfix release path so a livesite fix can ship off an
already-deployed version without dragging main drift or moving the latest
dist-tag. Andy's spec; continuity-critical (release-process holder departs 07-02).

- publish.yml: prerelease-aware npm publish. A version with a SemVer prerelease
  identifier (a '-', e.g. 2.18.0-hotfix.1) publishes under a 'hotfix-v<version>'
  dist-tag, never latest; stable path unchanged (bare npm publish). Fails CLOSED
  if the version can't be read (refuses to risk moving latest). dist-tag is NOT
  the bare version — npm rejects a dist-tag that parses as valid SemVer.
- docker-publish.yml: Docker 'latest' skips prerelease tags (mirror guard), so a
  hotfix gets its exact-version Docker tag but doesn't steal latest.
- docs/RELEASE.md (new): normal + hotfix runbook (branch off tag, fix-on-main-
  then-cherry-pick, bump to -hotfix.N, release from hotfix branch, pin exact
  version in HF) + rationale; linked from CONTRIBUTING.

Pre-push dual self-review (GPT-5.5 + Opus) caught 3 blockers on local code before
this commit: npm rejecting a valid-SemVer dist-tag (fatal), Docker latest moving
on hotfixes, and fail-open on empty version. All fixed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* @W-23149256 preflight: release tag must equal v<package.json version> + runbook clarifications

Review follow-up (self-review + Jaehun's walkthrough ask): npm decides
prerelease-vs-stable from package.json while Docker decides from the release
tag name — a tag/version mismatch could move Docker 'latest' while npm holds
(or vice versa). Both workflows now fail closed on mismatch before publishing;
the tag name reaches the scripts via env, not inline interpolation. Runbook:
Release-publish (not tag push) is the trigger, mark hotfix releases
pre-release, and rerun-after-partial-failure semantics (npm immutability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* @W-23375797: consolidate admin-gated tools into polymorphic dispatchers (#475)

* @W-23375797: consolidate admin-gated tools into polymorphic dispatchers

Two new polymorphic tools reduce the model-visible admin tool surface:

- `query-admin-insights` (kind discriminator) merges 4 insight readers:
  get-stale-workbooks, get-stale-datasources, get-content-usage-metrics,
  get-user-activity.
- `delete-content` (resourceType discriminator) merges 3 preview→confirm
  destructive tools: delete-workbook, delete-datasource,
  delete-extract-refresh-task.

Behavior preserved end-to-end. `delete-content` records
AppApprovalEvidence under legacy namespaces so the mcp-apps HITL panel
flow (flag-ON) continues to dispatch confirm-delete-workbook /
confirm-delete-datasource / confirm-delete-extract-refresh-task via
the existing iframe machinery. The flag-OFF preview→confirm tag/nonce
evidence gates work unchanged.

Auth: scope-based auth in scopes.ts adds delete-content and
query-admin-insights, each carrying the union of its component tools'
MCP + REST scopes. Both are dropped from `getEnabledToolNames()` when
`ADMIN_TOOLS_ENABLED=false`.

Legacy tools remain wired during the shim window.

Bumps package version 2.25.0 → 2.26.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* @W-23375797: fix self-review findings on consolidated tools

- delete-content: replace three per-resource MCP scopes with the
  umbrella tableau:mcp:content:delete so authMiddleware's AND-enforcement
  doesn't 403 callers who only granted one legacy scope. Callers who
  need per-resource granularity keep using the legacy delete-{workbook,
  datasource,extract-refresh-task} tools during the shim window.
- query-admin-insights: move the missing-query check inside
  logAndExecute so failed invocations are logged and product-telemetry
  emitted instead of being silently dropped by the pre-callback return.
- query-admin-insights: honor per-kind MAX_RESULT_LIMITS entries for
  the four legacy admin-insights tool names by taking the tightest of
  consolidated cap, legacy cap, and caller-provided limit — prevents
  operators' existing cap config from silently disappearing on migration.

* @W-23375797: adapt consolidated stale-content to new resolver shapes

PR #473 changed getStaleContentReport's helpers to return richer objects:
- _resolveProjectScopeIds now returns {scopeIds, boundedOutOfScopeIds}
- _resolveProjectIdsToNames now returns {names, unknownIds}
- new _buildProjectIdWarnings helper + _StaleReportWarning type

Mirror the legacy get-stale-content-report tool in queryAdminInsights so
projectId warnings and the all-invalid widening guard behave the same via
kind=stale-content. Re-export the two helpers + type from getStaleContentReport
so both tools share one implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* @W-23375797: fix e2e tool-list test for gated consolidated tools

The consolidated query-admin-insights and delete-content tools are
disabled unless ADMIN_TOOLS_ENABLED=true, so they don't appear in the
server's tool list in the default e2e run. Add them to the
adminOnlyTools filter in the default and combined variant "should list
tools" tests, matching how the legacy per-kind admin tools are already
filtered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* @W-23375797: lock in tightest-cap via legacy MAX_RESULT_LIMITS

Add a queryAdminInsights.test.ts case that sets
MAX_RESULT_LIMITS=query-admin-insights-ts-events:5, invokes the
consolidated tool with kind=ts-events and limit=100, and asserts the
underlying VDS query is issued with rowLimit=5 — so the legacy per-kind
cap still wins after callers migrate to query-admin-insights. Guards
against the silent-migration hazard flagged in the self-review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* @W-23375797: fix oauth-embedded scopes_supported count for umbrella scope

The new tableau:mcp:content:delete umbrella scope (added by the
self-review fix on delete-content) bumps scopes_supported from 14 to
15. Update the three well-known-endpoint tests to expect the new scope
by name and the new length.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* @W-23375797: address review — trim descriptions, type logAndExecute, document flag-ON path

- Trim query-admin-insights description from ~546 to ~120 tokens; remove
  inline JSON schema + per-kind param tables (Major, Alon).
- Trim delete-content description from ~437 to ~100 tokens; remove
  per-resourceType parameter reference (Major, Alon).
- Document that flag-ON preview path writes both TagEvidence AND
  AppApprovalEvidence in all three resource branches (Minor, Alon).
- Add explicit <QueryOutput> type param to raw-VDS logAndExecute for
  symmetry with <StaleContentResult> branch (Minor, Alon).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* @W-23375797: export QueryOutput type from adminInsightsToolBase

The previous commit imported QueryOutput in queryAdminInsights.ts from
adminInsightsToolBase.js, but that module only used it locally without
re-exporting. Add `export type { QueryOutput }` so the consolidated
tool's explicit type param compiles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: add documentation pages for query-admin-insights and delete-content

- docs/docs/tools/admin-insights/query-admin-insights.md — consolidated tool
- docs/docs/tools/content/delete-content.md — consolidated tool
- docs/docs/tools/content/_category_.json — sidebar category
- docs/docs/intro.md — 2 new table rows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* @W-23415681: make FeatureGateProvider.isFeatureEnabled async (#497)

* @W-23415681: make FeatureGateProvider.isFeatureEnabled async

* @W-23415681: await async isFeatureEnabled in get-embed-token authz test

* @W-23415681: prune redundant feature-gate init tests

* @W-23375797: remove deprecated admin tool shims (Phase 2) (#498)

* @W-23149256: fix publish.yml startup failure from empty ${{ }} in run: block (#506)

The prerelease-aware comment block added in #416 included an empty
${{ }} expression inside a run: | script. GitHub Actions evaluates
${{ }} in run: text before the shell sees it, so the shell-comment #
does not protect it; the empty expression is invalid and the workflow
fails to load (startup failure, no publish job ever runs). Reword the
comment to drop the token. actionlint clean; publish logic unchanged.

* @W-12345678 Docs update 7-14 (#509)

* 7/14 docs

* version bump

* @W-22350774: Add Tableau Prep flows tools (list-flows, get-flow) (#344)

@W-22350774 Add Tableau Prep flows tools (list-flows, get-flow)

Add two read-only MCP tools that wrap the public Tableau REST Flows API:

- list-flows: lists Prep flows on a site with field:operator:value
  filtering (createdAt, name, ownerName, projectId, projectName, updatedAt)
  and sorting. Every successful response includes an always-present
  mcp.resultInfo { returnedCount, truncated, truncationReason?,
  totalAvailable? } so a caller can distinguish a complete list from one
  cut short by the caller's limit or the admin MAX_RESULT_LIMIT cap.
  totalAvailable is surfaced only when no server-side bounded context
  (PROJECT_IDS/TAGS) is configured, since that count is taken before the
  tool's allow-list filtering.
- get-flow: retrieves a single flow with optional connections and run
  history. Sidecar OAuth scopes are requested only when the corresponding
  sidecar is included, and access is gated by the bounded-context resource
  checker (isFlowAllowed) so PROJECT_IDS/TAGS restrictions apply. Run
  history is bounded by flowRunLimit and emits a FLOW_RUNS_TRUNCATED
  warning when more runs exist.

Supporting changes:
- Gate all flow tools behind FLOW_TOOLS_ENABLED, disabled by default; set
  it to "true" to register list-flows and get-flow (mirrors
  ADMIN_TOOLS_ENABLED). Individual tools can still be excluded via
  EXCLUDE_TOOLS.
- Add the flow REST API definitions, Zod types, and methods, plus the
  dedicated tableau:flows:read, tableau:flow_connections:read, and
  tableau:flow_runs:read scopes.
- Harden parseAndValidateFilterString bracket / in: list handling
  (repo-wide fix).
- Make project.description optional (Tableau returns it on embedded
  <project> elements).
- Tools, types, unit tests, a description-quality eval test, and docs for
  both tools.

* @W-23375797: fix unawaited async feature gate in confirmDeleteContent (#513)

* fix: await async isFeatureEnabled in confirmDeleteContent disabled field

PR #497 made isFeatureEnabled async but this call site was not updated,
causing `!Promise` to always evaluate to `false` — the tool was never
disabled when mcp-apps was OFF.

Wrap in Provider(async () => ...) to match confirmUpdateCloudExtractRefreshTask.
Update test mocks from mockReturnValue to mockResolvedValue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version to 3.1.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(test): remove flow:read from OAuth scope assertions

Flow tools are gated behind FLOW_TOOLS_ENABLED (off by default), so
tableau:mcp:flow:read is never advertised in scopes_supported. The
assertions were incorrect as shipped by the flows PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* @W-23388458 - Add insight-cards MCP tool (deterministic insights from datasource context) (#502)

* @W-23388458: Add generate-chiron-insight-cards tool + datasource LUID resolver

Add a deterministic Chiron insight-cards path to tableau-mcp:
- resolve-datasource-luid: exact case-sensitive contentUrl -> LUID resolution
  (name is not unique; REST contentUrl filter is case-insensitive).
- generate-chiron-insight-cards: resolves a datasource, reads metadata to pick
  measures/time field/dimensions, runs period-over-period detail bundles via a
  shared runChironBundle helper, and shapes governed InsightCard JSON
  (headline, delta, direction, PoPC comparison, series, breakdown).
- Register the tool + scopes; expose contentUrl on datasource types.

Co-authored-by: Cursor <cursoragent@cursor.com>

* @W-23388458: Expand Chiron insight cards to full detail payload + drill-down

Extend generate-chiron-insight-cards from a headline card into the full
governed insight-detail payload and support recursive drill-down:

- InsightCard now carries context, contributors (with % of lift),
  pronounced (most/least), unusual (anomaly), followUps, actions, and
  breakdownDimension; extractors read these from the Pulse detail bundle.
- Add drill params: breakdownDimension (scopes the breakdown to one
  dimension) and filters (member scoping). Response returns the full
  dimension list so drilled cards still propose fresh follow-ups, while
  only the active dimension scopes allowed_dimensions.
- Member-filter contract confirmed live against Pulse: OPERATOR_EQUAL with
  categorical_values [{ string_value }] only, in metric_specification
  (adding '='/bool_value/null_value or using basic_specification 400s).
- Unit tests cover the detail payload, drill params, and the filter shape.

Co-authored-by: Cursor <cursoragent@cursor.com>

* @W-23388458: Rename tool to generate-insight-cards; drop Chiron/Pulse from public name + description; fix tsc types; bump version

* @W-23388458: Address PR review - bounded-context guards, surface bundle failures, validate drill args, fix nondeterministic/aliased fields

* @W-23388458: De-Chiron internal names -> insight* (folder/files/functions/types); no behavior change

* @W-23388458: Address review - indistinguishable not-found/not-allowed + gate before identity lookup; opt-in (disabled-by-default) registration; validate measures/timeField; deterministic series sort

* @W-23388458: Gate insight tools behind INSIGHTS_TOOLS_ENABLED (off by default) + deterministic field selection

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* @W-23377934: fix extract-refresh-task delete preview existence check + confirm double-audit (#529)

* @W-23377934: fix extract-refresh-task delete preview existence check + confirm double-audit

Two defects in the consolidated delete-content flow:

1. Preview minted a confirmationToken for a non-existent extract-refresh-task.
   The task branch's resolveTarget was a pure echo with no REST read, so a
   well-formed but non-existent taskId passed the UUID check and got a live
   nonce. Unlike the workbook/datasource branches (which 404 early via
   getWorkbook/queryDatasource), the task branch never read the target and
   there is no single-get task endpoint. Fix: list-and-find existence check
   before minting the nonce (preview + confirm), returning a 404 not-found.
   Requires tableau:tasks:read, added to the delete-content scope map.

2. Confirm phase double-logged audit. guardMutation emitted an unconditional
   'allowed' record plus the terminal completed/failed via recordOutcome, so
   every confirmed mutation logged twice (all resourceTypes / all preview->
   confirm tools). Fix: suppress 'allowed' on the confirm phase so a confirm
   emits exactly one terminal record; preview still emits its single 'allowed'.
   Audit lifecycle doc comments updated to the one-record contract.

Tests: 6 new pinning tests (nonexistent/empty-list task -> 404 + no token + no
delete; confirm -> exactly one completed/failed; preview -> single allowed).
Docs: delete-content.md documents the existence check and corrects the REST-API
reference (List, not the non-existent Get, Extract Refresh Tasks).
Version: 3.1.2 -> 3.1.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @W-23377934: fix e2e list-tools to filter INSIGHTS_TOOLS_ENABLED-gated tools

The e2e "should list tools" assertions (default + combined variants) built
expectedToolNames from webToolNames but never filtered the insights tools
(generate-insight-cards, resolve-datasource-luid) added in #502, which are
gated behind INSIGHTS_TOOLS_ENABLED (off by default). The built server omits
them, so the toHaveLength check failed (received 20/26 vs expected 22/28).

This surfaced on this PR because it runs from the main repo (not a fork), so
the fork-guarded E2E CI step actually executes. Added the insights filter to
both variants, mirroring the existing FLOW_TOOLS_ENABLED gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @W-23125362: complete mutation-tool safety layer — audit target enrichment + centralized HITL text + durability contract (#533)

Scope-completes the common mutation-tool safety layer (guard + evidence +
audit already merged). Finishes the in-scope remainder; auth-session tools
(revoke/reset), a durable external sink, and e2e tiers are forked to a
follow-up story.

- AC-3 (Gap B): enrich extract-refresh-task audit target with name/project/
  owner, derived from the underlying datasource (else workbook). New shared
  helper resolveExtractRefreshTaskTarget.ts; best-effort, degrades to id-only
  on Server sites or 403/404 and never blocks/fails the mutation. Applied to
  all four task paths (delete, confirm-delete, update, confirm-update). The
  delete branch reuses its existence-check task list (no extra round-trip).
- AC-4 (Gap D): centralize tool-side HITL text into new hitlText.ts
  (NEXT-STEP preview, confirm-closed, PreviewNotRun recovery). Byte-for-byte
  behavior-preserving; rethreaded delete-content, update-cloud, and the
  guard's PreviewNotRun branch. Kept in tools/_lib to avoid a tools→prompts
  cross-layer import.
- AC-3 (Gap C, doc): document the durability contract — audit records emit as
  structured JSON on the dedicated `audit` logger (bypasses LOG_LEVEL); durable
  retention is the deployment's responsibility (ship the stream to an immutable
  store). Added to auditRecord.ts and delete-content.md.
- AC-6 (Gap E, unit): per-tool assertions that enriched target fields appear in
  audit records on allow and deny; forged-confirm reject and preview→confirm
  happy paths.

Version 3.1.3 → 3.1.4. Full suite green (150 files / 2306 tests); tsc, lint,
build clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @W-23448424: add update-user tool — two-phase HITL site role update (#516)

* feat: add update-user tool — two-phase HITL site role update

- New `update-user` MCP tool with preview/confirm flow using RegistryEvidence
- OAuth scope: `tableau:mcp:users:write` → `tableau:users:update` + `tableau:users:read`
- Admin-gated, UUID-validated userId, enum-constrained siteRole
- Primary use case: downgrade inactive users to Unlicensed for license reclamation

@W-23448424

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: relax update-user response schema to accept partial user

The Tableau PUT /users/:userId endpoint only echoes back changed
fields (e.g. siteRole, name, email) without `id`. Using
`userSchema.partial()` prevents Zodios response validation failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version to 3.2.0 for update-user feature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review findings — idempotentHint, test coverage, scope phrasing

- Set idempotentHint: false (nonce is single-use; repeated calls reject)
- Add preview safety test: mockUpdateUser.not.toHaveBeenCalled()
- Add partial-response fallback test (API omits siteRole)
- Add missing siteRole/email fallback text test
- Add RegistryEvidence binding-isolation test (role-swap protection)
- Add e2e behavior test (preview, schema validation, bogus token rejection)
- Fix scope phrasing: list both tableau:users:update and :read
- Add comment explaining SupportUser/ServerAdministrator omission

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: gate update-user e2e tests behind UPDATE_USER_E2E_ID

Unlike the sibling tool, update-user's resolveTarget calls
queryUserOnSite which 404s for non-existent user IDs. Gate
preview and bogus-token tests behind the env var so they skip
gracefully when no real user LUID is configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: split mutating e2e test behind UPDATE_USER_E2E_DESTRUCTIVE_ID

Mirror sibling pattern (updateCloudExtractRefreshTask) — the preview
and bogus-token tests use UPDATE_USER_E2E_ID, while the actual mutation
uses a separate UPDATE_USER_E2E_DESTRUCTIVE_ID to prevent accidentally
downgrading a real user. All guidance consistently says "disposable user".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* @W-23336786  ci: bump GitHub Actions to latest major versions (#507)

ci: bump GitHub Actions to latest major versions

  Updates actions/checkout (v3/v4→v7), actions/setup-node (v4→v7),
  actions/upload-artifact (v4→v7), actions/upload-pages-artifact (v3→v5),
  actions/deploy-pages (v4→v5), actions/github-script (v8→v9),
  docker/setup-buildx-action (v3→v4), docker/login-action (v3→v4),
  docker/metadata-action (v5→v6), and docker/build-push-action (v5→v7)
  across all workflows.

* @W-23336786 Add action to automatically label "feature" PRs (#451)

* Ignore .DS_Store

* Add .github/workflows/pr-feature-label.yml, which fires when a PR is
opened against a base branch starting with "feature/" and labels it with
the branch remainder prefixed "feature:" (e.g. base feature/rounded-corners
-> label feature:rounded-corners).

Uses actions/github-script to get-or-create the label (auto-created gray if
missing) then apply it. The on.pull_request.branches: ['feature/**'] filter
scopes the trigger to feature base branches; the job carries issues:write +
pull-requests:write and the standard fork/dependabot guard.

* Adjust based on source or destination branchs as "feature"

* Operate consistently on PR actions

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: pin system time in integration tests to eliminate date-dependent flakiness

* Revert "fix: pin system time in integration tests to eliminate date-dependent flakiness"

This reverts commit 4cb7d5f.

Still want to fix this, but in a separate PR.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* @W-23448422: add user-license-reclamation-inform prompt (#517)

* @W-23448422: add user-license-reclamation-inform prompt

- New MCP prompt that identifies inactive licensed users as reclamation
  candidates by paginating list-users (role + lastLogin filters) and
  cross-referencing activity via query-admin-insights (ts-events)
- Config knobs: LICENSE_RECLAIM_INACTIVE_DAYS, LICENSE_RECLAIM_ROLES
- 13 unit tests covering defaults, arg overrides, env var wiring
- Docs page + intro.md entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version to 3.3.0

New prompt feature warrants a minor version bump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove broken markdown link to unreleased apply prompt doc

The Docusaurus docs build fails when a markdown link targets a file
that doesn't exist yet. Replace with inline code reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review — input validation, lookback cap, docs

- Add regex validation on `roles` arg to prevent prompt injection
- Cap TS Events `rangeN` at 90-day lookback max (avoids false
  positives when inactiveDays > 90)
- Add `limit: 10000` to ts-events query to bound result set
- Align inactiveDays regex to 1–3650 (matches env var clamp)
- Document MAX_RESULT_LIMIT caveat in docs workflow description
- Add LICENSE_RECLAIM_INACTIVE_DAYS and LICENSE_RECLAIM_ROLES to
  env-vars.md configuration docs
- Add source reference comment for TS Events field captions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: correct TS Events field captions and address live-verified findings

BLOCKER fixes (live-verified against prod TS Events datasource):
- `Created At` → `Event Date` (correct DATETIME field on TS Events)
- `Actor User Id` → `Actor User Name` (STRING matching username/email,
  joinable with list-users; Actor User Id is an internal integer)

MEDIUM fixes:
- Add second-pass instruction for null-lastLogin users (never signed
  in — strongest reclamation candidates, previously dropped silently)
- Add ETL lag warning (TS Events lags 24–48h behind live state, making
  the safety net leaky — candidates must be human-verified)
- Strengthen "INFORM-only" framing in recommendation note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* @W-23093467 @W-22551424: enforce stale-content row cap server-side + harden Admin Insights LUID cache (#553)

Part A (W-23093467): move the stale-content-cleanup safety cap from prompt-text
only to server-side enforcement. Add configurable STALE_CONTENT_MAX_ROWS
(default 100, range 1-10000) and enforce it in the stale-content branch of
query-admin-insights: above the cap the tool returns a structured success with
rows:[], the TRUE totalStaleItems/totalStaleSizeBytes, and a ROW_CAP_EXCEEDED
warning guiding the caller to narrow scope. Read-only inform prompt keeps working
(no throw); the destructive apply flow can no longer receive a huge actionable set
even if the model ignores the prompt guard (which remains as defense-in-depth).

Part B (W-22551424): light hardening of the existing Admin Insights LUID resolver
cache. Add an optional maxSize to ExpiringMap (default unbounded; other callers
untouched) with oldest-inserted eviction that clears the pending timeout, wire a
256-entry bound into the resolver cache, and add hit/miss/resolve debug telemetry
(no siteId/PII). Cache invalidation remains TTL + siteId-keying; an explicit
refresh hook is deferred.

Tests: below/at/above-cap + combined-warning cases, config precedence/validation,
ExpiringMap eviction, resolver eviction. Docs: env-vars, request-overrides,
site-settings, query-admin-insights tool page.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* @W-23336786 Add flexibility to PR title check (docs: prefix and skip-title-check label) (#455)

* @W-23336786 Add flexibility to PR title check (docs: prefix and skip-title-check label)

* Improve case insensitive title matching

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Avoid race condition around label creation

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Improve least-privilege for write permissions

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Exclude dependabot PRs

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* @W-23448423: add user-license-reclamation-apply prompt (HITL-guarded) (#518)

* @W-23448423: add user-license-reclamation-apply prompt

HITL-guarded apply prompt that chains 3A inform output into
update-user downgrades to Unlicensed. Follows the same two-phase
preview→confirm pattern as stale-content and extract-optimization
apply prompts. Defaults to dry run; no mutation without explicit
human approval at the Step 4 gate.

- src/prompts/userLicenseReclamation/apply.ts (prompt impl)
- src/prompts/userLicenseReclamation/apply.test.ts (23 tests incl.
  HITL-refusal adversarial cases)
- src/prompts/index.ts (registration)
- docs/docs/prompts/user-license-reclamation-apply.md
- docs/docs/intro.md (table row)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* @W-23448423: add deterministic VDS query templates for Steps 2-3

Self-review finding: Steps 2 (ts-events) and 3 (site-content) told
the model to call query-admin-insights but didn't provide VDS query
objects with exact field captions. The model would have to guess
Tableau-specific field names, causing runtime failure.

Now provides deterministic JSON query blocks (same pattern as
job-optimization-inform) with exact field captions and filters:
- Step 2: ts-events with Login/Login (Embedded) filter, LASTN date
- Step 3: site-content with Workbook/Datasource item-type filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* @W-23448423: align apply prompt with validated inform findings

- Fix TS Events field names (Actor User Name, Event Date — not Actor User ID / Event Created At)
- Use Access events instead of Login (catches embedded/API usage)
- Add all compound roles (SiteAdministratorCreator, SiteAdministratorExplorer) to defaults
- Cap rangeN at 90 days (TC standard lookback limit)
- Add limit: 10000 to prevent unbounded result sets
- Add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES env var support
- Handle null-lastLogin users (provisioned but never signed in)
- Add ETL lag and lookback cap caveats to Fixed notes
- Bump version to 3.5.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* @W-23448423: address review findings — Owner LUID, inactivity rule, docs

MAJOR fixes:
- Replace invalid `Owner LUID` caption with `Owner Email` (join by email)
- Fix Step 2 inactivity rule: "whose lastLogin from Step 1 is null" not
  "absent from Step 1"
- Docs: list all 6 license-consuming roles including compound variants

MINOR fixes:
- Clamp arg-path inactiveDays to max 3650 (matches env-path behavior)
- Docs: reword Step 2 from "last login date" to "activity signals"
- Docs: add LICENSE_RECLAIM_INACTIVE_DAYS / LICENSE_RECLAIM_ROLES config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prettier formatting for SITE_CONTENT_FIELDS array

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add lastLogin pre-filter, truncation warnings, and tighten regex

- Require lastLogin to be null or older than inactiveDays threshold
  before flagging a user as inactive (prevents false positives from
  ETL lag on recently-active users)
- Warn admin when TS Events or Site Content queries hit the 10k row
  limit (silent truncation could cause false-positive downgrades)
- Tighten inactiveDays regex to reject 3651-9999 at schema level
  (previously only clamped at runtime via Math.min)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prettier formatting for regex line break

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* @W-23518533: Group embed-viz and HITL MCP-App code into feature folders (#556)

* refactor: group embed-viz and HITL MCP-App code into feature folders

Split the flat src/web/apps/src/lib/ directory into feature-scoped folders so
each browser-side MCP-App bundle is self-contained:

- embed/  — Tableau viz embedding for get-view/get-workbook (mcp-app entry,
            handleToolResult, embedTableauViz, loadTableauEmbeddingApi,
            getEmbedTokenToolClient, openInTableauLink) + mcp-app.html
- hitl/   — in-iframe confirm panel for delete/update preview tools
            (hitl-confirm entry, handleConfirmResult, *ConfirmClient renderers)
            + hitl-confirm.html
- shared/ — mcp-app.css, showError, vite-env.d.ts, assets/, and a new
            vizContainer.ts holding TABLEAU_VIZ_CONTAINER_ID (extracted to
            keep shared/ free of a shared->embed dependency)

HTML templates move next to their entry .ts with relative <script src>. Each
Vite build's root is set to its feature folder in build.ts so single-file
output still lands flat as dist/mcp-app.html and dist/hitl-confirm.html;
appConfig.ts and server.web.ts are unchanged. All moves use git mv to preserve
history. 2365 unit tests pass, lint clean, build produces flat dist.

* chore: bump version to 3.4.1

* bump

* remove unused export

---------

Co-authored-by: Alon Siman Tov <asimantov@salesforce.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Akash Rastogi <akashrastogi12@gmail.com>
Co-authored-by: jarhun88 <34752438+jarhun88@users.noreply.github.com>
Co-authored-by: Joey Constantino <15789096+joeconstantino@users.noreply.github.com>
Co-authored-by: Jialin Mao <30403691+showmethecold@users.noreply.github.com>
Co-authored-by: Tony <3278731+tonythebest@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brian Cantoni <bcantoni@salesforce.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Akash Rastogi <akash.rastogi@salesforce.com>
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