# Changelog

> Released versions of servicenow-mcp-ai, generated from [CHANGELOG.md](https://github.com/IvanBBaev/servicenow-mcp-ai/blob/main/CHANGELOG.md) by `npm run docs:sync`. Unreleased changes are listed in that file.

## [3.1.1] - 2026-10-31

### Fixed

- The outbound `User-Agent` no longer carries `user=<user>` (added in 3.1.0); it is `servicenow-mcp-ai/<version> (node/<major>; <transport>)` again, naming neither the MCP client nor the user. `SN_USER_AGENT_SUFFIX` is unchanged. `tools/list`: +0 B.
- MCP Registry: the `server.json` remote is `https://{servicenow_mcp_ai_host}/mcp`; the registry refused 3.1.0 because `https://{host}/mcp` is already registered by another server.

## [3.1.0] - 2026-10-30

### Added

- Attachment file I/O (N-66): `servicenow_upload_attachment` accepts `file_path` — a file under `SN_DOCS_DIR` (the docs-store confinement, symlink check and journal-file refusal apply; the size is checked against `SN_MAX_UPLOAD_BYTES` before the file is read; the content type defaults by extension) — as the alternative to `content_base64`, and `servicenow_download_attachment` accepts `format: "file"`, which writes the bytes to `<profile>/exports/<name>-<stamp>.<ext>` and returns the path, size, sha256 and, for text-like content, a preview instead of base64. The write journal records the effective name, type and path, never the bytes. `tools/list`: +60 B per profile; the five attachment tool descriptions were shortened to pay for it.
- Schema authoring through the registry (N-70): `servicenow_upsert_artifact` with `artifactType: "table"` now writes a column's `internal_type`, `max_length`, `reference`, `column_label`, `mandatory`, `default_value`, `choice` and `attributes` on the `sys_dictionary` child, `label`, `plural` and `hint` on the `sys_documentation` child and `value`, `sequence` and `inactive` on the `sys_choice` child — a string field with its choices is one call. `internal_type` and `reference` take the glide-object / table name (verified on a PDI); `read_only` is read-only on the instance and stays out. `sys_db_object` and `sys_dictionary` join the protected tables: under the `confirm` default a write runs only when the call itself passes `apply: true` (artefact upserts already do), `deny` refuses it and an exact `SN_TABLES_ALLOW` entry lifts the rule. `tools/list`: +0 B.
- Event queue and three prompts (N-69): `servicenow_read_ops` gains `kind: "events"` over `sysevent` — the ready backlog and its oldest entry (all-time), the events fired in the window by state and by name (top 10) and, for the single kind, the newest rows (`name`, `table`, `state`, `queue`, `process_on`, `processed`, `processing_duration`, `claimed_by`, `user_name`, `parm1` / `parm2` clipped); the overview carries the section as counts only and an unreadable `sysevent` degrades alone. Three prompts: `servicenow_incident_rca` (`record`, `table`, `since`; the `history` package) walks audit trail → table logic → changes in the window → error log and events → SLAs; `servicenow_release_notes` (`update_set`, `audience`; `updatesets`) groups the set by type and action, explains each script and writes Markdown for a technical or business audience; `servicenow_clone_down_checklist` (`target`, `source`; `instance`) snapshots the target, reads the mail properties and `clone_data_preserver`, diffs source vs target and, after the clone, the target against its snapshot. `sysevent` and `clone_data_preserver` verified on a PDI. `tools/list`: +11 B on `all` / `all+tasks` / `all+legacy`, 0 B on `core` / `discovery`; the `read_ops` description was shortened to stay inside the budgets.
- Multi-row and file-sourced import (N-68): `servicenow_insert_import_set_row` takes `rows` (up to 500 column/value objects) or `file_path` — a `.csv` with a header row or a `.jsonl` with one object per line under `SN_DOCS_DIR` (the docs-store confinement, symlink check and journal-file refusal apply; `SN_DOCS_MAX_FILE_BYTES` is checked on the stat before the read; a BOM, CRLF and RFC 4180 quoting are handled; JSONL values may be strings, numbers, booleans or null) — as the alternatives to the single `values` row; exactly one of the three. Several rows go through `POST /api/now/import/{table}/insertMultiple` in one request; an instance without the endpoint (404 / 405) gets one request per row and the call stops at the first row that fails (`failed: { index, error, code }`, `sent` of `rows`). The result reports `mode`, `rows`, `sent`, the transform `results`, the distinct `import_sets` and the run and transform maps of the last import set; the plan preview shows `row_count`, `columns` and the first row. The write journal holds one envelope per call (row count, columns, the file's path and format, a sha256 of the rows; its write-cap weight is the row count, so `SN_MAX_WRITES_PER_SESSION` refuses the whole call before any request) and, in the sequential mode, one line per sent row under the envelope's `batch_id`. `insertMultiple` is verified on a PDI (OD-14); the fallback covers its absence. `tools/list`: +339 B on `all` / `all+tasks` and +348 B on `all+legacy` for the two parameters, 0 B on `core` / `discovery`, paid for by shortening four shared parameter descriptions (`update_set`, `lanes`, `wait_seconds` and the opt-in script-type note; no tool description changed, the offline eval is unchanged) — net −472 B on `all` / `all+tasks`, −537 B on `all+legacy`, −144 B on `core` / `discovery`.
- Basic-auth preflight (N-75): for a basic-auth profile, `servicenow-mcp-ai doctor` reads the instance's basic-auth restriction (`glide.authenticate.basic_auth.restriction.*`, verified on a PDI) and reports whether the connected user holds an allowed role (`snc_basic_auth_api_access` by default) or is in `allowed_users`; a `basic_auth` check appears in `doctor --json` and the support bundle. Informational — the verdict does not change; an unreadable restriction renders nothing.
- Resource polish (N-80): every prompt and every `ui://` app view carries the server icon (tools do not — ≈120 B per tool); `servicenow://reference/tools`, the `servicenow://reference/tools/{name}` template, `servicenow://reference/encoded-query` and `servicenow://policy` are annotated `audience: ["assistant"]`; docs entries in `resources/list` carry `size` and `annotations.lastModified` from the file on disk, and the index entry says how many documents the 100-entry cap left out; a capped schema list (`servicenow://schema/{table}`, `servicenow://{profile}/table/{table}/schema`) ends with a synthetic "N more — narrow with <template>" entry instead of cutting silently. A not-found resource error (`NOT_FOUND`, `UNKNOWN_PROFILE`, `INSTANCE_HTTP_404`) adds `uri` to its `data` beside `code` / `source` / `hint`; the wire code stays `-32602`. Cursor pagination of `resources/list` is deferred until a client asks. `prompts/list` and `resources/list` grow by the icon data URI per prompt / view; `tools/list`: +0 B.
- SSE resumability on stateful HTTP sessions (N-79): each session keeps an in-memory ring of its last 256 events (at most 256 KiB, oldest dropped, gone with the session), so a client whose SSE stream drops reconnects with `Last-Event-ID` and gets the missed messages; an id the ring no longer holds answers 400. Clients on protocol 2025-11-25 or later now see a priming event (an id with empty data) at the start of each stream. The stateless modern wire is unchanged. `tools/list` +0 B.
- Task progress (N-82): `tasks/get` on a background run shows its progress as `statusMessage` ("3/12 — incident", capped at 200 characters) — for `run_as_task` on the 2025-11-25 wire and for the tasks extension on 2026-07-28. Advertising `taskSupport` waits for `tools/list` headroom (OD-15). `tools/list` +0 B.
- `GEMINI.md` and Cursor rules (N-76, part 3): `npm run docs:instructions` also renders the root `GEMINI.md` from `AGENTS.md` (the same text as `.github/copilot-instructions.md`) and one auto-attached Cursor rule per area, `.cursor/rules/<area>.mdc` (the area's title as `description`, its `applyTo` globs as `globs`), from `.github/agent-instructions/`. A generated rule whose source is gone is removed; a hand-written one is kept. `-- --check` covers them. `tools/list` +0 B; `initialize` instructions +0 B.
- `servicenow-mcp-ai skills install --target agents|codex|cursor|gemini|claude|copilot` (N-76, part 2) copies the bundled skills into that agent's skills folder in the current directory, or in the home directory with `--global`; a skill already there is kept unless `--force`. `tools/list` +0 B; `initialize` instructions +0 B.
- Skills over MCP (N-76, part 1): the eight plugin skills ship in the npm package under `build/skills/` (`npm run build` copies them; the container image and the `.mcpb` bundle carry them too) and the server serves them through the Skills extension (SEP-2640, `io.modelcontextprotocol/skills`) — `skills/list` (one page; a cursor is `-32602`) returns each skill's verbatim frontmatter and every file with its `sha256` digest and size, plus `ttlMs` / `cacheScope` on the 2026-07-28 wire; `skills/get` returns one skill, an unknown URI is `-32602`; each file reads as `skill://<name>/<path>` with its exact bytes and each `SKILL.md` is in `resources/list`. The skills ride on the `docs` package: the extension is declared unless `docs` is denied, and the catalog is empty while `docs` is off. A skill that breaks a rule (name ≠ folder, no `SKILL.md`, over 512 files or 16 MiB) is left out with a warning. The Docker build stage now copies `tsconfig.build.json` (missing since N-85, so the in-image `npm run build` failed), `skills/` and the copy script; a test checks that the Dockerfile copies every input of `npm run build`. `tools/list` +0 B; `initialize` instructions +0 B.
- `server.json` (MCP Registry): `websiteUrl`, a `remotes` entry for a self-hosted HTTP deployment (`https://{host}/mcp` with a bearer header; there is no hosted instance) and, at publish time, the release bundle as an `mcpb` package — `scripts/registry-mcpb.mjs` appends it with its `fileSha256` in the registry workflow (N-91).

### Changed

- The outbound `User-Agent` no longer names the MCP client (`claude-code`, `vscode`, …): it is now `servicenow-mcp-ai/<version> (node/<major>; <transport>; user=<user>)`, where `user` is the ServiceNow user the request calls as — the OAuth subject of a delegated request, otherwise the profile's `SN_USER` — reduced to printable ASCII without `;`, `(`, `)` or a backslash and capped at 64 characters; it is left out when no user name is configured. `SN_USER_AGENT_SUFFIX` is unchanged. `tools/list`: +0 B.
- Record watch on the 2026-07-28 wire (N-83): a `subscriptions/listen` filter's `resourceSubscriptions` is dropped before the SDK sees it, so the acknowledgement no longer claims record updates that only the legacy wire delivers; `resources/subscribe` there stays `-32601`. Record watch is unchanged on 2025-11-25. `tools/list` +0 B.
- A 401 under basic auth (`INSTANCE_HTTP_401`) carries a hint naming the restriction property, the role and the OAuth alternative (N-75); `servicenow_test_connection` and `doctor` show it. The generic 401 hint names `servicenow_test_connection` and `doctor` instead of a tool that does not exist. `tools/list`: +0 B.
- MCP Apps views (N-84): each `ui://servicenow-mcp/…` resource declares `_meta.ui.csp` with empty `connectDomains` and `resourceDomains` — the document's own Content-Security-Policy at the host's level — instead of relying on the host's default. `tools/list`: +0 B.
- Build slimming (N-85): `npm run build` compiles through `tsconfig.build.json` (`removeComments`, LF newlines), which drops 504 KB (19.3 %) of shipped JavaScript (2,676,313 → 2,159,750 B); `project/FOOTPRINT.md` is measured by the new `npm run footprint`, and `npm run footprint -- --check` joins `npm run check` (ceiling and production dependency list exactly, byte figures within 5 %). The pack ceiling is untouched. `tools/list`: +0 B.
- Test speed, bucket eviction and the eval gate (N-89): the JSON Schema 2020-12 conformance test shares one Ajv instance per test and compiles each distinct schema once (22.7 s → 5.3 s, the example-call test 5.8 s → 1.8 s, measured locally); `createLogBridge` returns a `LogBridge` with `dispose()`, called when an HTTP session closes (`HttpSessionManager.close`, the 2026-07-28 per-request `onclose` chain), so a session's rate buckets and pending suppressed-lines summary go with it; CI runs the offline tool-selection eval (`node evals/tool-selection/run.mjs --max-drop 0`, lexical backend, no key) on the ubuntu / Node 22 leg — a description change that loses a single top-1 case against `baseline.offline.json` fails the leg. `tools/list`: +0 B.
- Gate tightening (N-87): the coverage ratchet is 97 / 88 / 97 (lines / branches / functions) on every CI leg (the Windows job comment now says so); `noImplicitOverride`, `noFallthroughCasesInSwitch`, `noImplicitReturns` and `allowUnreachableCode: false` are on; every error thrown under `src/{api,mcp,tools,core}` is a `ServiceNowError` with a code (28 sites, messages unchanged — ESLint bans a plain `throw new Error` there; the CLI entry points keep theirs); a bare `catch {` that swallows carries a `// swallowed: <reason>` comment, enforced by the local `swallowed-catch` rule (86 sites). `tools/list`: +0 B.
- SDK forward-compatibility (N-81): ESLint's `@typescript-eslint/no-deprecated` is an error under `src/`, and the deprecated SDK call sites are migrated — the 15 prompts declare `argsSchema` as a `z.object`, client capabilities, client info and the protocol revision are read from the per-request `_meta` envelope first (`src/mcp/client-identity.ts`, the one place that falls back to the 2025-era server accessors), the 2025-11-25 `Task` wire shape is the server's own type and the zod issue code is a string literal; the legacy-only `elicitInput` and MCP logging (deprecated by SEP-2577, in the spec for twelve more months) stay behind an inline disable that says why. `test/sdk-internals.test.js` pins the private SDK members the server reaches (`Protocol._requestHandlers`, `McpServer._registeredTools`, `Server.isMessageIgnored`); `wireModernTasks` logs an error instead of returning quietly when `SN_EXPERIMENTAL_TASKS=1` meets an SDK without them. The four SDK packages are pinned to exact versions (`server` 2.3.1, `node` 2.1.1, `client` 2.3.1, `sdk` 1.32.1), bumped in one deliberate change per minor. `tools/list`: +0 B.
- Docs truth (N-91): README, `docs/llms.txt` and the publish workflow link the `.mcpb` attached to every GA release (N-51 done); the extension README pins `@3.x`; the landing page names MCP SDK 2.3; `AGENTS.md` and the project README quote O-25 / B24; the extension walkthrough names the `discovery` default. Each string now has a test.

### Removed

- `smithery.yaml`, its README bullet and its container test: Smithery shuts down on 2026-10-31 (F5-21, OD-16). The container image itself is unchanged.

### Fixed

- Schema authoring (N-70): a `sys_dictionary`, `sys_documentation` or `sys_choice` child of a `table` artefact names its table by value (`name`), not by a reference, so `servicenow_upsert_artifact` refused every such child — a column with choices could not be written. A child linked by the parent's single key field is now written with that value (an encoded-query separator in it is refused); a child linked by a composite value is still refused, with a message that says so. Found and verified on a PDI (OD-14): a table, a string column and three choices were created in one plan, read back and reverted. `tools/list`: +0 B.
- A new child follows the scope of its new primary record (OD-14): a REST create lands in the session's current application, so a column of a table created with `sys_scope: "global"` was claimed by that application and its name got the scope prefix. When the primary record is created with `sys_scope`, the children created with it carry the same value. `tools/list`: +0 B.
- `servicenow_insert_import_set_row` warns when `insertMultiple` drops the values (OD-14): without an active `sys_rest_insert_multiple` record and its column mapping for the staging table, the instance creates the rows and leaves every column empty. After an `insertMultiple` call the tool reads that record and, when it is missing (or the read fails), adds a `warnings` entry; the transform-run warnings are kept beside it instead of being replaced. Verified on a PDI. `tools/list`: +0 B.

### Security

- `zod` floor raised to `^4.6.6` (installed 4.6.7): GHSA-mrhx-c7xc-m3w5, memory exhaustion while validating oversized constrained collections (high). No other package moved.

## [3.0.0] - 2026-10-19

### Breaking changes

These are the 3.0 changes the owner approved on 2026-10-01 (O-4). Each write-safety default has an explicit opt-out.

**Upgrading from 2.x in one step:** set **`SN_COMPAT=2`** (owner, 2026-10-14). Every compat setting you leave unset then takes its 2.x default: `SN_PROTECTED_TABLES_WRITE=allow`, `SN_DESTRUCTIVE_CONFIRM=off`, `SN_BATCH_UNMAPPED=allow`, `SN_LEGACY_TOOL_NAMES=1` and `SN_TOOL_PACKAGES=core`. A value you set explicitly always wins, and the `prod` profile still forces `elicit` confirmation. Unset (or `3`), the 3.0 defaults below apply. **`servicenow-mcp-ai migrate --write`** carries a 2.x env file over: it moves a project-root `.env` to the XDG file, renames a reserved profile (B13) `<name>_v2`, comments out values 3.x rejects (B5) and adds `SN_COMPAT=2`, and does the same for the `env` blocks of the server in Claude Desktop, Claude Code, Cursor, VS Code and Windsurf config files; every other setting stays as it was.

#### Migration at a glance

One row per breaking change, from the breaking register in `project/ROADMAP-V3.md`. The bullets below give the details.

| #   | Change                                                                                                                                                                                                            | Item | Migration                                                                                                                            |
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------ |
| B1  | Node ≥ 22.12                                                                                                                                                                                                      | E-1  | Upgrade Node; the launcher prints the requirement.                                                                                   |
| B2  | Tool renames + parameter normalization (`sys_id`, `table`, `fields`/`values`)                                                                                                                                     | M-7  | None for a 2.x client: old names answer as hidden aliases. Generated old→new table; `SN_LEGACY_TOOL_NAMES=1` also lists them.        |
| B3  | Error contract: `snDetail` → `detail`, new `code`; resources throw `McpError`                                                                                                                                     | M-2  | Update client parsers; codes documented in the manifest.                                                                             |
| B4  | Destructive `apply:true` needs a `plan_token` in plan mode (none → the preview); elicitation refusals fail                                                                                                        | H-3  | Call the tool without `apply` first and pass its `plan_token`; `SN_DESTRUCTIVE_CONFIRM=off` or `SN_WRITE_MODE=apply` for automation. |
| B5  | Fail-fast safety settings (tuning values still warn); project `.env` deprecated                                                                                                                                   | E-4  | `servicenow-mcp-ai migrate --write`, or fix the reported value and move `.env` to XDG / set `SN_ENV_FILE`.                           |
| B6  | HTTP: per-session profile; non-loopback bind refuses to start without `SN_HTTP_TOKEN`                                                                                                                             | H-7  | Set the token; call `use_instance` per session.                                                                                      |
| B7  | `set_credentials`: a new `instance` requires the auth material in the same call                                                                                                                                   | H-2  | Send `user` + `password` (or API key) with the host.                                                                                 |
| B8  | `batch`: unmapped `/api/*` prefixes denied by default                                                                                                                                                             | H-4  | `SN_BATCH_UNMAPPED=allow` or add the prefix to the map.                                                                              |
| B9  | Extension pinned to `servicenow-mcp-ai@^3`                                                                                                                                                                        | D-6  | None for users; publish the pinned extension first.                                                                                  |
| B10 | Dark Jira scaffold removed (ARCH-14 = NO-GO, 2026-10-09)                                                                                                                                                          | E-8  | None (no tools were exposed).                                                                                                        |
| B11 | Protected system tables need the call's own `apply: true` to write (`sys_user`, `sys_user_has_role`, `sys_security_acl`, …)                                                                                       | H-11 | Pass `apply: true`, list the table in `SN_TABLES_ALLOW`, or set `SN_PROTECTED_TABLES_WRITE=allow`.                                   |
| B12 | `use_instance` no longer persists `SN_ACTIVE_PROFILE` to the env file                                                                                                                                             | H-7  | Pass `persist:true`; set `SN_ACTIVE_PROFILE` in the env file for a permanent default.                                                |
| B13 | Profile resources move to `servicenow://profiles/{profile}/schema/{table}`; profile names `docs`, `schema`, `status`, `capabilities`, `reference`, `profiles`, `policy` become reserved                           | M-7  | Old template aliased for one minor; rename a reserved profile (`migrate` does it).                                                   |
| B14 | ATF tools merged: `list_atf_tests` / `list_atf_suites` → `servicenow_list_atf`, `run_atf_test` / `run_atf_suite` → `servicenow_run_atf` (`kind`: `test` \| `suite`) (O-4, 2026-10-13)                             | N-64 | Use the new names with `kind: "suite"` for suites; the old names still answer as hidden aliases.                                     |
| B15 | `servicenow_generate_fluent` and the Fluent emitters removed; Fluent generation belongs to syncrona (ADR 0010, 2026-10-10)                                                                                        | P-26 | Use `syncrona fluent transform` (or `now-sdk transform`) in the app's source repository.                                             |
| B16 | Cross-scope report: `restricted[].status` is a name (`requested` / `denied` / `allowed` / `invalidated`) instead of the raw integer choice (OD-7, 2026-10-13)                                                     | N-14 | Compare against the names; an unknown value still passes through raw.                                                                |
| B17 | UIB key fields renamed to the real columns: `ux_declarative_action_definition` `action_name` → `name`, `ux_declarative_action_payload` `key` → `action_key`, `uib_event` `name` → `event_name` (OD-1, 2026-10-13) | N-25 | Read the new key names; the old keys were always empty.                                                                              |
| B18 | `app_dependency` retargeted from the absent `sys_scope_dependency` to `sys_package_dependency_m2m`, `verified:true`, key `sys_package` + `dependency` (OD-4, 2026-10-13)                                          | P-2  | Read the new key and meta fields; the type listed nothing before.                                                                    |
| B19 | `sys_ux_form_action` has one descriptor: `uib_form_action` merged into `workspace_form_action` (`verified:true`), the old id kept as an input alias (O-4, 2026-10-13)                                             | N-34 | Read `workspace_form_action` where outputs named `uib_form_action`.                                                                  |
| B20 | `discovery` (`table`, `schema`, `aggregate` + admin) is the default tool profile; `core` (the 2.x default, with `attachment`) stays as before (O-4, 2026-10-13; eased 2026-10-14)                                 | N-63 | Set `SN_TOOL_PACKAGES=core` for the 2.x set, or add `attachment` / enable it with `servicenow_enable_package`.                       |
| B21 | `batch`: only `/api/sn_cicd/testsuite` and `/api/sn_cicd/progress` map to `atf`; other `sn_cicd` paths are unmapped, and ATF runs are refused on a production profile (O-4, 2026-10-13)                           | H-4  | Use the dedicated tools; `SN_BATCH_UNMAPPED=allow` lets unmapped paths through the table and read-only checks.                       |
| B22 | `sys_group_has_role` and `sys_user_role_contains` join the write-protected tables (O-4, 2026-10-13)                                                                                                               | H-11 | Pass `apply: true`, list the table in `SN_TABLES_ALLOW`, or set `SN_PROTECTED_TABLES_WRITE=allow`.                                   |
| B23 | `exports` map: only `servicenow-mcp-ai` (the entry point) and `servicenow-mcp-ai/package.json` resolve; `sideEffects` lists only the entry point (owner, 2026-10-14)                                              | D-6  | Run the `servicenow-mcp-ai` binary or import the package root; deep imports of `build/...` are not supported.                        |

- **B3: error contract v2 (M-2).** A failed tool result is now one flat JSON object: `{ error, code, source, status?, hint?, detail? }`.
  - `error` is the message (it was `error.message`). `code` is always present: a code from the table in `src/core/errors.ts` (`NOT_CONFIGURED`, `POLICY_DENIED`, `PLAN_REQUIRED`, `PLAN_EXPIRED`, `UNREADABLE`, `INSTANCE_HTML_RESPONSE`, `RECIPIENT_NOT_ALLOWED`, `CREDENTIALS_INCOMPLETE`, `CANCELLED`, `INVALID_INPUT`, `NOT_FOUND`, `INTERNAL_ERROR`, …) or `INSTANCE_HTTP_<status>` for a non-2xx instance answer that no more specific code covers.
  - `source` is new: `servicenow` (the instance answered with an error), `policy` (this server's policy or a confirmation step refused the call) or `server` (validation, transport, configuration, bugs).
  - `snDetail` is renamed `detail` (the instance's own error object). `hint` is the fix in one sentence. Policy denials now name the setting to change (`SN_TABLES_DENY`, `SN_TABLES_ALLOW`, `SN_PROTECTED_TABLES_WRITE`, `SN_READONLY`, `SN_IMPORT_SET_TABLES`, `SN_PACKAGES_DENY`, `SN_PACKAGES_READONLY`), and `doctor` reports a `hint` per failed check (`Fix:` in the text report).
  - A plan token whose TTL elapsed is now `PLAN_EXPIRED` (it was `PLAN_REQUIRED`). A 401 under Basic auth is `INSTANCE_HTTP_401` (token mode keeps `AUTH_EXPIRED`).
  - Messages no longer repeat the code as a prefix: `CREDENTIALS_INCOMPLETE: …` and the M-5 `UNKNOWN_PACKAGE: …` / `PACKAGE_DENIED: …` / `PACKAGE_ALWAYS_ON: …` messages now carry the code in `code` only; hints moved from the message into `hint`.
  - Resource reads (`servicenow://…`) throw an MCP `McpError` instead of returning a 200 body with an `error` field: a malformed URI, an unknown profile or a missing document is `InvalidParams` (-32602; the SDK has no ResourceNotFound), anything else `InternalError` (-32603). The error's `data` carries `{ code, source, hint? }`. The docs resource template no longer declares a fixed `text/markdown` type, since it also serves JSON companions; each listed entry and each read declares its own.
  - The tool manifest is now `manifestVersion: 3` and publishes the code table as a global `errorCodes` object (code → `{ source, description }`, including the `INSTANCE_HTTP_<status>` family). The table is global rather than per tool because most codes come from shared layers (HTTP, policy, configuration), so a per-tool list would over-promise.
  - Internals (ARCH-11b): `IntegrationError` is the neutral base class and `ServiceNowError` extends it (`JiraError` was deleted with B10).
  - **Migration:** read `result.error` as the message, not `result.error.message`; read `code`, `source`, `hint` and `detail` from the top level; replace `snDetail` with `detail`; branch on `code`, not on message text. Resource clients catch the JSON-RPC error and read `error.data.code`. Manifest consumers accept version 3.

- **B4: a destructive `apply:true` needs a `plan_token` (H-3).** `SN_DESTRUCTIVE_CONFIRM` now defaults to `token` (it was `off`). In plan mode (the default), the following calls run only when they carry the `plan_token` from their own plan preview. A call with no token at all is not refused: it returns that preview with a fresh `plan_token` and `apply_skipped`, and changes nothing (self-healing, eased for 2.x upgrades, owner 2026-10-14). A wrong, used or expired token is refused with `PLAN_REQUIRED` / `PLAN_EXPIRED` (428) before any request:
  - `servicenow_delete_record` and `servicenow_delete_attachment`;
  - `servicenow_batch` with a write sub-request;
  - `servicenow_send_email` and `servicenow_order_catalog_item`;
  - `servicenow_revert_write` and `servicenow_upsert_artifact`;
  - `servicenow_check_change_conflicts` (2.x: `servicenow_change_conflicts`) with `calculate:true`.

  A token can be used once and expires after `SN_PLAN_TOKEN_TTL_SEC` (600 s by default). `SN_WRITE_MODE=apply` still skips the token, except on a prod profile.
  - **Migration:** none for a client that shows the returned preview; otherwise call the tool without `apply` first, then repeat the same call with `apply:true` and the `plan_token` from that preview. To restore the 2.x behaviour, set `SN_DESTRUCTIVE_CONFIRM=off`, or set `SN_WRITE_MODE=apply` for trusted automation.

- **B8: `batch` denies unmapped `/api/*` paths and caps the request count (H-4).**
  - `SN_BATCH_UNMAPPED` now defaults to `deny`. A sub-request whose REST prefix maps to no package is refused with `POLICY_DENIED`.
  - `SN_BATCH_MAX_REQUESTS` now defaults to 50 (it was 1000). The cap is checked before any request is built.
  - **Migration:** set `SN_BATCH_UNMAPPED=allow` to forward unmapped paths. Raise `SN_BATCH_MAX_REQUESTS` (up to 1000) or split large batches.
- **B11: writes to protected system tables need explicit consent by default (H-11).**
  - `SN_PROTECTED_TABLES_WRITE` now defaults to `confirm` (eased from `deny` for 2.x upgrades, owner 2026-10-14). A create, update, delete, upsert, revert, import-set insert, CMDB write or batch write on one of these tables runs only when the tool call itself passes `apply: true`; under `SN_WRITE_MODE=apply` a call without it returns `POLICY_DENIED` with a hint to show the user the change and re-run with `apply: true`. Plan previews are unaffected. `deny` refuses every such write; `allow` is the 2.x behaviour. The tables: `sys_user`, `sys_user_has_role`, `sys_user_role`, `sys_user_grmember`, `sys_security_acl`, `sys_security_acl_role`, `sys_properties`, `oauth_entity`, `sys_auth_profile_basic`, `sys_script`, `sys_ws_operation`, `sys_public`, `sys_ldap*`, `sys_certificate`, `sys_data_source` and `sys_rest_message*`.
  - This means `servicenow_set_property` and business-rule writes need `apply: true` per call by default. Reads are not affected.
  - Attachments count as writes to the parent record's table. `servicenow_upload_attachment`, `servicenow_delete_attachment` and a batch `DELETE /api/now/attachment/<sys_id>` meet the same rule when the parent table is protected, and are refused when it is write-denied; reading an attachment stays a read.
  - **Migration:** pass `apply: true` on the call after the user agrees. To open one table without that step, add it as an exact entry in `SN_TABLES_ALLOW`. To restore the 2.x behaviour, set `SN_PROTECTED_TABLES_WRITE=allow`; `deny` is the strict option.
- **Import sets accept staging tables named `u_*` and `imp_*` by default (H-11).** `SN_IMPORT_SET_TABLES` used to be unrestricted and now defaults to `u_*,imp_*`. Scoped staging tables such as `x_acme_stage` are refused until you list them.
  - **Migration:** add the table's pattern to `SN_IMPORT_SET_TABLES`, or set it to `*`.
- **Write caps are on by default (H-11).** `SN_MAX_DELETES_PER_SESSION` defaults to 100 and `SN_MAX_BATCH_WRITES` to 50. `SN_MAX_WRITES_PER_SESSION` defaults to 500 for each HTTP session; stdio sessions stay uncapped. A call over a cap is refused with `WRITE_CAP` (429) and journaled.
  - **Migration:** set the variable to a higher number, or to `0` for no cap. A single batch is limited by `SN_BATCH_MAX_REQUESTS` (50) before it can reach the 50-write cap.

- **B2: tool naming convention v3 (M-7).** Every tool is now `servicenow_<verb>_<noun>`; `servicenow_aggregate`, `servicenow_batch`, `servicenow_where_used` and `servicenow_identify_reconcile` keep their names. Eleven tools are renamed (table below). The tool manifest is now `manifestVersion: 4`: it publishes the renames as `toolRenames`, and per tool the accepted parameter aliases (`legacyParams`, `deprecatedParams`) and the reason a tool overlaps another (`overlap`: `get_status` / `test_connection` / `check_capabilities`, `list_atf_tests` / `list_atf_suites`, all kept).

<!-- GENERATED:TOOL-RENAMES:BEGIN (npm run docs:readme) -->

_Generated from `TOOL_RENAMES` in `src/mcp/naming.ts` — run
`npm run docs:readme` after changing it._

| v2 name                            | v3 name                                | Why                                       |
| ---------------------------------- | -------------------------------------- | ----------------------------------------- |
| `servicenow_artifact_dependencies` | `servicenow_get_artifact_dependencies` | No verb.                                  |
| `servicenow_change_conflicts`      | `servicenow_check_change_conflicts`    | No verb.                                  |
| `servicenow_code_health`           | `servicenow_check_code_health`         | No verb.                                  |
| `servicenow_data_health`           | `servicenow_check_data_health`         | No verb; parallels check_code_health.     |
| `servicenow_docs_list`             | `servicenow_list_docs`                 | Noun before verb.                         |
| `servicenow_docs_read`             | `servicenow_read_doc`                  | Noun before verb.                         |
| `servicenow_docs_search`           | `servicenow_search_docs`               | Noun before verb.                         |
| `servicenow_docs_write`            | `servicenow_write_doc`                 | Noun before verb.                         |
| `servicenow_knowledge_highlights`  | `servicenow_get_knowledge_highlights`  | No verb.                                  |
| `servicenow_list_atf_suites`       | `servicenow_list_atf`                  | Merged with list_atf_tests (kind: suite). |
| `servicenow_list_atf_tests`        | `servicenow_list_atf`                  | Merged with list_atf_suites (kind: test). |
| `servicenow_ops_read`              | `servicenow_read_ops`                  | Noun before verb.                         |
| `servicenow_run_atf_suite`         | `servicenow_run_atf`                   | Merged with run_atf_test (kind: suite).   |
| `servicenow_run_atf_test`          | `servicenow_run_atf`                   | Merged with run_atf_suite (kind: test).   |
| `servicenow_table_logic`           | `servicenow_describe_table_logic`      | No verb.                                  |

<!-- GENERATED:TOOL-RENAMES:END -->

- Parameters are normalised. Record ids are `sys_id`: `attachment_sys_id` (get / download / delete attachment), `item_sys_id` (get / order catalog item), `catalog_sys_id` (list catalog categories), `test_sys_id` (run ATF test) and `suite_sys_id` (run ATF suite) are renamed. The table is `table`: `staging_table` (import set tools) and `class_name` (CMDB tools) are renamed. `fields` now always means the list of columns to return; a write payload is `values`: `fields` on `create_record`, `update_record`, `upsert_record`, `create_change`, `update_change`, `insert_import_set_row` and `upsert_artifact` (top level and `children[]`), and `attributes` on `create_ci` / `update_ci`, are renamed. `run_atf_*` keep `execution_id` (a CI/CD progress id, not a record).
- `class_name` stays accepted on the CMDB tools as a documented deprecated alias of `table`. Passing both an alias and its new name is refused with `INVALID_INPUT`.
- **The old tool names keep working (eased for 2.x upgrades, owner 2026-10-14).** Every old tool name is registered as an alias that runs the new tool and accepts the old parameter names (one warning per name, and per tool and parameter). By default the aliases are hidden: they answer `tools/call` but stay out of `tools/list` (+0 B on every profile). **`SN_LEGACY_TOOL_NAMES=1`** (default off) also lists them in `tools/list` with a deprecated title and accepts the old parameter names on the new tool names. Without it, an old parameter name on a new tool name fails validation.
- **Migration:** none for a client that calls the old names. Rename the tools and parameters in your prompts, scripts and client allow-lists using the table above; a client that allow-lists tools from `tools/list` needs `SN_LEGACY_TOOL_NAMES=1` while you migrate. Manifest consumers accept version 4.
- **B13: per-profile resources move under `servicenow://profiles/` (M-7).**
  - The per-profile schema template is now `servicenow://profiles/{profile}/schema/{table}`. The old `servicenow://{profile}/schema/{table}` still reads the same schema until the next minor release, logs a deprecation warning once and is no longer listed.
  - Profile names `docs`, `schema`, `status`, `capabilities`, `reference`, `profiles` and `policy` are reserved: creating one, or loading an env file that defines one, fails with `RESERVED_PROFILE_NAME` and a hint.
  - **Migration:** read `servicenow://profiles/<profile>/schema/<table>`; rename a profile that uses a reserved name (its `SN_<NAME>_*` variables).
- **B15: Fluent generation is removed (P-26 … P-29, owner decision 2026-10-10).** `servicenow_generate_fluent` (opt-in `artifacts` package, now five tools), the `src/api/fluent*.ts` emitters, `npm run fluent:verify`, `npm run fluent:actions` and the `@servicenow/sdk` dev dependency are gone; `tools/list` shrinks by about 2 KB on `all`. Fluent generation and source control belong to the syncrona CLI. The SDK-managed scope guard (P-3, P-22) and the registry's `sdkApi` stay. **Migration:** use `syncrona fluent transform` (or `now-sdk transform`) in the app's source repository. See `project/adr/0010-drop-fluent-generation.md`.
- **B10: the dark Jira scaffold is deleted (E-8, O-1 NO-GO, owner decision 2026-10-09).** `src/core/jira/`, `src/api/jira/` and the `JiraError` class are gone; `ServiceNowError` is now the only `IntegrationError` subclass. No tool, setting or documented export referred to Jira, so there is nothing to migrate. See `project/adr/0007-delete-jira-scaffold.md`.
- **B16: cross-scope restricted caller status is a name (N-14, owner decision 2026-10-13, OD-7).** In the cross-scope access report (`document_app` and the security section), `restricted[].status` now maps the `sys_restricted_caller_access.status` integer choice to `requested` (0), `denied` (1), `allowed` (2) or `invalidated` (3); it carried the raw `"2"` before. An unknown value passes through unchanged. **Migration:** compare against the names instead of the digits.
- **B17: UIB key fields name the real columns (N-25, owner decision 2026-10-13, OD-1).** The registry key of `ux_declarative_action_definition` is `name` (was `action_name`), of `ux_declarative_action_payload` `action_key` (was `key`) and of `uib_event` `event_name` (was `name`). The old names have no `sys_dictionary` row on these tables, so their keys were always empty. **Migration:** read the new key names in artefact listings and explanations.
- **B18: `app_dependency` reads `sys_package_dependency_m2m` (P-2, owner decision 2026-10-13, OD-4).** The registry pointed `app_dependency` at `sys_scope_dependency`, which does not exist on an instance, so the type always listed nothing. It now reads `sys_package_dependency_m2m` (checked with GETs on a PDI) and is `verified:true`: the key is `sys_package` + `dependency` (was the record `sys_id`), the meta fields are `sys_package`, `dependency` and `min_version`, and a `scope` filter matches `sys_package` (an app sys_id) or `sys_package.source` (a scope namespace). `dependency` may name an app or a plugin. **Migration:** read the new key and meta fields in artefact listings and explanations.
- **B19: one descriptor for `sys_ux_form_action` (N-34b, owner decision 2026-10-13, O-4, UX-24).** The registry described the table twice, as `workspace_form_action` (classic-ui) and `uib_form_action` (uib). `uib_form_action` is gone from the registry; `workspace_form_action` is the one descriptor and is `verified:true` (checked with GETs on a PDI). `uib_form_action` stays accepted as an alias wherever a type is an input (`list_artifacts`, `get_artifact`, `explain_artifact`, `compare_instances` `types`, …) and resolves to `workspace_form_action`; the `servicenow://artifact-types` catalogue gains an `aliases` map. Outputs change: the catalogue and the discovery artefact document list 143 types instead of 144, `compare_instances` `all` and where-used reverse types name `workspace_form_action` only. tools/list delta 0 B. **Migration:** read `workspace_form_action` where outputs named `uib_form_action`.
- **B20: `discovery` is the default tool profile (N-63 / N-36b, owner decision 2026-10-13, O-4, O-10 (b)).** With `SN_TOOL_PACKAGES` unset the server now loads the `discovery` profile: `table`, `schema` and `aggregate`, plus the always-on admin tools, among them `servicenow_find_tools`, which names the package for any other tool. `core` stays accepted and, eased for 2.x upgrades (owner, 2026-10-14), names the 2.x default set again: discovery plus `attachment` (tools/list: `core` 27,070 B, 25 tools; `discovery` unchanged). The five `attachment` tools (`list_attachments`, `get_attachment`, `download_attachment`, `upload_attachment`, `delete_attachment`) are no longer on by default. The VS Code extension offers `discovery` in its package picker. tools/list delta: default (`discovery` / `core`) 27,084 → 23,689 B (−3,395 B, 25 → 20 tools); `all` +0 B. **Migration:** set `SN_TOOL_PACKAGES=core` to keep the 2.x surface, add `attachment` to `SN_TOOL_PACKAGES` (for example `discovery,attachment`), or call `servicenow_enable_package` with `attachment` for the session.
- **B14: the ATF list and run pairs merge (N-64, owner decision 2026-10-13, O-4, O-10).** `servicenow_list_atf_tests` and `servicenow_list_atf_suites` become `servicenow_list_atf`; `servicenow_run_atf_test` and `servicenow_run_atf_suite` become `servicenow_run_atf`. Both take `kind` (`test`, the default, or `suite`); `run_atf` takes `sys_id` (the old `test_sys_id` / `suite_sys_id` stay accepted as parameter aliases), and `list_atf` returns `tests` or `suites` as before. The four old names stay as aliases that add the matching `kind` (hidden from `tools/list` unless `SN_LEGACY_TOOL_NAMES=1`). Legacy alias descriptions no longer repeat the tool description, only the replacement name. The package and local-docs pairs were considered and kept (always-on admin tools with a high blast radius; different result shapes). The tool-selection eval is neutral (`all` 32.9 % → 32.9 %). tools/list delta: `all` 121,271 → 119,543 B (−1,728 B, 100 → 98 tools), `all+tasks` −1,905 B, `all+legacy` +1,454 B (four new aliases), default (`discovery` / `core`) +0 B. **Migration:** call `servicenow_list_atf` / `servicenow_run_atf`, adding `kind: "suite"` for suites; the old names keep working as hidden aliases.
- **B22: group role grants and role containment are write-protected (H-11, owner decision 2026-10-13, O-4, ADR 0011).** `sys_group_has_role` (a role granted through a group) and `sys_user_role_contains` (the roles a role contains) join the protected tables, so creates, updates, deletes and batch writes on them need the call's own `apply: true` under the `SN_PROTECTED_TABLES_WRITE=confirm` default (`POLICY_DENIED` under `deny`). Reads are not affected. tools/list delta: +0 B. **Migration:** pass `apply: true`, list the table in `SN_TABLES_ALLOW`, or set `SN_PROTECTED_TABLES_WRITE=allow`.
- **B23: the package exports only its entry point (D-6, owner decision 2026-10-14).** `package.json` gains an `exports` map with `.` (`build/index.js`) and `./package.json`, so deep imports such as `servicenow-mcp-ai/build/cli.js` fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`. `sideEffects` lists only `build/index.js`, which runs the CLI when it is imported. The `servicenow-mcp-ai` binary, `npx servicenow-mcp-ai` and the VS Code extension are not affected. tools/list delta: +0 B. **Migration:** run the binary or import the package root; the build layout is not a public API.
- **B21: `servicenow_batch` reaches only the ATF part of the CI/CD API, and ATF runs refuse a production profile (H-4, owner decision 2026-10-13, O-4, ADR 0011).** Only `/api/sn_cicd/testsuite` and `/api/sn_cicd/progress` map to the `atf` package; every other `sn_cicd` path (update set commit, app install and publish, plugin activation) is unmapped and refused in a batch under the `SN_BATCH_UNMAPPED=deny` default (B8). `servicenow_run_atf` and a batched ATF run return `POLICY_DENIED` on a profile marked prod (`SN_ENV=prod`) before any request. tools/list delta: +0 B. **Migration:** call those APIs through a dedicated tool or set `SN_BATCH_UNMAPPED=allow`; run ATF against a test or dev profile.
- **B5: invalid safety settings stop the server, and the project-root `.env` is deprecated (E-4, owner decision 2026-10-13, O-4, ADR 0011).** An invalid value of a safety setting (connection, profiles, packages, policy, transport, and the trust / privacy keys `SN_ALLOWED_HOSTS`, `SN_TLS_*`, `SN_REDACT_*`, `SN_CSV_FORMULA_GUARD`) is a startup error that names every offending key. An invalid tuning value (timeouts, caps, caches, logging) keeps the 2.x behaviour: a logged warning, and the default applies (eased for 2.x upgrades, owner 2026-10-14). `SN_STRICT_SETTINGS=1` makes every invalid value an error, `0` none; unknown `SN_*` keys stay warnings. The env file is `SN_ENV_FILE` or `~/.config/servicenow-mcp-ai/.env`; a `.env` next to the installed package is still read when neither exists, as a deprecated fallback with a startup warning that names where to move it (eased for 2.x upgrades, owner 2026-10-14). tools/list delta: +0 B. **Migration:** fix the reported value, or set `SN_STRICT_SETTINGS=0` to warn and keep the defaults for safety settings too; when warned, move the project `.env` to `~/.config/servicenow-mcp-ai/.env` or point `SN_ENV_FILE` at it.
- **B6: the HTTP transport refuses a non-loopback bind without a token (H-7, owner decision 2026-10-01, O-4, ADR 0001).** `SN_HTTP_REQUIRE_TOKEN` now defaults to on: binding `SN_HTTP_HOST` to anything but loopback without `SN_HTTP_TOKEN` / `SN_HTTP_TOKEN_FILE` or `SN_HTTP_OAUTH_*` is a startup error instead of a warning. The container image binds `0.0.0.0`, so it needs a token too. stdio is unchanged. tools/list delta: +0 B. **Migration:** set `SN_HTTP_TOKEN` (or OAuth), bind `127.0.0.1`, or set `SN_HTTP_REQUIRE_TOKEN=0` to keep the open bind.
- **B12: `servicenow_use_instance` no longer writes the env file unless asked (H-7, owner decision 2026-10-01, O-4, ADR 0001).** Over stdio a switch now lasts until the process exits, as it already did per session over HTTP; `persist: true` writes `SN_ACTIVE_PROFILE` (journaled) on either transport. tools/list delta: −14 B. **Migration:** pass `persist: true`, or set `SN_ACTIVE_PROFILE` in the env file for a permanent default.

### Added

- **A 2.x setup that was never migrated is pointed at `migrate`.** While `SN_COMPAT` is unset, the one-time warning for a call under a v2 tool name or a read of the v2 `servicenow://{profile}/schema/{table}` URI now ends with the fix: run `servicenow-mcp-ai migrate --write`, or set `SN_COMPAT=2`. The README opens with an "Upgrading from 2.x?" note.
- **`servicenow-mcp-ai migrate`: 2.x settings carry over (owner, 2026-10-14).** The command reads the env file the server resolves (or `--from <file>`) without loading it, so a 2.x file that 3.x refuses at startup still migrates, and prints each change; `--write` applies them. It copies a project-root `.env` (B5) to `~/.config/servicenow-mcp-ai/.env` or `SN_ENV_FILE`, renames a reserved profile (B13) to `<name>_v2` with all its keys and `SN_ACTIVE_PROFILE` (its docs and write-journal folder under `SN_DOCS_DIR` follows, never over an existing `<name>_v2` folder), comments out each value 3.x rejects at startup (the default applies, as in 2.x), rewrites a dotenv `KEY: value` line (3.x skips it) as `KEY=value`, and appends `SN_COMPAT=2` unless the file sets it or `--v3-defaults` is given. Comments and every other key and value stay; a file rewritten in place is backed up as `<file>.v2.bak` (`0600`). The same rules apply to an `env` block with `SN_*` keys in a client config that launches this server (Claude Desktop, `~/.claude.json` and `.mcp.json`, Cursor, VS Code `mcp.json`, Windsurf), where an invalid value is removed; a file with comments is reported for a hand edit, and a VS Code `envFile` is named for `migrate --from`. `SN_COMPAT=2` now also makes an invalid value a warning (`SN_STRICT_SETTINGS=0`) unless `SN_STRICT_SETTINGS` is set, and the startup errors for a reserved profile, an invalid value and the project-root notice name the command.
- **N-43: `sn-upgrade` and `sn-security` flagship skills.** The plugin now ships eight workflow skills. `sn-upgrade` reviews an upgrade: the history, the unresolved skips and their classification, store updates and the upgrade document (`servicenow_review_upgrade`, `servicenow_document_instance`). `sn-security` is read-only: it explains one user's access to a table (`servicenow_explain_access`), runs the ACL scan and hardening checks and documents the security posture. Each has a prompt twin (`servicenow_upgrade_review`, `servicenow_security_posture`), a trigger and a negative eval case and recorded mocks; the fake eval instance gained roles, ACLs and an upgrade with two unresolved skips.
- **N-24: grant and revoke one user role (O-17, opt-in `access` package).** `servicenow_change_access` grants or revokes one role for one user as a direct `sys_user_has_role` row. Both actions are destructive (plan_token under `SN_DESTRUCTIVE_CONFIRM`); the plan shows the effective roles before and after and is refused when the user's roles changed after it. Privileged roles (`admin`, `security_admin`, `maint`, `elevated_privilege` roles and every role containing one) and inherited roles are always refused. `sys_user_has_role` stays protected: a write also needs `SN_TABLES_ALLOW` or `SN_PROTECTED_TABLES_WRITE=allow`.
- **N-4: preview and commit retrieved update sets (O-11, opt-in `cicd` package).** `servicenow_commit_update_set` runs `step: "preview"` or `step: "commit"` on a `sys_remote_update_set` through the CI/CD API. A commit is destructive (plan_token under `SN_DESTRUCTIVE_CONFIRM`), needs a clean preview — state `previewed` and no unresolved preview problem — is never forced, and is refused when the preview changed after the plan. `wait_seconds` polls the job; a finished preview reports its problems. `servicenow_batch` never runs an update set preview or commit.
- **N-3: read only the Instance Scan findings.** `instance_scan: true` on `servicenow_check_code_health` returns the latest Instance Scan result, its findings by priority and artefact type and the `agreed` set, without the full code-health run (no inventory, ACL scan, hardening, baseline or report file). With a table `scope` that table is linted to fill `agreed`; instance-wide `agreed` stays empty and a warning says so. It cannot be combined with `scan_run` (INVALID_INPUT). `tools/list` delta: +91 B on `all` and `all+tasks`, +182 B on `all+legacy` (within budget on `all` and `all+tasks`; the `all+legacy` budget raised one 256 B step, O-10); `core` unchanged.
- **D-3: release notes on the documentation site.** `npm run docs:sync` now writes `docs/changelog.md` (the released versions from CHANGELOG.md; Unreleased stays in this file, so a new entry does not drift the page) and the footer release links of `docs/index.html` (the changelog page and the five newest version tags); `docs/llms.txt` links the page, and `test/changelog-docs.test.js` guards both.
- **N-17: per-user ServiceNow tokens over HTTP (O-15 phase 2).** With `SN_HTTP_OAUTH_USER_TOKENS`, `SN_HTTP_OAUTH_TOKEN_KEY` and an instance OAuth client (`SN_OAUTH_CLIENT_ID` / `SN_OAUTH_CLIENT_SECRET`), each OAuth subject links its own ServiceNow account once: the first call returns a URL elicitation to the instance's authorize page (Authorization Code + PKCE; `-32042` on a 2025 session, an error carrying the link without URL elicitation or in a background task), the callback at `/oauth/servicenow/callback` verifies the account against the token's claim (`SN_HTTP_OAUTH_USER_MATCH`, default `email=email`), and the refresh token is stored AES-256-GCM encrypted (`SN_HTTP_OAUTH_TOKEN_STORE`). Later calls reach the instance as that user, refresh on a 401 and rotate the stored token; schema caches are per subject. Off by default. A subject drops its own links with `DELETE /oauth/servicenow/link` (its bearer; `{ unlinked, revoked }`), which also revokes the refresh tokens on the instance (`/oauth_revoke_token.do`, best effort). Replicas that share `SN_HTTP_OAUTH_TOKEN_KEY` and the store can each complete a link: the OAuth `state` carries the pending link sealed with a key derived from `SN_HTTP_OAUTH_TOKEN_KEY`, and the PKCE verifier is derived from its nonce. Known limit: `SN_HTTP_TOKEN` requests keep the shared credential. Conformance: 18 URL mode `mrtr` rows covered, pending down to 2. `tools/list` delta: 0 B.
- **N-17: the HTTP transport as an OAuth 2.1 resource server (O-15 phase 1).** With `SN_HTTP_OAUTH_ISSUER` and `SN_HTTP_OAUTH_RESOURCE`, the server publishes RFC 9728 protected-resource metadata, answers 401/403 with RFC 6750 `WWW-Authenticate` challenges (`resource_metadata`, `scope`), and validates JWT access tokens locally against the issuer's JWKS (discovered or `SN_HTTP_OAUTH_JWKS_URI`): asymmetric signature, issuer, audience bound to the resource (RFC 8707, plus `SN_HTTP_OAUTH_AUDIENCE`), expiry and `SN_HTTP_OAUTH_SCOPES`. The token's subject owns its 2025 session and its 2026-07-28 plan tokens, and is recorded on write-journal lines. The token is never forwarded: the instance is still called with the configured credentials. `SN_HTTP_TOKEN` keeps working alongside. Opaque access tokens are accepted through the issuer's RFC 7662 introspection endpoint when `SN_HTTP_OAUTH_INTROSPECTION_URI` (with `SN_HTTP_OAUTH_INTROSPECTION_CLIENT_ID` / `SN_HTTP_OAUTH_INTROSPECTION_SECRET`, `client_secret_basic`) is set: the answer must be `active` and pass the same audience, expiry, subject and scope checks; an active answer is reused for at most 60 s and never past its `exp`, and an unreachable endpoint is a 503. Conformance: the 35 `auth` rows are covered (one n/a). `tools/list` delta: 0 B.
- **N-3: run an Instance Scan suite over scoped apps or update sets.** `scan_run: "apps"` / `"update_sets"` on `servicenow_check_code_health` take `sys_id` (a `scan_check_suite`) and `targets` (1–100 app scope or update set sys_ids; duplicates dropped) and start the suite through `suite_scan/{suite}/scoped_apps` / `/update_sets`, with the same plan → apply, journal, `SN_READONLY` and prod refusal as the other runs. The table policy checks `sys_scope` / `sys_update_set`, also for those routes in `servicenow_batch`. Route and body keys match the CICD Spoke REST steps on a PDI. `tools/list` delta: +202 B on `all` (within budget) and `all+tasks`, +404 B on `all+legacy` (budgets raised one 256 B step, O-10); `core` unchanged.
- **N-3: run an Instance Scan from `servicenow_check_code_health`.** `scan_run` (`full`, `point` with `scope` + `sys_id`, `suite` with `sys_id`) previews the scan as a plan; `apply: true` starts it through the CI/CD Instance Scan API and journals the write. A missing or malformed target is `INVALID_INPUT`; `SN_READONLY` refuses it. Without `scan_run` the report is unchanged. A profile marked prod (`SN_ENV=prod`) is refused with `POLICY_DENIED`, before the plan and whatever `SN_PROD_WRITES` says. Endpoints are unverified until O-5. `tools/list` delta: +126 B on `all` (within budget) and `all+tasks`, +252 B on `all+legacy` (budgets raised, O-10); `core` unchanged.
- **N-28: UI Builder paths named in tool descriptions.** `servicenow_get_artifact_dependencies` names UIB components, data brokers and page callers; `servicenow_where_used` says a UIB component, macroponent or broker sys_id is a `script` target; `servicenow_get_update_set` says touched UIB pages list the records the set lacks. Behaviour is unchanged. `tools/list` delta: +121 B on `all` and `all+tasks`, +159 B on `all+legacy` (budgets raised, O-10).
- **N-26: `detail` on `servicenow_explain_ui_experience`.** An optional list of depth levels — element props and components, data bindings, event → handler → target chains, script bodies — returns the UIB page explanation at that depth inline in `json` or `markdown`, not only through `format: "file"`. Without it the inline output is unchanged; `format: "file"` still writes every level unless `detail` narrows it. Shapes are unverified until O-5. `tools/list` delta: +222 B on `all`, `all+tasks` and `all+legacy` (budgets raised, O-10); `core` unchanged.
- **N-65: file deliveries link without the docs package.** A `format:"file"` result now carries a `resource_link` to the new always-on `servicenow://exports/{+path}` resource when the docs package is off (the default profile); with docs on it still links `servicenow://docs/…`. The exports resource reads file deliveries only (`.json`, `.jsonl`, `.csv`, `.mmd`, …), lists nothing, and is off only when docs is denied (`SN_PACKAGES_DENY=docs`). `tools/list` delta: 0 B.
- **N-31: `raw` on `servicenow_compare_instances`.** With `types`, `raw: true` adds `rawDiff` to each changed UIB composition: the unified diff of the pretty-printed composition JSON, next to the element diff (at most 50), and the report gains a `Composition JSON diffs` section. Without it the output is unchanged. `tools/list` delta: +101 B on `all`, `all+tasks` and `all+legacy` (budgets raised, O-10).
- **N-29: GraphQL resolver scripts in script intelligence.** `graphql_resolver` (`sys_graphql_resolver`) and `graphql_type_resolver` (`sys_graphql_typeresolver`) are new opt-in script types: `list_scripts`, `get_script`, `lint_script` and `search_code` (`extended:true`) read the resolver scripts behind GraphQL data brokers when asked. The default sweep, snapshot and compare are unchanged. Tables are unverified until O-5. `tools/list` delta: +295 B on `all`, `all+tasks` and `all+legacy` (budgets raised, O-10).
- **N-2: `servicenow_explain_access`.** Why a user can or cannot read, write, create or delete a table, record or field: the user's roles, the matching row and field ACLs with their role, condition and script parts (scripts stay `undetermined`; conditions run as the connected user), admin overrides and the domain caveat. `format: "markdown"` adds the Mermaid decision diagram. It sits in the opt-in `directory` package, so `SN_PACKAGES_DENY=directory` removes it with the other user-data reads (O-13). Tables and fields are unverified until O-5. `tools/list` delta: +1355 B on `all`, `all+tasks` and `all+legacy` (budgets raised, O-10); `core` unchanged.
- **N-12: `domain` scope on `servicenow_query_table`.** An optional domain `sys_id` narrows a read to that
  domain: `sys_domain=<sys_id>` is AND'd into every `^NQ` segment of the encoded query, so an OR'd query cannot
  escape it. Without it the query and the result are unchanged. Pair it with `queryNoDomain` to read a domain other
  than your own (platform behaviour unverified until O-5). Some `query_table` input descriptions were shortened to keep the tool under the per-tool size cap;
  tools/list grows by 8 B on every profile.
- **P-7: five more artefact types from the SDK guide audit.** `assignment_rule` (`sysrule_assignment`),
  `knowledge_base` (`kb_knowledge_base` with its four `kb_uc_*_mtom` user-criteria children), `security_attribute`,
  `security_data_filter` and `formatter` (`sys_ui_formatter`). The registry now has 142 types. Tables and fields are
  unverified until O-5. CORE-20 (table augments) has no type of its own: augment columns stay columns of the `table`
  type. tools/list: +0 B.
- **N-7: the `i18n` kind of `servicenow_document_app`.** With `kind: "i18n"` (and an optional `language`) the tool
  writes `i18n/<scope>.md` + `.json` instead of the app document: the scoped app's translation coverage per language
  over UI messages, field labels, choices, translated text, translated fields and UI Builder strings, with the missing
  keys per category. Without `language`, every active `sys_language` except the base language is measured. Table and
  field names are unverified until O-5. tools/list: all +227 B, core +0 B (owner: one 256 B step on `all`).
- **N-18: repeats and a paired comparison in the tool-selection eval.** `npm run eval:tools -- --repeats <n>` runs
  every case n times and scores the majority answer. The report lists each run's top-1, and the baseline keeps
  `correctRuns` / `runs` per case. Against a baseline the comparison prints an exact two-sided McNemar p-value over
  the cases that flipped, and `accepted` or `REJECTED` under the acceptance rule (top-1 drops by at most 4 points).
  The owner chose the model baseline (O-10 (d)): `claude-sonnet-5-5` with three repeats. Outside the gate;
  `tools/list` +0 B.
- **P-7: the `interceptor` artefact type (SDK 4.13 `Interceptor`).** The registry gains the classic-UI type `interceptor` over `sys_wizard`, with its answers (`sys_wizard_answer`, script field `script`) as children and the back / next panels as references. It is `verified:false` until O-5 confirms the tables; `sys_wizard_choice` rows stay plain `Record` calls, as in the SDK. tools/list delta: 0 B.
- **N-37: an example call per tool in `servicenow://reference/tools/{name}`.** Each tool's reference now carries
  `example: { summary, arguments }` — one call its input schema accepts, with placeholder sys_ids (`ID`, `ID2`); write
  tools show the plan preview, never `apply`. The examples live in `src/mcp/tool-examples.ts`, and a test fails when a
  tool has no example or its example no longer validates. `tools/list` +0 B on every profile.
- **N-1: the `upgrade` document kind and the `servicenow_upgrade_review` prompt.** `servicenow_document_instance`
  gains `upgrade` (`upgrade.md`, needs the `docs` and `instance` packages): the upgrade history, the unresolved skipped
  records of the newest upgrade grouped by application and artefact type, and the store apps with an update. The
  `servicenow_upgrade_review` prompt (listed with the `instance` package, optional `upgrade`) walks the history, the
  skips, one skip's base vs customer versions and the store updates, then the document. tools/list: all +10 B, core +0 B.
- **N-15: `explain` on `servicenow_query_table`.** With the flag no records are read: the encoded query is explained
  against the table chain's indexes (`sys_index`) — which conditions can use an index, cost notes for `ORDERBY`, `LIKE`
  and scans, the index count and a cached row estimate. Advice from the usual index rules, not ServiceNow's planner.
  tools/list: +136 B on every profile (core included; owner-approved core step to 26,880 B).
- **N-22: `role_history` on `servicenow_lookup_directory` and the `access_review` document kind.** With `role_history`
  (kind `user`, one match) the result gains the user's role grants and revokes of the last 90 days, newest first.
  `servicenow_document_instance` gains `access_review` (`access-review.md`): privileged roles, their holders with the
  grant path, dormant accounts and recent revokes. It holds account data, so it carries its own caveat and follows the
  `directory` package (`SN_PACKAGES_DENY=directory` refuses it before any read). tools/list: all +170 B, core +0 B.
- **N-16: `with_results` on `servicenow_list_atf_tests` and `servicenow_list_atf_suites`.** With the flag, each test or
  suite gains its last result, its pass rate over the last 20 runs and a `flaky` flag (edits to the test restart the flip
  count); a `history` section reports the window, the rows scanned, truncation and the definitions check, or why the
  history is unavailable (up to 100 items). Without the flag the output is unchanged. tools/list: all +294 B, core +0 B.
- **N-1: `servicenow_review_upgrade` in the opt-in `instance` package.** The upgrade history by default; with `upgrade`
  the unresolved skipped records of that upgrade grouped by application and artefact type; with `update_name` one
  skipped record classified by its base and customer versions (with field diffs); with `store_updates` the store apps
  that have an update and their customised artefacts. tools/list: all +808 B, core +0 B.
- **N-5: `servicenow_get_task_context` in the opt-in `history` package.** For one task (sys_id or number) it returns the
  assignment, approvals and task SLAs (breach flag, time left), optionally recent journal entries; with `pending_for`
  it lists an approver's requested approvals instead. tools/list: all +859 B, core +0 B.
- **N-36: `servicenow_find_tools` in the always-on admin surface.** A client on `core` can search every tool the session could expose, loaded or not, by intent (`query`, optional `limit` up to 25). Each match carries its `package`, `readOnly` and `enabled` state, and when a match's package is not loaded a `hint` names the packages to load with `servicenow_enable_package`. Denied packages and the write tools of read-only packages are never offered, and `tools/list` stays the same for every connection. `tools/list` cost: +761 B on every profile (`core` 28,546 → 29,307, `all` 123,278 → 124,039, `all+tasks` 124,648 → 125,409, `all+legacy` 140,167 → 140,928); the budgets were raised with the owner's O-10 approval of 2026-10-09 (core may grow for N-36). The tool count is now 98.
- **N-54: output schemas on 31 more tools (code mode).** Tools in the catalog, change, knowledge, cmdb, email, importset, properties, revert, history, directory and batch packages now declare an `outputSchema`, so clients get `structuredContent` they can use without parsing text. The shapes are shared (`src/mcp/output-shapes.ts`): `{ result }` for reads, `{ mode, message, result }` for writes (plan preview or applied write), `{ count, <rows> }` for bounded lists; each is a loose object, so a payload may carry more keys. `tools/list` cost: `all` +3,831 B (119,447 → 123,278), `core` +0 B, `all+tasks` +3,831 B, `all+legacy` +4,056 B, within the owner's O-10 allowance of 2026-10-09 (up to +4 KB on `all`, `core` unchanged). The 29 tools still without one are listed with a reason in `test/fixtures/output-shape-register.json` (core budget frozen, inline-or-file delivery, argument-dependent shape, or past the allowance), and a test keeps that register exact and shrink-only. A second pass (owner, about +1 KB more) declares the 7 stable-shape tools that were past the first allowance — `run_atf_test`, `run_atf_suite`, `list_docs`, `read_doc`, `search_docs`, `write_doc`, `upsert_artifact` — at `all` +1,143 B, `core` +0 B, and registers `explain_portal` as an inline-or-file tool; 75 / 98 tools now declare an `outputSchema`, 22 remain in the register. A third pass (owner, O-10, 2026-10-13) declares the ten inline-or-file and argument-dependent tools by the fields every success path carries, inline or as a file reference: `document_table` / `document_app` (`kind`, `path`, `caveats`), `document_instance` (`profile`, `path`, `files`), `generate_er_diagram` (`tables`), `generate_table_flow` (`table`, `count`), `snapshot_instance` (`profile`, `generatedAt`), `compare_instances` (`a`, `b`), `explain_portal` (`mode`, `verified`), `read_ops` (`kind`, `window_minutes`), `check_data_health` (`table`, `available`). `tools/list` cost: `all` +1,651 B (119,529 → 121,180), `core` +0 B, `all+tasks` +1,518 B, `all+legacy` +1,986 B. 88 / 100 tools now declare an `outputSchema`; the register keeps only the 12 core tools. N-54 is frozen there for 3.0 (owner, ADR 0012): the register now gives the 7 default-profile tools and the 5 attachment tools (outside the default profile since B20, deferred to 3.x) their own reasons; `tools/list` +0 B.
- **MC-5: `initialize` instructions point at the rest of the surface.** Two lines: prompts (`prompts/list`; `servicenow_instance_overview` is always there) with the `servicenow://reference/tools/{name}` and `servicenow://reference/encoded-query` resources, and large results (`SN_MAX_RECORDS` / `truncated`, paging with `offset`, `format "file"`). +417 B (worst case 1094 → 1511 B of the 2048 B cap; configured `core` 708 → 1125 B). `tools/list` unchanged.
- **MC-3: prompt twins for the plugin skills.** Four prompts mirror the skills that had none, so clients without the Claude Code plugin get the same workflows: `servicenow_safe_write` (`table`; read → plan → apply after approval → verify → revert path; the write-mode block is read when the prompt is fetched — in apply mode there is no preview, so the model must ask the user before every write call, and a prod profile adds a caution), `servicenow_drift_review` (`instance`), `servicenow_schema_impact` (`scripts`) and `servicenow_discover_instance` (`docs`). Choice-like arguments (`kind`, `depth`) are completable strings. `test/prompts-skills.test.js` fails a skill without a prompt twin and a twin that misses a tool its skill names. `tools/list` unchanged (0 B).
- **N-5: task context (API layer).** `src/api/task-context.ts` `taskContext()` reads one `task` record (by sys_id or number, any descendant table) with its assignment (group, assignee, state), its approvals from `sysapproval_approver` (approver, approval group, state, created, due; counts per state), its task SLAs from `task_sla` (definition, stage, breach flag, planned end, business elapsed % and time left) and, with `history`, the newest `sys_journal_field` entries through the S-10 reader. `pendingApprovals()` lists the requested approvals waiting on one approver (sys_id or user_name, capped at 200). Each section degrades to `available:false` with a reason instead of failing; `taskContextMarkdown()` / `pendingApprovalsMarkdown()` render them. Callable from code only: `servicenow_get_task_context` waits for the tools/list budget (O-10), so tools/list is unchanged (0 B). Table and field names are unverified until O-5.
- **N-2: access explainer (API layer).** `src/api/access-explain.ts` `explainAccess()` explains why one user can or cannot `read` / `write` / `create` / `delete` a table, a record or a field. It reads the user's active roles (`sys_user_has_role`, inherited rows included), walks the table chain for the row ACL (`table` up the chain, then `*`) and the field ACL (`table.field`, `table.*`, `*.field`, `*.*`), and stops at the first name level that has an active record ACL. Each ACL's role part is checked against the roles; its condition is run as a query on the record, as the connected user; a script part is `undetermined`. The result is `granted`, `denied` or `undetermined` with the deciding ACL and the full chain; admin overrides, roles that need elevation, and the high-security default when no ACL matches are noted. Only role names leave the module. `servicenow_explain_access` waits for O-13 (placement, privacy) and O-10. Fields are unverified until O-5. No tool yet; `tools/list` delta: 0 B.
- **N-2: access decision diagram and ACL-script hints.** `renderAccessMermaid()` draws the decision as a Mermaid flowchart: per check, the names tried down to the matched level, each ACL there with its result (the deciding one marked), the check decision and the overall decision. An undetermined script part now carries the S-12 ACL-script lint hits (`scriptHints`, also in the ACL notes); the rules moved to `aclScriptHints()` in `src/api/security.ts`, shared with the code-health security scan (findings unchanged). No tool yet; `tools/list` delta: 0 B.
- **N-12: per-domain grouping in the access explainer.** On a domain-separated instance `explainAccess()` reads each ACL's `sys_domain` / `sys_overrides`, attributes the ACL to its domain (an overriding ACL notes what it overrides), and the rendered check table gains a Domain column with rows grouped by domain (global first); the result adds the domain caveat. Output on an instance without domain separation is unchanged. Fields are unverified until O-5. `tools/list` delta: 0 B.
- **N-34: UI Builder page review prompt and `sn-uib` skill (UX-24).** The `servicenow_uib_page_review` prompt (`experience`, optional `page`; listed with the `ui` package) walks a UIB page review: `explain_ui_experience`, then what a change would touch (`get_artifact_dependencies`, `where_used`, `get_update_set` `uib_completeness`), data broker security (`## Broker hints`, `check_code_health` `domains`) and page weight (`## Page hints`, `compare_instances` `elementDiff`); steps whose package is off are skipped. The plugin ships a sixth skill, `sn-uib`, with trigger and negative evals. A registry lint test allows one artefact descriptor per table; the duplicate `sys_ux_form_action` pair (`workspace_form_action` / `uib_form_action`) is its one documented exception until the owner decides the breaking merge (O-4). `tools/list` unchanged (0 B).
- **N-7: translation coverage (API layer).** `src/api/i18n-coverage.ts` `i18nCoverage()` reports, for an application scope and / or a set of UI Builder macroponents, per language (given codes, or the active `sys_language` rows without the base language): UI messages (`sys_ui_message`), field and table labels (`sys_documentation`), choices (`sys_choice`) and translated text (`sys_translated_text`) and translated-field values (`sys_translated`, keyed by table, field and source value) of the scope's tables, and the macroponents' composition strings plus `required_translations` (N-31 `requiredTranslations`) matched against `sys_ui_message` keys. Each language carries translated / missing / total counts per category and a bounded sample of missing keys; an unreadable table marks its category `unreadable` with a caveat instead of failing. `i18nCoverageMarkdown()` renders the report. Callable from code only: the `document_app` kind `i18n` waits for the tools/list budget (O-10), so tools/list is unchanged (0 B). Table and field names are unverified until O-5.
- **N-16: ATF result history (API core).** `src/api/atf-history.ts` `atfResultHistory()` reads, for given test and / or suite sys_ids, the newest `sys_atf_test_result` / `sys_atf_test_suite_result` rows in one bounded `IN` query per section and summarises each over a window (default 20, max 100): last result (status, start time, run time, suite result), pass rate (`success` passes, `failure` / `error` fail, skipped or cancelled runs count for neither) and `flaky` (both outcomes and at least 2 pass / fail flips). `summariseResults` is pure and property-tested; `withAtfResults` merges the summaries onto `list_atf_tests` / `list_atf_suites` items, and `renderAtfHistory` renders Markdown. A failed read degrades to `available:false` with a reason. Not wired to a tool yet: the `with_results` parameter waits for O-10. Unchanged-test refinement: the test / suite records' `sys_updated_on` and `sys_mod_count` are read in one more bounded query (only for ids that ran), and flips and `flaky` count only the runs started at or after the definition's last update (`stableRuns`, `definition`), so a fail, an edit and a pass is a fix, not flakiness; pass rate and last result keep the whole window, and a failed read falls back to the whole window (`definitionsChecked:false`). The tables, fields and status values are unverified until O-5. `tools/list` delta: 0 B.
- **N-12: domain separation awareness.** `check_capabilities` (full run) reports `domainSeparation` from one cached probe of the server account's `sys_domain`; `get_status` shows it only when a cached probe found it active (no extra request). Artifact and script lists, business rules in `trace_table_event` (both views), and `explain_artifact` keep `sys_domain` / `sys_overrides` for a domain-specific record, and traces add a caveat that the view is the session user's domain. Output on an instance without domain separation is unchanged. Field names are unverified until O-5.
- **N-37: per-tool reference resource.** `servicenow://reference/tools/{name}` returns one tool in full (description, annotations, published input and output schema, package, registration state, M-7 aliases, error codes by source); an unknown name is `InvalidParams` / `NOT_FOUND`, a retired M-7 name points to its successor. The tool manifest is now `manifestVersion: 5` with per-tool `sizes` (description, input schema, parameter descriptions, output schema) measured from the published `tools/list`. `tools/list` delta: 0 B.
- **N-26: UIB page explainer depth.** `explain_ui_experience` with `format: "file"` exports element props and binding expressions (`@data` / `@state` / `@context`, resolved to data resource, state property or context path), component resolution (OOB `now-*`, custom `sys_ux_lib_component`, nested macroponent), event chains (element → event → client script, broker operation, state change or `sys_ux_event`) with an event-flow Mermaid diagram, and client script / broker bodies (capped at 100 000 characters). Inline json / markdown output is unchanged; a `detail` input waits for O-10. Shapes are unverified until O-5. `tools/list` delta: 0 B.
- **N-28: UIB dependencies and where-used.** `get_artifact_dependencies` adds macroponent → component (`composition`) and → data broker (`data_broker`) edges, client script → client script include edges from `imports[...]` and from the `includes` list (by sys*id), and the inbound direction. `where_used` with `kind: "script"` searches macroponent compositions, data resources and UIB client scripts. `get_update_set` returns `uib_completeness` when the set captures `sys_ux*\*`records: per page, which route, screen, variant, applicability, macroponent, client scripts, brokers and broker ACLs are missing from the set. It follows nested macroponents (composition components that are`sys_ux_macroponent`records, up to 3 levels, as`nested_macroponent`) with their own client scripts and brokers, and names REST and GraphQL brokers from their own tables. Shapes are unverified until O-5. `tools/list` delta: 0 B.
- **N-29: UIB data brokers and security.** Registry descriptors `uib_data_broker_rest` (`sys_ux_data_broker_rest`) and `uib_data_broker_graphql` (`sys_ux_data_broker_graphql`), read tier, `verified: false`; dependency edges to REST / GraphQL brokers now carry these types. `explain_ui_experience` resolves REST and GraphQL brokers next to transforms and scriptlets and returns `brokerHints` (markdown `## Broker hints`). Three rules in `src/api/uib-broker-lint.ts`: `uib-broker-mutates-no-acl` (error: `mutates_server_data` without a `ux_data_broker` ACL), `uib-transform-gliderecord-no-acl-check` (warn: a transform queries with `GlideRecord` / `GlideAggregate` and never checks access) and `uib-broker-no-input-schema` (info: empty `properties`). They run in the `check_code_health` UIB domain; the REST / GraphQL tables are optional there, and an unreadable ACL table disables only the ACL rules. A field the instance does not return never fires a rule. The broker tables and fields are unverified until O-5. The security scan has a `ux_data_brokers` check (UX-12): a mutating broker with no `ux_data_broker` ACL (`uib-broker-mutates-no-acl`, judged only when the ACL read is complete) and a broker ACL that is open or granted to a public role (`ux-broker-acl-open`, error when the broker mutates, else warn), in `check_code_health` and in the `security` document. `tools/list` delta: 0 B.
- **N-30: workspace coverage.** `explain_ui_experience` adds, at the result level: `shell` (root macroponent plus decoded `chrome_toolbar` / `chrome_header` / `chrome_tab` / `chrome_footer` page properties; unknown shapes are listed in `undecoded`), `actions` (`sys_declarative_action_assignment` on the experience's list, form-layout and new-tab tables, with model, surface, form position, decoded record conditions, roles and the payload / action / component they run), `forms` (per table: form action layouts, action bar, related items and contextual side panel, derived from `form_position`), `themes` (`m2m_app_theme` → `sys_ux_theme`) and `workspace` (Agent vs Configurable Workspace classification with signals and an Agent Workspace migration list). `sys_ux_list` entries gain `columns` and `conditionTerms`, decoded by the new `readEncodedQuery()` reader; the markdown report gains a `## Workspace` section. Unreadable tables become caveats; the new tables and fields are unverified until O-5. Registry descriptors (read tier, `verified: false`, unverified until O-5): `ux_declarative_action` (`sys_declarative_action_assignment`), `ux_declarative_action_definition`, `ux_declarative_action_payload`, `uib_app_theme` (`m2m_app_theme`), `aw_master_config` (`sys_aw_master_config`) and `aw_list` (`sys_aw_list`); action `runs` now name their payload, action definition and UI component (`sys_ux_lib_component`) from one read per table. `check_capabilities` reports `workspaces` beside the matrix on a full run (configurable workspaces vs `sys_aw_master_config` rows, `detail.kind` `configurable` / `agent` / `mixed` / `none`, cached like the other probes; a 404 Agent Workspace table counts as none), and the `document_instance` README gains `## Workspaces` and `## Workspace migration` (Agent Workspace configs, lists and AW-restricted declarative actions, each with its configurable counterpart or `none yet`). Fluent fallback goldens `workspace_fallback` and `uib_broker_fallback` cover the six workspace types and the two data broker types, built by `npm run fluent:verify`. `tools/list` delta: 0 B.
- **N-31: UIB composition diff and page hints.** `compare` with `types` adds `elementDiff` to a changed `uib_macroponent`: the `composition` diffed per element (added, removed, moved — another slot or out of sibling order — and changed attributes, props and bindings, bounded to 200 entries), and the report shows `composition [elements +A -R ~C (moved M)]`. New pure modules: `uib-page-weight` (`src/api/uib-page-lint.ts` — element count, slot depth, data resources fired on load, on-load data resources without a `when` condition) and `requiredTranslations()` (`src/core/artifacts/uib-translations.ts` — user-facing composition strings against the declared `required_translations`, for N-7). `explain_ui_experience` surfaces both: `pageHints` lists the `uib-page-weight` findings of each macroponent that has any (with the screen variants that render it; the rule hint is left out per finding and shown once in a new markdown `## Page hints` section; omitted when no page trips the rule), and the full depth (`format: "file"`, API `detail: "elements"`) adds `pageMetrics` and a `translations` summary per macroponent (strings, unique texts, declared, undeclared and a sample of up to 20 undeclared strings; `declared: null` when `required_translations` is unreadable) plus `detail.translatableStrings` / `undeclaredTranslations`; `required_translations` is read only at that depth, so the default inline JSON does not grow for a light page. UIB shapes are unverified until O-5. `tools/list` delta: 0 B.
- **N-32: UIB client-script lint.** `lint_script` and the `check_code_health` `extended` sweep run 19 `uib-*` rules on `sys_ux_client_script` / client script includes: server APIs and synchronous waits in client code, `eval`, `api.state` mutation and loops on state, DOM access, blocking dialogs, updates in loops, hard-coded sys_ids / instance URLs, unused imports, and — for `lint_script`, from one read of the owning macroponent — `setState` / `emit` / `api.data` names the macroponent does not declare. The UIB runtime API is unverified until O-5. `tools/list` delta: 0 B.
- **N-23: least-privilege advice in `doctor`.** `doctor` compares the enabled tool packages with what the account can read: the reads a package needs but cannot get, and elevated roles (`admin`, `security_admin`, …) that no enabled package needs. The section is informational and never fails the run; `doctor --json` carries it as `privilege` (`src/api/privilege.ts`). `tools/list` delta: 0 B.
- **N-15: index and table-size advisor.** `describe_table` with `details: true` adds the indexes of the table chain (`sys_index`) and a cached row estimate from the Aggregate API. `src/api/query-explain.ts` (`explainQuery`) reports which conditions of an encoded query can use an index. The `query_table` `explain` parameter waits for core budget headroom (O-10). `sys_index` fields are unverified until O-5. `tools/list` delta: 0 B.
- **N-20: elevated-privilege roles.** The `roles` capability probe reports the account's `elevatable` roles. A 403 on a Table API write that needs an elevated role (`sys_security_acl*` → `security_admin`) now returns `ELEVATION_REQUIRED` with a hint instead of a plain `INSTANCE_HTTP_403`. Elevating from a tool waits for O-16. `tools/list` delta: 0 B.
- **N-13: hardening compliance.** A versioned hardening rule table (`src/api/hardening-rules.ts`) is checked against `sys_properties`: pass, fail by severity, or not set (the platform default applies). It appears as `## Security — hardening` in `check_code_health` and in the `security` kind of `document_instance`. An unreadable `sys_properties` is reported as unknown, never as compliant. `tools/list` delta: 0 B.
- **N-14: cross-scope access report.** `document_app` kind `security` joins an app's outbound cross-scope calls with `sys_scope_privilege` and lists the restricted caller access records, so a call that has no granted privilege stands out. `tools/list` delta: 0 B.
- **N-19: `servicenow_security_posture` prompt.** A guided security review: the ACL scan and the hardening table (`check_code_health`), the instance `security` document, and, with `scope`, the cross-scope access of one app. It is listed when the `codecheck` package is on; the document steps need `docs`.
- **N-3: Instance Scan results.** `check_code_health` adds an Instance Scan section: the latest `scan_result`, its `scan_finding` rows by priority and artefact type, and the findings that our own lint also reports ("both agree"). It degrades to unavailable when the scan tables cannot be read. Running a scan from a tool is a later phase; `scan_*` tables are unverified until O-5. `tools/list` delta: 0 B.
- **N-10: record watch resources.** New subscribable template `servicenow://profiles/{profile}/records/{table}/{sys_id}` (`table` package). A read is masked like `servicenow_get_record`. A subscription polls `sys_updated_on` / `sys_mod_count` every `SN_RECORD_WATCH_INTERVAL_MS` (floor 30 s) and sends `notifications/resources/updated` on a change. Subscriptions are capped per session (`SN_RECORD_WATCH_MAX_PER_SESSION`, default 10) and per process (`SN_RECORD_WATCH_MAX`, default 50; one more is `WATCH_LIMIT`). A subscription stops on unsubscribe, when the record is deleted, and when the session closes. `tools/list` delta: 0 B.
- **N-3: Instance Scan run (API layer).** `runInstanceScan` in `src/api/instance-scan.ts` starts a full, point (one record) or suite scan through the CI/CD API (`/api/sn_cicd/instance_scan/...`) behind the ATF rails: a write, refused in read-only mode and on a table the policy blocks (`scan_result`, and the target table of a point scan); an inactive `sn_cicd` plugin is reported clearly. `waitForInstanceScan` polls the CI/CD progress API like an ATF run (cancellable, progress reported); `waitForAtfRun` takes the progress message as a parameter for this. The refusal on a production-marked profile waits for the H-11 marker (O-4), the `run` input for O-10, and the endpoints are unverified until O-5. No tool yet; `tools/list` delta: 0 B.
- **N-62: compact reads (API layer).** `src/api/compact-read.ts` resolves a table's summary field set: `sys_id`, the child-most `display=true` column (else `name`, then `number`), up to 12 columns of the default list view from `sys_ui_list_element` (formatters dropped; a table without a layout uses its nearest ancestor's) and `sys_updated_on`, or a heuristic set filtered by the dictionary when no table of the chain has a layout. Its reads are cached with the schema, and a failed layout read becomes a warning. Pure helpers compact a `display_value=all` record by dictionary type (reference `[sys_id, display]`, choice label, dates raw, journal fields dropped unless named), drop empty cells (never `0` or `false`) and encode rows as `{columns, rows, cell, total, truncated}`. The `query_table` / `get_record` options wait for O-21 (a); the default-view query is unverified until O-5. No tool yet; `tools/list` delta: 0 B.
- **N-62: compact-read size goldens.** `test/compact-read-sizes.test.js` reads the N-57 synthetic incident (100 columns; the generator moved to `test/synthetic.js`) as `display_value=all` and pins the JSON bytes of every compact form in `test/fixtures/compact-read-sizes.json`, for one row and for 200. On 200 rows, 1,479,627 B shrink to 621,049 B in the display form (−58 %), 547,531 B with `omit_empty`, and 343,528 B in the table form (−77 %); the summary field set in the table form is 63,226 B (−96 %). On one row, the `omit_empty` wrapper and the table legend cost a few bytes more than the display form. The figures are synthetic until O-5. `tools/list` delta: 0 B.
- **N-62: opt-in compact reads (O-21 (a)).** `servicenow_query_table` takes `fields:"summary"` (the table's summary field set), `displayValue:"display"` (references as `[sys_id, name]`, choices collapsed), `omitEmpty` (drops empty strings and nulls, keeps `0` / `false`) and `format:"table"` (`{columns, rows}`); `servicenow_get_record` and `servicenow_aggregate` take the display variants. Every option is opt-in and the defaults are unchanged. `tools/list` +357 B on `core` and `all`.
- **N-65: result size hints (O-21 (c)).** Seven large readers (`document_app`, `document_instance`, `document_table`, `snapshot_instance`, `compare_instances`, `compare_update_set`, `get_script`) publish `_meta["anthropic/maxResultSizeChars"]` set to `SN_MAX_RESULT_CHARS`, so a client that honours it keeps their results inline. On by default; `SN_RESULT_SIZE_HINTS=false` turns it off. `tools/list` +329 B on `all`, +0 B on `core`.
- **N-65: `resource_link` results.** A `format:"file"` delivery (query exports, snapshot / compare files, Mermaid diagrams) now carries a second content block, a `resource_link` to `servicenow://docs/<path>` with its MIME type and size, while the docs package is enabled; the text block is unchanged. The docs resource now also reads the delivered `.jsonl`, `.csv` and `.mmd` files. `tools/list` +0 B.
- **N-39: `SN_STRUCTURED=false` (O-21 (d)).** An opt-in switch for clients that read only the text: tools publish no `outputSchema` and results carry no `structuredContent`, so each payload goes out once. The default is unchanged. Off, `tools/list` shrinks by 3,392 B on `core` and 15,914 B on `all`.
- **N-40: `next_offset` on `servicenow_query_table`.** A paged read (no `fetchAll`) carries `next_offset` while the instance has more rows: offset + limit against the count, or a full page under `noCount`. A page cut by `SN_MAX_RESULT_CHARS` names the offset right after the rows it kept, in the field and in the note. The next page may repeat a row the instance withheld but never skips one. `tools/list` delta: 0 B.
- **N-22: access review (API layer).** `src/api/access-review.ts` lists the holders of privileged roles: `admin`, `security_admin`, `elevated_privilege`, and every role that contains one of them through `sys_user_role_contains`. Each grant has its path (direct, group, or contained role), its creator and its date. Each account has `active`, `last_login_time` and a dormant flag (no login for 90 days). Role revokes come from `sys_audit_delete`. `readRoleHistory` gives one user's role history: their `sys_user_has_role` rows as grants and their deleted rows (matched in the `sys_audit_delete` payload) as revokes, newest first. It reads account data, so it is not part of the metadata-only `security` document. The `access_review` kind and `lookup_directory` `role_history` wait for O-10. No tool yet; `tools/list` delta: 0 B.
- **N-1: upgrade readiness (API layer).** `src/api/upgrade.ts` reads the upgrade history (`sys_upgrade_history`, newest first). For one upgrade it groups the unresolved skipped records from `sys_upgrade_history_log` by application and artefact type. For one skipped update name it finds, in `sys_update_version`, the new base, the old base and the customer version. It then diffs them per field and classifies the skip as `only_customer_changes`, `only_base_changes`, `both_changed` or `unknown`. Every read degrades to unavailable. `servicenow_review_upgrade`, the `upgrade` document kind and the `review-upgrade` prompt wait for O-10. Tables, fields and choices are unverified until O-5. No tool yet; `tools/list` delta: 0 B.
- **Token measurement harness (N-57).** `test/surface.js` measures the published `tools/list` per profile; a ratchet test fails when a budget sits more than 2 % above its measurement, and `test/tool-size.test.js` caps every tool (total, description, input and output schema) with a reasoned allow-list for the five outliers. `npm run tokens:budget` checks or lowers the budgets; `npm run tokens:report` prints the byte breakdown, estimated tokens, repeated parameter text and the heaviest tools (`--json` for machines; `--base <ref>` builds that ref in a temporary git worktree and adds the per-profile delta). The report's `raw` column is the size before the N-58 lean serializer. Tool results are measured in use too: the `get_status` `observability` block gives each tool's result bytes (text and `structuredContent` separately, p50 / p95), and `GET /metrics` adds `servicenow_mcp_tool_result_bytes` and `servicenow_mcp_tool_result_channel_bytes_total`. `test/response-size.test.js` pins `{textBytes, structuredBytes}` for 11 tool calls over synthetic ServiceNow payloads (a 100-column incident, a 200-row page, `describe_table`, `get_artifact`, `explain_artifact`, one list tool per family) in `test/fixtures/response-sizes.json` (`UPDATE_GOLDEN=1` refreshes it).

- **OpenTelemetry MCP spans (N-55).** Tool calls now publish on `node:diagnostics_channel` (`servicenow-mcp:mcp.tool.call.start` / `.end` / `.error`: tool, package, profile, outcome, error code, duration, result size; never arguments or results). The W3C `traceparent` / `tracestate` from `params._meta` are validated and carried into the HTTP channel messages together with the call's `callId`.
  - `SN_OTEL=1` turns the channels into OpenTelemetry spans (a `SERVER` span per tool call, a `CLIENT` span per ServiceNow request) through `@opentelemetry/api`, a new optional peer dependency loaded only when the setting is on. When the package is missing, one warning is logged and nothing else changes. The MCP / GenAI semantic-convention names are provisional.
  - `SN_OTEL_PROPAGATE=1` (off by default) sends `traceparent` / `tracestate` to the ServiceNow instance.
  - With both settings off, the only cost is a `hasSubscribers` check per call; no tool definition or `tools/list` byte changes.
- **Claude Code plugin: two read-only subagents (N-47).** `sn-investigator` answers a question about the instance and returns a short report with its evidence. `sn-change-reviewer` reviews a plan preview or an update set before it is applied and returns a go / hold verdict. Each has an explicit `tools` allowlist made only of this server's read-only tools; `test/plugin-agents.test.js` checks every entry against the manifest's `readOnlyHint`.
- **Claude Code plugin: hooks v2 (N-48).** Two plain Node hooks with no dependencies.
  - A `SessionStart` hook adds the active profile, its instance host, the write mode (plan, apply, read-only or a held prod profile), the tool packages and the other profile names. It reads only the environment and the local env file, makes no network call, never prints a user name, password or token, and is silent on any error.
  - A `PostToolUse` hook adds a one-line hint when a ServiceNow result says `truncated: true`, naming the knobs that tool really has (`fields`, `offset`, `format: "file"`). The knob lists are pinned to the tool manifest.
- **Portable skills (N-52).** The five plugin skills are now checked against the open Agent Skills spec: name format, allowed frontmatter keys, and no client-specific `mcp__` tool names. The README documents installing them for VS Code Copilot, OpenAI Codex and Cursor (`.agents/skills/` works in all three).
- **Add Agent Skills to Workspace (N-52).** The VS Code extension bundles the five `sn-*` skills (`extension/scripts/bundle-skills.mjs` runs at `vscode:prepublish`) and its new **ServiceNow MCP: Add Agent Skills to Workspace** command copies them into `.agents/skills/`, `.github/skills/` or `.claude/skills/` of a trusted workspace folder; a skill already there is replaced only after a modal confirmation.

- **MCP bundle (`.mcpb`) for one-click installs (N-51, TK-27).** `npm run mcpb:pack` builds `dist/mcpb/servicenow-mcp-ai-<version>.mcpb`, a zip of the compiled server, its production dependencies, `manifest.json` and the icon. Claude Desktop and other MCPB hosts install it in one click and need no `npm` / `npx`. The bundle is built outside the npm package, so the tarball and `pack:check` do not change.
  - `mcpb/manifest.json` (MCPB manifest 0.3) is generated by `scripts/mcpb-manifest.mjs` (`npm run mcpb:manifest`, `--check` runs in `npm run check`):
    - name, version, description, author, license and links come from `package.json`;
    - the tool list comes from the registry;
    - the install form (`user_config`) comes from the settings manifest: every connection / auth key plus the MCP-registry keys (`SN_TOOL_PACKAGES`).
  - Secrets are `sensitive: true`, and only `SN_INSTANCE` is required. No field carries a default, so an empty field falls back to the env file and then to the server default.
  - `loadEnv()` drops an unsubstituted `${user_config.<key>}` placeholder that a host passes for an empty optional field.
  - `mcpb/manifest.json` is a version follower of `scripts/sync-version.mjs`. `npm run mcpb:pack -- --validate` also runs the official `@anthropic-ai/mcpb` validator through `npx`; it is pinned but not a dependency.
  - `test/mcpb-manifest.test.js` covers the mapping, sensitive flags, drift, the zip writer, the placeholder guard and the dormant release job.
- **MCP Apps views, opt-in (N-50).** `SN_MCP_APPS=1` (default off) implements the MCP Apps extension (`io.modelcontextprotocol/ui`, SEP-1865, spec 2026-01-26).
  - Four `ui://servicenow-mcp/…` resources (`text/html;profile=mcp-app`): `plan-diff` (the plan of every write tool with `apply`: per-field before/after and the plan token), `mermaid` (the five Mermaid generators: entities, edge list, source), `flow` (`servicenow_explain_flow`: trigger, nested steps, activities, lanes) and `uib-tree` (`servicenow_explain_ui_experience`: routes → screens → macroponent tree, data brokers).
  - The linked tools carry `_meta.ui.resourceUri`, but only for a client whose `initialize` declares the extension with that MIME type. Tool results never change.
  - Each view is a static, self-contained HTML document (no network, inline script and style, its own CSP with `default-src 'none'` and the script and style pinned by hash, no eval) that renders the tool result the host pushes over `ui/notifications/tool-result`. All record data is HTML-escaped into text nodes. Mermaid is shown as tables and source, not drawn.
  - Off: `tools/list`, resources and every result are byte-identical to before (`test/mcp-apps.test.js`).

- **`ops` v2: integrations, transactions and MID servers (N-6).** `servicenow_read_ops` gains three `kind` values; no new tool.
  - `integrations` reads failed (status >= 400) and slow (over 5,000 ms) outbound calls from `sys_outbound_http_log` and groups them by host and REST message, with status counts, the slowest call and up to three sample URLs. URLs keep only origin and path (no credentials, query string or fragment), as under H-6. An empty log carries a `note`: outbound logging may be off, so it is not proof of health.
  - `transactions` reads slow transactions from `syslog_transaction`, slowest first, grouped by URL with the query string stripped (count, average and maximum response time, distinct users).
  - `mid` reads `ecc_agent` (status counts, name, version, last refresh, host) and, as a sub-section that degrades on its own, the `ecc_queue` backlog (`state=ready` by agent, the oldest ready entry) and the recent `error` rows by agent.
  - All three join `overview` as Aggregate API counts. Each section degrades on its own to `available: false` with the reason, and rows stay capped by `limit`.
  - The filter fields are checked against the dictionary before any count. If a field is missing, the section fails rather than counting every row; if the dictionary is unreadable, the result lists `unverified_fields`.
  - The `servicenow_why_is_it_slow` prompt now reads the `transactions`, `integrations` and `mid` kinds.
  - The table, field and state names are not yet checked on a live instance (O-5).

- **Fluent round-trip oracle (P-29, O-7).** `@servicenow/sdk` 4.12.2 is now an exact dev dependency (never a runtime one). `npm run fluent:verify` (`scripts/fluent-verify.mjs`) does two things with every golden in `test/fixtures/fluent/`. It type-checks the golden against the SDK types with strict `tsc` and excess-property checks. It then runs `now-sdk build` offline, in conflict-free projects, and checks that every declared key comes out under its source sys_id, either as its own update XML or nested in its parent's (flow logic, playbook lanes). It also checks that no `DELETE` record is emitted. `test/fluent-sdk-oracle.test.js` runs the oracle and is skipped when the SDK is not installed (`npm ci --omit=dev`).
  - `npm run fluent:actions` (`scripts/gen-fluent-actions.mjs`) generates `src/api/fluent-sdk-actions.ts`, the `action.core` / `actionStep` input table, from the SDK's built-in definitions.
  - `npm run check` runs `fluent:actions -- --check`, which fails when the table is stale after an SDK bump.
  - All 27 goldens type-check with 0 errors and build: 93 keyed records found among 100 built.
  - Limit: the oracle does not compare field values with live instance records. That needs O-2 / O-5 fixtures.

- **Plugin hook that blocks a token-less destructive apply (D-8).** The Claude Code plugin now ships `hooks/hooks.json`, which runs `hooks/require-plan-token.mjs` as a `PreToolUse` hook on the server's tools. When one of the B4 destructive calls has `apply:true` and no `plan_token`, the hook denies it before the call reaches the server and tells the assistant to preview first.
  - It only checks that a token is present; the server validates it.
  - Input it cannot parse passes through.
  - `SN_DESTRUCTIVE_CONFIRM=off` turns the hook off.
  - The hook ships with the plugin and is not in the npm tarball.
  - `test/plugin-hook.test.js` keeps the hook's tool list identical to the manifest's `plan_token` tools.
- **AST-based lint and a code-health baseline (S-12).** Script lint now parses with `acorn` (new runtime dependency, ~568 KB unpacked, no dependencies of its own; `src/api/script-ast.ts`): ES5 for global-scope records (`sys_scope` = `global`), ES2021 for scoped apps and client fields, with `return` allowed at the top level. The line rules (`hardcoded-sys-id`, `hardcoded-instance-url`, `eval-usage`, `gs-sleep`, `gs-log-deprecated`, `set-workflow-false`, `current-update-in-br`, `sync-get-reference`, `sce-trust-as-html`, `sanitize-bypass`, `gr-on-client`), `query-in-loop` (only inside a loop body), `gr-unbounded-query` (per receiver and function) and `sp-param-unvalidated` run on the syntax tree, so comments and string contents no longer produce findings; the ACL rules `eval-in-acl` / `gr-write-in-acl` / `getuser-in-acl` and the P-19 `portal-public-data-widget` data-read check do the same. Rule ids and finding shapes are unchanged. A source that does not parse (or Jelly `${…}` markup, or one over 512,000 chars) falls back to the regex engine, which reports `syntax-error` as before; the parser never throws to the tool. Additive: per-field `engine` (`ast` | `regex`), `ecma` and `parseError` on script lint results, and the new rule `es2021-syntax-in-es5` (warn — a global-scope script that only parses as ES2021). `servicenow_code_health` keeps a baseline in `<profile>/code-health.baseline.json` (docs store, one scope per `instance` / `table:<t>`): the first run records it, later runs return a `delta` (new / fixed / unchanged / partial per section: lint, artefacts, security, domains) and a "Baseline delta" report section; the new input `update_baseline: true` resets it to the run. Fingerprints leave out line numbers; a finding missing from a capped, unreadable, truncated or unavailable unit is counted `partial`, never `fixed`. `TOOLS_LIST_BUDGET_ALL` unchanged. Tests: `test/s12-ast-lint.test.js` (17 tests, including a fast-check property that the parser never throws through); code-health writer goldens updated.
- **Flow, subflow, action and playbook Fluent emitters (P-27, unverified).** `servicenow_generate_fluent` emits `flow`, `subflow`, `flow_action` and `playbook` artefacts from their `explain_flow` tree (`src/api/fluent-flow.ts`; the result's `emitter` is `"flow"`): `Flow()` / `Subflow()` / `Action()` with `wfa.trigger(trigger.*)`, `wfa.action(action.core.*)`, `wfa.subflow(…)`, `wfa.flowLogic.*` (if / elseIf / else chains, forEach, doUntil, tryCatch, doInParallel, waitForADuration, endFlow, assignSubflowOutputs, …), `wfa.dataPill(…)` in template literals for trigger / input / flow-variable pills, `*Column()` inputs, outputs and flow variables, and stages; `PlaybookDefinition()` with lanes, `ActivityDefinitions.Core.*` activities, timers and triggers. Spoke and custom actions, nested doInParallel, playbook Questionnaire activities, variants, unknown logic or activity definitions, orphan Else blocks, undecodable step values, flow inputs / outputs and rows the tree does not represent each get an explicit `unsupported[]` entry and a `Record()` fallback (the raw child row where one was read); unmapped data pills keep their `{{…}}` text and are reported. Secrets become placeholders as in P-26, output is deterministic, and a degraded, unavailable or mismatched tree falls back to the P-26 `Record()` form with a warning. Every file and the result carry a `verified:false` note: the `trigger.*`, `action.core.*`, `wfa.*` and `ActivityDefinitions.Core.*` names follow `project/SDK-PARITY.md`, not checked SDK signatures (the P-29 oracle waits for O-7). No new tool or setting.
- **Portal, workspace and catalog Fluent emitters (P-28).** `servicenow_generate_fluent` gains dedicated emitters (`src/api/fluent-ui.ts`, dispatched from `src/api/fluent.ts`) for `ServicePortal`, `SPPage` (container / row / column / instance tree), `SPWidget` and `SPHeaderFooter` (template, CSS, client / server script and link as `Now.include` sidecars; Angular templates inline; dependencies and providers as references), `SPTheme`, `SPMenu`, `SPPageRouteMap`, `SPWidgetDependency`, `SPAngularProvider`, `JsInclude`, `CssInclude`, `Workspace`, `Dashboard`, `UxListMenuConfig`, `Applicability`, `CatalogItem` / `CatalogItemRecordProducer` (typed variables with choices, variable-set includes, categories, user criteria, followed by the `VariableSet`, `CatalogClientScript` and `CatalogUiPolicy` calls), and standalone `VariableSet`, `CatalogClientScript`, `CatalogUiPolicy`. UI Builder internals and standalone Angular templates stay `Record()` with an `unsupported[]` entry; a workspace references its root macroponent / app config by `Now.ref` and reports them. Rows that do not fit the tree and unmapped fields are emitted as `Record()` rows or reported, never dropped; secrets and determinism as in P-26. Property names follow `project/SDK-PARITY.md` and are unverified until the P-29 oracle (O-7): such runs carry a `warnings[]` entry and a header note. No new tool, env key or dependency. Tests: `test/fluent-ui.test.js` (26 tests, goldens `test/fixtures/fluent/ui_*.golden.txt`, mutation-checked); the P-26 portal fallback golden is replaced by a UI Builder one (`uib_fallback`).
- **Fluent emitter core (P-26, `artifacts` package, opt-in).** New read-only tool `servicenow_generate_fluent({artifactType, sys_id | key | scope, limit?, format?, overwrite?})` (97 tools) emits SDK Fluent source from the artefact registry (`src/api/fluent.ts`, renderer `src/api/fluent-render.ts`): one `.now.ts` file per artefact with `Now.ID['<key>']` keys, a `keys.ts` fragment, `Now.include('./…')` sidecars for script / HTML / CSS bodies and `Now.ref(table, id)` for sys_id references. Dedicated emitters cover business rules, script includes, client scripts, UI policies (with actions), UI actions, scheduled jobs, script actions, ACLs (with roles), UI pages, system properties and roles; every other type is emitted as `Record()` and listed in `unsupported[]` with a reason (flows and playbooks wait for P-27, portal / workspace / catalog for P-28) — never silent loss. Secrets (password properties, credential-like field names, `SN_REDACT_FIELDS`) become `<redacted:credential>` placeholders. Output is deterministic (code-point ordering of records, fields, keys and files). Every file header names `@servicenow/sdk` `4.12.2` (`SDK_BASELINE`) as an assumption pending owner gate O-7; the output is TS-parse-checked in tests but not type-checked against the SDK. `format:"file"` writes under `<SN_DOCS_DIR>/<profile>/fluent/<scope>/` through the docs store with a `<base>.fluent.json` companion (manifest kind `fluent`, frontmatter contract) and refuses to overwrite hand-edited files unless `overwrite:true` (`DOC_GENERATED`). New `docsReadRaw` in `src/api/docs.ts`. `TOOLS_LIST_BUDGET_ALL` raised to 148,500 (measured 148,384); the core budget is unchanged. Tests: `test/fluent.test.js` (23 tests, goldens in `test/fixtures/fluent/`, mutation-checked).
- **Playbooks in `explain_flow` (P-12).** `servicenow_explain_flow({kind:"playbook", sys_id})` explains a Process Automation Designer playbook (`sys_pd_process_definition`): its lanes (`sys_pd_lane`) in order with their activities (`sys_pd_activity`, numbered `1.1`, `1.2`…) and each activity's definition (`sys_pd_activity_definition`), conditions and timers (`sys_pd_timer_attributes`), the triggers (`sys_pd_trigger_instance`), inputs / outputs (`sys_pd_process_input` / `_output`) and variants (`sys_pd_process_variant`); opt-in `runs` read `sys_pd_context` with per-run activity states from `sys_pd_activity_context`. The Mermaid diagram draws one subgraph per lane with its activities chained by order; the markdown and file formats work as for flows. An instance without the `sys_pd_*` family (not installed or not licensed) returns `available: false` with a caveat instead of an error; an unreadable child table is a caveat; `depth` is ignored with a caveat. Every table and field is `verified:false` until gates O-5 / O-9. `test/explain-playbook.test.js` (10 tests, golden `test/fixtures/explain/playbook.mmd`).
- **Portal and catalog artefact writes with SDK pre-flight (P-24).** `servicenow_upsert_artifact` now writes the Service Portal (SP-1 … SP-10: widgets with their Angular templates and dependency links, pages, portals, templates, dependencies, providers, JS / CSS includes, theme includes, menu items, headers / footers, route maps) and Service Catalog (CAT-1 … CAT-6: catalog items and record producers with their variables, variable choices, client scripts, UI policies and their actions and categories; variable sets; variables) types. A child that hangs off another child names it by position with the new `children[].parent`, so a page's `sp_container` → `sp_row` → `sp_column` → `sp_instance` layout is written as one plan, each level linked to the record written (or matched) before it; a child key may be `{sys_id}`, so a `servicenow_get_artifact` result round-trips unchanged. The plan runs the ServiceNow SDK's pre-flight checks: registry `unique` rules (`sp_widget.id` — also across `sp_header_footer`, which extends it — `sp_page.id`, `sp_portal.url_suffix`, `sp_dependency` name and module, `sp_ng_template.id`, `sp_angular_provider.name`) must match no other record on the instance and no other record of the plan (`DUPLICATE_UNIQUE_FIELD`, 409, with `existing` / `existing_scope`; a key that resolves to a record in another scope is the same error); a catalog variable name must be a valid identifier (`PREFLIGHT_INVALID`); a create into a scoped application whose `scopePrefixFields` value lacks the `<scope>_` prefix gets a `SCOPE_PREFIX` plan `warnings` entry. New error codes `CHILD_PARENT_INVALID`, `DUPLICATE_UNIQUE_FIELD`, `PREFLIGHT_INVALID`. Every portal and catalog field name beyond the links is unverified until O-5. `test/upsert-artifact.test.js` (+7 tests).
- **Flow activation toggle (P-25, unverified).** `servicenow_upsert_artifact` accepts `artifactType: "flow"` with `fields: {active: true|false}` on an existing flow and nothing else; any other field, a child, an invalid value or a key that matches no flow is refused with the new `FLOW_ACTIVE_ONLY` (400). The write is journaled and revertible like any other. **Unverified until O-5**: that toggling `sys_hub_flow.active` through the Table API leaves `master_snapshot` unchanged has not been confirmed on a PDI; every flow plan and apply carries an `UNVERIFIED` warning saying so. `test/upsert-artifact.test.js` (+2 tests).
- **UI Builder experience explain (P-14, P-15, `ui` package, opt-in).** New read-only tool `servicenow_explain_ui_experience({sys_id | path, format?})` explains a UI Builder experience or workspace (`sys_ux_page_registry`) as a page map: page properties, app config, routes → screens (with applicability) → macroponents, each macroponent's composition tree, data resources, client state and event wiring (decoded by the new `uib-composition` decoder, `src/core/artifacts/uib-composition.ts`), client scripts, transform / scriptlet data brokers with their `ux_data_broker` ACLs, and (P-15) the workspace landing page, dashboards, list menus (categories, lists, applicability) and form action layouts. `format` is `json`, `markdown`, `mermaid` or `file`; reads are bounded, the call reports progress and honours cancellation, and every unreadable table is a caveat. The `uib-composition` decoder is now registered, so `servicenow_explain_artifact` decodes macroponent `composition` without the `via: "json"` fallback. All table / field names and JSON shapes are unverified (U) until gate O-5; results carry `verified: false`.
- **UI Builder domain rules (P-19, UIB part).** `servicenow_code_health({domains: true})` adds three UI Builder rules: `uib-route-no-screen` (warn — a custom route whose screen type has no screen), `uib-screen-no-applicability` (info; warn when the audience-less screen shadows a later variant of its screen type) and `uib-data-broker-no-acl` (warn — a custom transform / scriptlet broker with no `ux_data_broker` ACL). A capped confirming read reports nothing and marks the rule truncated; an unreadable table disables its rule only. Unverified (U) until gate O-5.
- **UI Builder experiences in the application document (P-21, UIB part).** `servicenow_document_app({detail: true})` also draws a page map for each `workspace` artefact of the scope (first 10), through the `ui` package (a denied `ui` package is a caveat).
- **HTTP transport v2 (H-7).** One `StreamableHTTPServerTransport` and one `McpServer` per MCP session (`src/mcp/http-sessions.ts`), each on its own session runtime — a child of the process runtime: packages, the logging level and list-changed notifications, plan tokens, tasks and write caps are per session; connection pools, breakers, OAuth tokens and the schema cache are shared and keyed by profile + host. A session is created by `initialize`, ends on DELETE or after `SN_HTTP_SESSION_TTL_SEC` idle (default 1800, `0` = never), and at most `SN_HTTP_MAX_SESSIONS` (64) are open (503 + `Retry-After` beyond). DNS-rebinding protection: the `Host` header must match `SN_HTTP_ALLOWED_HOSTS` (default `localhost`, `127.0.0.1`, `[::1]` on a loopback bind; unchecked with a warning on a non-loopback bind) and a browser `Origin` must match `SN_HTTP_ALLOWED_ORIGINS` (default loopback origins), else 403. `GET /healthz` and `GET /readyz` (open; `?probe=1` behind the token runs a cached `sys_user` read); an SSE keep-alive every `SN_HTTP_KEEPALIVE_MS` (25 s); a listener `error` handler; SIGTERM drains in-flight calls for up to 5 s before closing sessions. `servicenow_use_instance` is session-scoped over HTTP and takes `persist` (default: `true` over stdio, `false` over HTTP; a persisted switch writes the env file and is journaled); `get_status` shows the profile source `session` and `server.http.sessionId` / `sessions`. `SN_HTTP_REQUIRE_TOKEN=1` refuses a non-loopback bind without `SN_HTTP_TOKEN` (the default still warns). The stdio transport is unchanged. `test/http-transport-v2.test.js` (18 tests, including an end-to-end run with the SDK's `StreamableHTTPClientTransport`).
- **Settings manifest and startup validation (E-4).** Every environment setting is declared once in `src/core/settings-manifest.ts`: its kind, default, profile scope, aliases, secret flag, section and description. Each value is validated with zod when it is read. The policy, host, auth, logging and config reads go through the manifest, and the runtime `servicenow_use_instance` / `servicenow_set_credentials` flows are unchanged. An invalid value logs one warning and the setting keeps its default. Since B5 an invalid safety value is a startup error instead, and `SN_STRICT_SETTINGS=1` extends that to every setting (see Breaking changes). `doctor` now says which env file was loaded and why (`SN_ENV_FILE` or the user config directory); `doctor --json` also lists invalid settings under `settingsIssues`. The manifest's secret flag drives the D-5 `<KEY>_FILE` resolver. `test/settings-manifest.test.js` (14 tests).
- **Generated env docs and count sync (D-3).** `npm run docs:env` regenerates the README environment reference, `.env.example` and the `server.json` env block from the manifest, between `GENERATED:ENV` markers. `npm run docs:sync` keeps the tool and package counts in step with the registry across README, `package.json`, `server.json`, the extension, the Claude plugin and `docs/index.html`. Both have a `--check` mode, which `npm run check` runs. The guards are `test/env-docs-generated.test.js` (7 tests) and `test/docs-sync.test.js` (3 tests). Test and coverage counts are not synced. `docs/llms.txt` gives LLM clients a short project map.

- **VS Code extension v2 (D-7).** Settings `servicenowMcp.envFile` (`SN_ENV_FILE`; `~`, `${userHome}`, `${workspaceFolder}`), `servicenowMcp.packages` (`SN_TOOL_PACKAGES`, an enum of the registry packages and profiles) and `servicenowMcp.transport` (`stdio` default, or `http`: the extension starts the server's Streamable HTTP transport on a free loopback port with a random per-launch bearer token); a change re-registers the server definition. **Sign In** / **Sign Out** commands keep the instance, the method (basic, API key, OAuth client credentials, bearer token) and the secret in VS Code SecretStorage and pass them to the server through its environment only — injected in `resolveMcpServerDefinition`, never in the cached definition, settings or on disk. A **Get started** walkthrough (sign in, pick packages, run doctor, first prompt) with markdown media; a status-bar **doctor** that runs `servicenow-mcp-ai doctor --json` through the same launcher and writes the report to an output channel. The spawned package range is unchanged (`npx -y servicenow-mcp-ai`). `extension/src` is linted by the root `npm run lint`; the CI extension job runs its unit tests (`extension/src/test`, 14 tests); `test/extension-manifest.test.js` (4 tests) pins the packages enum to the registry.
- **Domain analysers (P-19, flows / portal / legacy workflows).** `servicenow_code_health` takes an opt-in `domains: true` (default off, so the report is unchanged without it) that runs eleven rules and adds a `domains` result (and a "Domain analysers" section in `code-health.md`); each finding has `rule`, `severity`, `ref` (`artifactType`, `table`, `sys_id`, `name`) and `message`. Flows: `flow-run-as-system-protected` (warn — an active flow running as System whose trigger table or a step `table` input is an H-11 protected table), `flow-draft-differs` (info — `latest_snapshot` is not `master_snapshot`; the step-level comparison stays in `servicenow_explain_flow`), `flow-unused-subflow` / `flow-unused-action` (info — a custom subflow or action, outside `sn_*` scopes, that no flow step calls), `flow-integration-no-error-handling` (warn — a REST / SOAP / HTTP / spoke action step with no Try ancestor) and `flow-long-wait` (info — a wait-for-duration over 24 h). Portal: `portal-public-data-widget` (warn — a public widget, or one placed on a public page, whose server script uses `new GlideRecord` / `GlideAggregate` / `GlideQuery` or `$sp.getRecord`), `portal-orphan-widget` / `portal-orphan-page` (info — not placed / not linked from a portal, menu, route map or widget code) and `portal-route-map-loop` (warn — active route maps that redirect in a cycle within a portal). Legacy: `workflow-migration-candidate` (info — a `wf_workflow` still referenced by a catalog item or an SLA definition). Reads are bounded: the newest `limit` candidates per rule (default 50, at most 200), child reads capped at 500 rows per table in chunked `IN` lists, `truncated` on a capped read; an unused / orphan rule never reports a candidate whose callers were not fully read. An unreadable table (policy, ACL, missing plugin) makes only the rules that need it `available: false` with the reason. The UI Builder rules wait for P-14; the rules carry caveats where a field is unverified (gate O-5).
- **Secrets from files and a container image (D-5).** Every secret setting can be read from a file: `SN_PASSWORD_FILE`, `SN_API_KEY_FILE`, `SN_BEARER_TOKEN_FILE`, `SN_OAUTH_CLIENT_SECRET_FILE`, `SN_OAUTH_REFRESH_TOKEN_FILE`, `SN_HTTP_TOKEN_FILE` and the per-profile `SN_PROFILE_<NAME>_<SECRET>_FILE` forms (`src/core/secret-files.ts`). The file is read at startup with one trailing newline trimmed; setting both `<KEY>` and `<KEY>_FILE`, or an unreadable / empty file, exits 1 with a one-line error naming the setting and never the value. A file-sourced value is never written back to the env file (`set_credentials` refuses; a rotated refresh token stays in memory with a warning). `SN_TOKEN_FILE`, `SN_OAUTH_JWT_KEY_FILE` and the `SN_TLS_*_FILE` settings keep their meaning. New multi-stage `Dockerfile` (distroless Node 22, non-root, HTTP on `0.0.0.0:3000` by default, TCP liveness `HEALTHCHECK`, `/data` for docs and the env file), `.dockerignore` (deny-by-default) and `smithery.yaml` (stdio, 12 config keys mapped to `SN_*` settings). The HTTP transport logs a warning when it binds a non-loopback host without `SN_HTTP_TOKEN`. Footprint measurements are in `project/FOOTPRINT.md`. `test/secret-files.test.js` (13 tests), `test/container.test.js` (5 tests).
- **Artefact upsert (P-23).** New `servicenow_upsert_artifact({artifactType, key, fields, children?})` in the opt-in `artifacts` package (95 tools): one plan over a primary record and its child records (e.g. a UI policy and its actions, a data policy and its rules). Each record is matched by its key with the S-8 semantics and planned as `create`, `update` (only the changed fields) or `noop`; the apply writes the parent first, then the children in the given order, inside one update-set binding, each through the journal with its `before` values and one shared `artifact_write` id, so `servicenow_revert_write` restores every line (a create is reverted by delete). Fields are limited to the type's descriptor (name, key, active, script, JSON, reference, applies-to, meta, when and the new `writeFields`); `sys_*` fields other than `sys_scope` on a create, and the child's link to the parent, are refused with `FIELD_NOT_ALLOWED`. A field the registry marks `jsonFields.writable:false` (e.g. `sys_ux_macroponent.composition`) gives a plan with `plan_only` and `would_refuse`, and the apply is refused with `PLAN_ONLY_FIELD` (409). The `flow` and `workflow` groups (flows, subflows, actions, playbooks, decision tables, workflows) and `baseQuery` types are refused with `NOT_WRITABLE_TYPE`; children behind a second parent level (`parentTable` / `parentKey` / `alsoMatch`) with `CHILD_NOT_WRITABLE`. The H-11 table policy and the P-22 SDK guard run for the parent and every child table before the first write; the plan returns `apply_with.expected_action` / `expected_sys_id` (a changed parent decision is `STALE_RECORD`), and under `SN_DESTRUCTIVE_CONFIRM=token` one plan token covers the whole plan. Children not named in the call are left alone; at most 200 children per table (`TOO_MANY_CHILDREN`). New error codes `NOT_WRITABLE_TYPE`, `CHILD_NOT_WRITABLE`, `FIELD_NOT_ALLOWED`, `DUPLICATE_CHILD_KEY`, `TOO_MANY_CHILDREN`, `PLAN_ONLY_FIELD`. The child `writeFields` of `ui_policy` / `data_policy` are unverified until O-5.
- **SDK-managed guard on batch writes and on creates without a scope (P-22 remainder).** `servicenow_batch` judges every write sub-request that targets the Table API (`/api/now/[v1/]table/<table>[/<sys_id>]`): the plan shows `sdkManaged` per sub-request and `would_refuse`; in `deny` mode the apply is refused with `SDK_MANAGED_SCOPE` before the batch is sent; in `warn` mode the result lists `sdkManaged` by sub-request id. A create that names no `sys_scope` on a table under `sys_metadata` is now judged by the session user's current application (the `apps.current_app` user preference, read once per call; no row = global) and the detail says `scope_source: "current_application"`. When the hierarchy or the preference cannot be read the write is judged without a scope and carries a `sdkScopeWarning` — it never fails because of the lookup. The preference name is unverified until O-5. Nothing is read when detection is not configured.
- **Application documentation detail (P-21).** `servicenow_document_app({scope, detail:true})` adds three sections to `apps/<scope>.md` (and `detail` to the JSON companion): **Diagrams** — one Mermaid diagram per flow, subflow and workflow (`explain_flow`, no call expansion) and per portal (`explain_portal`), the first 10 of each type; **Dependencies** — one graph of the outbound references of the app's first 10 artefacts (`artifact_dependencies`, depth 1), merged; **Lint summary** — the P-18 registry sweep limited to the scope (50 newest per type), with the top 20 scripts. Each piece degrades to a caveat (a denied `flows` / `ui` / `artifacts` / `codecheck` package, an unreadable table); without `detail` the document is byte-identical to before. UI Builder experiences are not drawn yet (P-14). `test/document.test.js` (+2 tests).
- **Snapshot / compare over the artefact registry (P-20).** `servicenow_snapshot_instance` and `servicenow_compare_instances` take `types` (registry artefact types, or `["all"]`) and `scope`. The snapshot writes `artifacts/<type>.json` and `.md`: every record of the type with its direct children, normalised so an unchanged instance re-snapshots as `unchanged` — `sys_updated_on` / `_by`, `sys_created_*`, `sys_mod_count` and `sys_tags` dropped, JSON columns decoded with sorted keys, secret fields masked, a sha256 per record over itself and its children. The comparison (live, or from those files with `from_snapshot`) matches records by sys_id, then by natural key (the registry key fields, else the name), and reports `artifactDiffs` — only in a / only in b / different, with the differing fields and per child table the added, removed and changed children; they count toward `drift` only when `types` is given. Unknown types are refused before any request. `test/p20-artifact-snapshot.test.js` (8 tests).
- **Mermaid diffs and published flow snapshots in compare (P-20 remainder).** `servicenow_compare_instances({types, mermaid: true})` renders each side of a changed record of a diagram type (`flow`, `subflow`, `flow_action`, `workflow`, `playbook`, `sp_portal`, `sp_page`, `workspace`) with the existing explainer and Mermaid builder and returns `mermaidDiffs` — a unified diff of the two Mermaid sources per record (`src/api/artifact-mermaid.ts`) — plus a "Mermaid diffs" report section. It is opt-in (default `false`; ignored without `types`), live even with `from_snapshot`, bounded to 10 records per comparison (a warning names the rest) and 120 diff lines per record (`cut`), notes nodes dropped by `SN_DIAGRAM_MAX_NODES` (`nodesTruncated`), and never counts toward `drift`; a render failure is a warning. The artefact snapshot now reads a flow's children from its published snapshot (`sys_hub_flow.master_snapshot` → children keyed by the snapshot sys_id) when that snapshot has rows: the record carries `source: "published"` and `snapshot`, and the result warns that this path is unverified until O-5. A flow without a `master_snapshot`, or whose snapshot has no child rows (a warning says the draft was read), is read from the draft as before. `test/p20-mermaid-published.test.js` (9 tests).
- **Registry-driven lint and search (P-18).** New lint rules: `sce-trust-as-html` (`$sce.trustAsHtml` / `trustAs`, client), `sanitize-bypass` (`$sceProvider.enabled(false)`, client, error) and `sp-param-unvalidated` (a `$sp.getParameter` value — directly or through a variable — reaching `addEncodedQuery`, `new GlideRecord(…)`, `new GlideAggregate(…)`, `gs.eval` or `GlideEvaluator.evaluateString`; `addQuery` and `get` are not flagged; server). `servicenow_lint_script` accepts the opt-in registry types (UI Builder client scripts and data-broker scripts, portal angular providers, search sources…). `servicenow_code_health({extended:true, limit?})` also counts the opt-in types and sweeps every registry script type instance-wide — the `limit` (default 50, max 200) most recently updated records per type, every non-markup script field, client fields with the client rules — into a new `artifacts` block and a report section; an unreadable type is a warning. `servicenow_where_used({extended:true})` also searches the opt-in types. `test/p18-registry-lint.test.js` (6 tests).
- **Optimistic concurrency, unknown fields and a bounded email preview (H-3 remainder).** `servicenow_update_record` and `servicenow_delete_record` plans return `apply_with.expected_mod_count` (the record's `sys_mod_count`); an apply that passes it is refused with `STALE_RECORD` (409) when the record changed since, before anything is written — omit it to apply unchecked. Create / update / upsert plans list `unknown_fields` when the schema cache already describes the table and a written field is not in it (nothing is read to find out; dot-walked names are skipped). The `send_email` plan shows the body in full up to 2,048 characters, otherwise `body_preview` + `body_truncated`, always with `body_chars` and `body_sha256`. The H-3 plan-token binding ignores `expected_action` / `expected_sys_id` / `expected_mod_count`, so a planned upsert, update or delete can be applied with both. `test/h3-remainder.test.js` (5 tests).
- **SDK-managed write guard (P-22).** New `SN_SDK_MANAGED_WRITES=warn|deny|allow` (default `warn`). When P-3 detection is configured (`SN_SDK_MANAGED_SCOPES` / `SN_SDK_PROJECT_DIRS`), `create_record`, `update_record`, `upsert_record`, `delete_record`, `set_property` and `revert_write` check the scope of the record they write — the `sys_scope` in the written values and the record's current one (one narrow read) — after the table policy: `warn` adds an `sdkManaged` block (scope, evidence, the Fluent alternative) to the plan and to the applied result; `deny` marks the plan `would_refuse` and refuses the apply with `SDK_MANAGED_SCOPE` (409). Tables without `sys_scope` and unconfigured servers are untouched (no extra request). `test/sdk-guard.test.js` (9 tests).
- **Policy model v2 (H-11).** One table-policy evaluator (`evaluateTable` in `src/core/policy.ts`) now backs every guard, a new always-on admin tool `servicenow_explain_policy({table?, action?})` (94 tools) and a new resource `servicenow://policy`, so what the model is told is what is enforced. Table lists take glob patterns (`*`, `?`): `SN_TABLES_DENY=sys_*` blocks `sys_user` and leaves `incident`; the order is exact deny, exact allow, pattern deny, protected tables, allowlist patterns (with exact entries only, the old behaviour). Policy denials carry `code: "POLICY_DENIED"` (messages unchanged). `SN_PROTECTED_TABLES_WRITE=deny` refuses writes to a built-in list of identity, role, ACL, property, OAuth, script, LDAP, certificate, data-source and REST-message tables unless `SN_TABLES_ALLOW` lists one exactly (default `allow`: no change). `SN_IMPORT_SET_TABLES` restricts import-set staging tables to patterns. Per-session write caps `SN_MAX_WRITES_PER_SESSION`, `SN_MAX_DELETES_PER_SESSION` and `SN_MAX_BATCH_WRITES` (unset = no cap) refuse with `WRITE_CAP` before any request, journaled with `cap_hit`; `get_status.writes.caps` shows the usage. Environment marker `SN_ENV` / `SN_PROFILE_<NAME>_ENV=prod|test|dev` and per-profile `SN_PROFILE_<NAME>_WRITE_MODE`: a `prod` profile stays in plan mode (and says why: `write_mode_hold`) unless `SN_PROD_WRITES` / `SN_PROFILE_<NAME>_PROD_WRITES=I_UNDERSTAND`, confirms destructive applies even in apply mode (`CONFIRM_REQUIRED` for a client without elicitation), tags results `_meta.environment`, and is named in the server instructions, `get_status.profileDetails`, `doctor`, the overview prompt and `use_instance` (with a warning). `test/policy-h11.test.js` (17 tests, including a glob property test and the explain-equals-guard grid).
- **Policy-axis bypass closure (H-4).** Every path to the instance now gets a policy verdict. Attachments: `get_attachment`, `download_attachment` and `delete_attachment` check the parent record's table (`table_name`), and an unscoped `list_attachments` drops rows of a denied table. The Change, Catalog, Knowledge, Email and ATF tools check their backing tables (`change_request`, `conflict`, `sc_catalog`, `sc_category`, `sc_cat_item`, `sc_request`, `sc_req_item`, `kb_knowledge`, `sys_email` plus a send's associated table, `sys_atf_test`, `sys_atf_test_suite`) before any request; Code Search API hits on a denied table are dropped. `servicenow_batch`: a nested batch is refused; tables named in a sub-request's query (`table_name`, attachment `sysparm_query`) or body (Email `table_name`, IRE item classes) are policed; an attachment addressed by sys_id is resolved to its parent table (one metadata read, only while `SN_TABLES_ALLOW` / `SN_TABLES_DENY` is set) and an unscoped attachment list is refused then; new `SN_BATCH_UNMAPPED=allow|deny` (default `allow`) and `SN_BATCH_MAX_REQUESTS` (default 1000); `sn_cicd`, `sn_codesearch` and `identifyreconcile` are mapped to their packages; plan previews show each sub-request's body (redacted), tables and package. `test/policy-h4.test.js` (17 tests) sweeps every tool with an `apply` argument under `SN_READONLY` and with its target table denied, and pins every REST prefix in `src/api` to a package (GA-7).
- **Plan tokens and confirmation for destructive applies (H-3, opt-in).** New `SN_DESTRUCTIVE_CONFIRM=off|token|elicit` (default `off` — no behaviour change) and `SN_PLAN_TOKEN_TTL_SEC` (default 600). With `token`, in plan mode, the plan preview of `servicenow_delete_record`, `servicenow_delete_attachment`, a writing `servicenow_batch`, `servicenow_send_email`, `servicenow_order_catalog_item` and `servicenow_revert_write` returns `plan_token` + `plan_token_expires_at`, and `apply:true` must pass that token back with the same arguments: a missing, unknown, used, expired, other-tool, other-profile or changed-argument token fails with `PLAN_REQUIRED` (status 428) before any request is sent. Tokens are opaque, letters-only (so `SN_REDACT_PII` cannot mangle them), single-use, bound to tool + profile + a sha256 of every argument except `apply` / `plan_token` / `instance`, held in the runtime container (at most 500) and journaled on the applied line (`plan_token`). With `elicit`, a client that supports elicitation is also asked to confirm; a decline or a failed prompt fails with `CONFIRM_DECLINED` (403) and is journaled as `refused`. `SN_WRITE_MODE=apply` bypasses both. The six tools gain an optional `plan_token` argument (tools/list `all` +618 bytes); `get_status.policy.destructiveConfirm` and the server instructions report the mode. Not in this change: the 3.0 default (O-4), `change_conflicts(calculate:true)` (becomes a plan/apply write with H-4), `STALE_RECORD` on delete, the email `body_preview` cap and `unknown_fields` in previews. `test/plan-token.test.js` (16 tests).
- **Native discovery (S-16).** `servicenow_document_instance` takes an optional `depth: "overview" | "apps" | "artefacts"` (cumulative) that writes `<profile>/discovery/`: `overview.md` (version, counts, automation, the files written), `apps.md` (one row per scope with links), `tables-<scope>.md` (tables and their dictionary) and `artifacts-<scope>.md` (every registry type with a "Collected / not collected and why" column — collected, cap, unverified, no such table, unreadable, package off, no records — then the artefacts per group). Scopes are the named `apps`, else every non-global `sys_app` scope up to the per-run cap, the rest reported as skipped. New `DOC_KINDS` `discovery_overview`, `discovery_apps`, `discovery_tables`, `discovery_artifacts`. Omitting `depth` is unchanged.
- **Plugin skills (D-8, partly).** The Claude Code plugin ships `skills/sn-discover`, `sn-triage`, `sn-impact`, `sn-drift` and `sn-safe-write`; they orchestrate the server's tools and hold no credentials. `test/plugin-skills.test.js` asserts that every `servicenow_*` name in a skill exists in the tool manifest (L4-02). The `PreToolUse` hook that blocks a token-less apply waits for H-3.
- **Artifact dependency graph (P-17, `artifacts` package).** New read-only tool `servicenow_artifact_dependencies({artifactType, sys_id? | key?, direction?, depth?, limit?, format?})`. Outbound edges come from registry reference fields on the record and its child rows, decoded JSON fields (flow step values, widget options, UI Builder data) and script text (script-include calls, `GlideAjax` classes, literal `GlideRecord` / `GlideAggregate` tables); inbound edges come from reverse reference queries and, for a script include, script callers (`search_code`, re-checked for a real call), flow steps whose decoded values call it and the S-9 structural pass (for a table, the structural pass). Breadth-first per direction with `depth` 1–3 (default 1), a visited set per direction (cycles are safe) and a 150-node cap (`truncated`); every failed source read becomes an `unavailable` entry. `format: "mermaid"` returns a `graph LR` diagram with the counts instead of `nodes` / `edges`. Publishes an `outputSchema`. The `all` `tools/list` budget rises to 134,000 bytes.
- **Dynamic packages and listChanged (M-5).** Three new always-on admin tools: `servicenow_list_packages` (every package with `enabled` / `configured` / `denied` / `readOnly` and its tool count), `servicenow_enable_package({name})` and `servicenow_disable_package({name})`. Every policy-permitted tool is now registered at startup and the ones outside the enabled packages are disabled, so a client that starts with `core` can pull `codecheck` (or any other package) in without a restart; the server declares `tools.listChanged` and emits one debounced `notifications/tools/list_changed` per toggle. Toggles are session-scoped (reset when the MCP session closes) and never exceed the policy axes: a package in `SN_PACKAGES_DENY` (checked at startup and again at enable time) is refused with `PACKAGE_DENIED`, and a package in `SN_PACKAGES_READONLY` stays read-only (its write tools are never registered). Package resources and templates follow the package; prompts declare the packages they need and appear / disappear with them (`prompts/list_changed`). The server declares `resources.subscribe` + `listChanged`; `servicenow_use_instance` and `servicenow_set_credentials` send `notifications/resources/list_changed` and `notifications/resources/updated` for `servicenow://status` subscribers. New always-on prompt `servicenow_instance_overview({goal?})`: capabilities first, then status and packages, with a production caution until H-11's environment marker lands. The default `tools/list` for `core` / `all` is unchanged apart from the three admin tools — 92 tools in 26 packages.
- **A real CLI and an `init` wizard (D-1).** `servicenow-mcp-ai` now parses its arguments with `node:util` `parseArgs` (`src/cli.ts`): `--help`, `--version`, and the commands `init`, `doctor`, `login`, `drift` and `support-bundle`; no command starts the MCP server over stdio exactly as before. `init` asks for the instance, the auth method (`basic` / `oauth` / `apikey` / `token`, with the OAuth grant) and the credentials through a hidden prompt, writes them with the existing atomic `0600` env-file writer (profile-scoped with `--profile`), then runs `doctor`; answers can be piped on stdin, and without a terminal or piped answers it refuses with exit 2. `doctor` now starts with the env file it used, and takes `--json` (one document with `checks[]` and the `get_status` payload), `--ascii` (automatic when stdout is not a terminal and on Windows outside Windows Terminal) and `--profile <name>`; the exit codes are unchanged. `support-bundle` writes one JSON file for a bug report — the doctor JSON, the `SN_*` settings with secrets masked (and scrubbed from the whole file), `npm ls --omit=dev`, the tool manifest summary and the `SN_LOG_FILE` tail — and prints its path.
- **Server instructions and configuration state (M-1).** At `initialize` the server now sends `instructions` generated from the live registry and configuration: version, active profile (and the others), whether credentials are configured — with the missing keys and how to fix them — the enabled packages and tool count, and the write mode (plan / apply / read-only). They never carry a user name, secret or env-file path and are capped at 2 KB. `serverInfo` gains `title`, `websiteUrl`, `description` and an inline SVG icon. An instance call without credentials now fails with `error.code: "NOT_CONFIGURED"` and a hint naming `servicenow_set_credentials` (the message is unchanged; `test_connection` carries the same code and hint). `servicenow_get_status` adds, without changing any existing key: `server` (name, version, pid, uptime, start time, Node, transport, HTTP bind and whether a token is set), `writeMode`, `policy` (`writeMode`, `readOnly`, `summary`), `redaction`, `docs` (directory, exists, writable), `limits`, `writes` (applied / failed / refused / local counters since startup), `profileSource` and `profileDetails` (per-profile auth mode, grant, refresh-token state, write mode, missing keys). The README has a first-run transcript. The core `tools/list` budget rises to 32,000 bytes.
- **Custom actions, call expansion and decision tables (P-11).** `servicenow_explain_flow` gains `kind: "action"`: a custom action (`sys_hub_action_type_definition`) is explained with its header (internal name, category, access, active, description), its inputs (`sys_hub_action_input`), outputs (`sys_hub_action_output`) and ordered steps (`sys_hub_step_instance`, one bounded read). For a flow or subflow, subflow and action calls are now expanded: the new `depth` input (default 1, max 3, `0` = none) resolves each called action's steps and each called subflow's step tree level by level, with a cycle guard (a callee already on the call path is marked `cycle` and not re-read), at most 20 distinct callees per call and one batched read per table per level; `counts.callees` reports the expansions, the Mermaid graph attaches callee steps with dotted edges and the Markdown nests them under a "calls" line. Decision tables get an explainer through `servicenow_explain_artifact` (`artifactType: "decision_table"`, `kind: "decision-table"`): the answer table, the inputs (`sys_decision_input`) and the rows (`sys_decision_question`) in order with their condition and answer, default and inactive rows marked; the registry row is now `R` + `X`. Every table and field involved is `verified: false` (gate O-5) and the explanations say so.
- **Flow and workflow explain (P-10, P-13, `flows` package).** New read-only tool `servicenow_explain_flow({sys_id?, kind?, runs?, migration?, format?})`. For a Flow Designer flow or subflow (`kind: "flow" | "subflow"`) it returns the trigger, the ordered step tree built from `ui_id` / `parent_ui_id` across the v1 and `_v2` action, logic and subflow instance tables (nested at most 32 levels; orphans, cycles and cut depth are caveats), the inputs, outputs, flow variables and stages, `run_as` / `run_with_roles`, and each step's `values` decoded by the new `flow-values` decoder (plain JSON, then base64 + gzip JSON, else `decoded:false` with the byte length — never throws) with data pills labelled from `label_cache`. Draft vs published compares the flow with its published snapshot. For a legacy workflow (`kind: "workflow"`) it returns the activity graph of the published `wf_workflow_version` (activities, conditions, transitions), the `wf_stage` list and, with `migration: true`, a migration report: the catalog items, SLA definitions and running contexts that still use the workflow (or every workflow in use, when no `sys_id` is given). `runs` (0 – 20, off by default) adds the latest `sys_flow_context` runs with their `sys_flow_log` errors, or the latest `wf_context` runs. `format`: `json` (default), `markdown`, `mermaid` (a `flowchart TD` step tree or activity graph) or `file`. Every table is `verified:false` (O-5): a table that is denied, forbidden or missing becomes a caveat plus an `unreadable` entry, never a failure. Progress and cancellation are reported per read. `servicenow_get_flow` is unchanged. 89 tools in 26 packages.
- **Instance document (S-15).** `servicenow_document_instance({profile?, tables?, apps?, kinds?, write?, format?})` (`docs` package) writes `<profile>/README.md` from the E-7 collectors — version and build (`glide.buildname` / `builddate` / `buildtag` / `war` from `sys_properties`), counts, applications with links to their documents, active plugins, automation counts, update sets in progress, a domain-separation caveat; the generation time is in the frontmatter only — then a table / app document for each named `tables` / `apps` target (at most 50 each), one document per requested `kinds` entry (`security`, `catalog`, `integrations`), and `<profile>/artifact-types.md` from the artefact-type catalogue with a "Collected in this run" column (ID-29). Each document is written as soon as it is built, with one progress notification per document (message = its path). A named target that fails is reported in `failed` and the run goes on; on cancellation the files already written stay valid and index.json `runs.servicenow_document_instance` records `partial: true`. `write: false` returns the Markdown instead; `format: "file"` writes the run summary to a file. `servicenow_snapshot_instance` is unchanged.
- **Catalog and integrations document kinds (S-15).** `DOC_KINDS` gains `catalog` (`<profile>/catalog.md`: catalogs → categories → items → item variables, from `sc_catalog`, `sc_category`, `sc_cat_item`, `item_option_new`) and `integrations` (`<profile>/integrations.md`: scripted REST APIs, outbound REST messages, transform maps and data sources, descriptive fields only — no credentials, connection strings or scripts), plus the `instance` and `artifact_types` kinds behind the instance document. A source table the user cannot read becomes a Caveats line, never a failure.
- **Long-running operations as MCP tasks (M-9, experimental).** With `SN_EXPERIMENTAL_TASKS=1`, `servicenow_snapshot_instance`, `servicenow_compare_instances`, `servicenow_run_atf_test`, `servicenow_run_atf_suite`, `servicenow_code_health` and `servicenow_query_table` (`format: "file"` only) take an optional `run_as_task: true`. The call returns a task handle at once (`{task: {taskId, status, ttl, pollInterval}}` with `_meta["io.modelcontextprotocol/related-task"]`) and the work continues in the background. The server declares the `tasks` capability (`list`, `cancel`) and answers the standard `tasks/get`, `tasks/result`, `tasks/list` and `tasks/cancel` through the SDK's experimental task API. Tasks live in an in-memory store per runtime: kept one hour from creation, results stored redacted, bound to the creating HTTP session, and `tasks/cancel` or expiry aborts the in-flight ServiceNow requests. Native `params.task` augmentation on `tools/call` is not advertised. With the flag off, the tool schemas and capabilities are unchanged. This is experimental and may be removed without a major version if the SDK drops the API.
- **`run_as_task` on the document generators (M-9, ID-15).** With `SN_EXPERIMENTAL_TASKS=1`, `servicenow_document_app` and `servicenow_document_instance` also take `run_as_task: true`; `tasks/cancel` stops the run at the next document. The shared `run_as_task` description is shorter, so the `all+tasks` surface grows by only 18 B (120,799 → 120,817, within its 120,832 B budget); `core` and `all` are unchanged (0 B).
- **Service Portal tree (P-16, `ui` package, opt-in).** New read-only tool `servicenow_explain_portal({portal | page})` explains a portal (by `url_suffix` or sys_id) or one page (by page `id` or sys_id) as a tree: portal → pages (with their roles) → containers → rows → columns → widget instances, with nested rows followed to `depth` levels (default 3, max 6). Widget instances decode `widget_parameters` and map each value onto the widget's `option_schema` (name, label, type, set / default); keys the schema does not name are listed in `unknownOptions`. Each widget lists its dependencies with their JS and CSS includes, its Angular providers and its `sp_ng_template`s; the result also carries the theme with its header / footer, the menu and the page route maps. The full layout is read for the first 5 portal pages (homepage first); the others are listed. `format`: `json` (default), `markdown`, `mermaid` (a `flowchart TD` layout tree with `mermaidTruncated` when capped by `SN_DIAGRAM_MAX_NODES`) or `file`. Every Service Portal table is `verified:false` (O-5): a table that is denied, forbidden or missing becomes a caveat plus an `unreadable` entry, never a failure, and fields the instance did not return are listed in `missingFields`. Progress and cancellation are reported per read. 88 tools in 26 packages.
- **Structured results and a token budget (M-6).** 30 read tools with a stable shape (`get_status`, `test_connection`, `list_instances`, `check_capabilities`, `list_tables`, `describe_table`, `query_table`, `get_record`, `aggregate`, the script, flow, codecheck and ATF reads, the artifact and update-set reads) publish an `outputSchema` and return `structuredContent` next to the unchanged text content. The schemas are permissive (`additionalProperties: true`), and an error result never carries `structuredContent`. Tool descriptions are capped at 250 characters and the automatic `instance` parameter now reads "Profile name (default: active)". The tool manifest moves to version 2: sorted-key `inputSchema`, `outputSchema`, `description_sha256` and `since` per tool. A test holds the `tools/list` size at 125,000 bytes (all packages) and 31,000 (core).
- **Operations and data health (S-10b, `ops` package, opt-in).** Two read-only tools. `servicenow_ops_read({kind})` gives bounded "why is it slow" views: `syslog` (entries of the last `minutes`, default 60 and at most 1440, at or above `level`, default `warning`, with an optional `source` fragment — counts by level, top sources and the newest rows, messages capped at 500 characters), `jobs` (the `sys_trigger` scheduler queue by state, the count of ready jobs more than `overdue_minutes` past their next action, and the `overdue` / `running` / `queued` jobs with their claiming node), `email_queue` (outbound `sys_email`: send-ready backlog and oldest entry, counts by type in the window, recent send failures), `semaphores` (`sys_semaphore` rows) and `overview` (the counts of every section in one call). Rows are capped by `limit` (default 25, max 200). `servicenow_data_health({table})` is the data twin of `servicenow_code_health`: duplicate groups over `key_fields` (Aggregate API grouping with count > 1), and per reference field — named, or the first 10 non-system ones — the orphaned references (target row missing) and stale references (target row with `active=false`), each with the encoded query that lists the rows; an optional `query` scopes every check (`^NQ` and `ORDERBY` are refused). Field names are checked against the dictionary before any count. Every section and every check degrades on its own to `available: false` with the reason; an unreadable table never fails the call. The syslog level values, the `sys_trigger` state values, the `sys_email` type values and the `sys_semaphore` table are not yet checked on a live instance (O-5). New prompt `servicenow_why_is_it_slow` (optional `symptom` and `table`) walks through these reads and the logic that runs on the table. 78 tools in 22 packages.
- **Catalog, quality, AI and application descriptors (P-8).** The artefact registry gains `verified:false` descriptors from SDK-PARITY §4.9 – §4.12. The catalog rows cover the definition records, not the `sn_sc` consumer view: `catalog_item`, `record_producer`, `variable_set`, `catalog_variable` and `catalog_ui_policy`. The quality rows are `atf_test`, `atf_test_suite`, the instance-scan checks (`linter_check`, `script_only_check`, `column_type_check`, `table_check`), `assessment` and `risk_assessment`. The AI rows are `ai_agent`, `ai_agentic_workflow` and `now_assist_skill`. The application rows are `application` (with its scope and package dependencies), `app_dependency`, and two read-only rows, `customer_update` and `source_control`. A catalog item or record producer now reads its own variables and their `question_choice` values, its variable sets and their variables, and the catalog client scripts, catalog UI policies and policy actions of the item and of its sets, plus its categories and user criteria. `servicenow_explain_artifact` returns a `catalog-form` explanation for these. It lists the variables in `order` with their type, whether they are mandatory, and their choices. It also lists each variable set with its variables, the client scripts with the variable they watch, and each UI policy with its effect on each variable when the condition holds and, with reverse-if-false, when it does not. A variable reference of the form `IO:<sys_id>` is resolved to the variable name. `catalog_variable` and `catalog_ui_policy` explain on their own, and `catalog_client_script` is now explained too. The AI families (`sn_aia_*`, `sn_nowassist_skill_*`) are marked `licensed` in the registry and in `servicenow://artifact-types`. When their table is absent, `servicenow_list_artifacts`, `_get_artifact` and `_explain_artifact` answer `available: false` with `requires: "<plugin>"` instead of an error. A child table that is absent or forbidden degrades that child only. No new tools, and the script tools read the same tables as before.
- **Operations reads and writes (S-10a).** Six new tools in three new opt-in packages. `history`: `servicenow_get_record_history` reads a record's field changes from `sys_audit` and its journal entries from `sys_journal_field` and merges them newest first. This covers comments and work notes, which read back empty through the Table API (C-5). It has filters for `source`, `fields`, `since`, `limit` and `value_max_chars`, and each source degrades on its own under an ACL or policy denial. `properties`: `servicenow_get_properties` reads by exact name or prefix, masks password-type and secret-looking values, and truncates long values. `servicenow_set_property` sets one existing property through plan/apply and the write journal, and `servicenow_revert_write` can undo it unless the property is secret. It honours `SN_READONLY` and `SN_PACKAGES_READONLY`, and a missing property fails with the new error code `PROPERTY_NOT_FOUND`. `directory`: `servicenow_lookup_directory` looks up users, groups and roles; with `include_details` it adds roles, groups, members and contained roles, and each detail table degrades on its own. The `cmdb` package gains `servicenow_list_ci_relations` (`cmdb_rel_ci`, inbound and outbound, filtered by type name or sys_id) and `servicenow_identify_reconcile`. That tool sends an IRE payload; its plan calls the identify-only endpoint, and applying is journaled but cannot be reverted. `servicenow_run_atf_test` and `servicenow_run_atf_suite` take `wait_seconds` (0–300) and poll the CI/CD progress under the request's cancel signal with progress notifications. On timeout they return `wait.state: "running"` plus a `tracker` id. `servicenow_insert_import_set_row` adds `import_set_run` (the `sys_import_set_run` row) and `transform_maps` (with `used` flags) to its result, and a failed follow-up read turns into `warnings`. All of this is additive, and no existing tool changes shape.
- **Document generators (S-15).** Two new `docs` tools write a metadata-only document plus a `.json` companion into the docs store, or return the Markdown with `write: false`. `servicenow_document_table({table, profile?, write?, diagrams?, columns?})` writes `<profile>/tables/<table>.md`: the inheritance chain, own columns then inherited columns per parent table, the columns that reference the table, an ER and a record-lifecycle diagram, business rules, client scripts, UI policies, UI actions and ACLs (with their roles from `sys_security_acl_role`), a caveats section (unreadable definition tables, capped reads, domain-separation visibility) and a manual Purpose block that survives re-runs. `servicenow_document_app({scope, profile?, write?})` writes `<profile>/apps/<scope>.md`: the `sys_app` / `sys_store_app` record, the scope's tables with an ER diagram, roles, cross-scope privileges and every artefact-registry type grouped by registry group, with types not confirmed on the instance (O-5) and unreadable types listed separately; property and preference values and descriptor secret fields are never written. `global` is refused. Both are driven by a `DOC_KINDS` registry with a per-kind `generatorVersion`; a run is recorded in `index.json` `runs` (partial on failure or cancellation), a re-run over unchanged metadata reports `unchanged` and rewrites nothing, and one progress step is sent per document. The registry also has an internal `security` kind (ACL matrix plus every S-3 check) that no tool exposes yet. The `servicenow_document_table` prompt now calls the tool and only fills the Purpose block. Prompts are registered only when their packages are enabled (triage: `table`; change impact: `change` or `table`; document table: `docs` and `scripts`). The trace and where-used results report how many diagram nodes the node cap left out (`truncated` / `mermaidTruncated`); where-used no longer has its own 40-edge cap and is bounded by `SN_DIAGRAM_MAX_NODES` only. 78 tools in 21 packages.
- **UI Builder, Service Portal, Flow Designer and legacy workflow descriptors (P-9).** The artefact registry gains read-tier (`R`) descriptors, each with its child tables and script fields, for Next Experience (`workspace`, `dashboard`, `ux_list_menu_config`, `ux_applicability`), UI Builder (`uib_app_config`, `uib_route`, `uib_screen_type`, `uib_screen`, `uib_macroponent`, `uib_client_script`, `uib_client_script_include`, `uib_data_broker_transform`, `uib_data_broker_scriptlet`, `uib_event`, `uib_component`, `uib_theme`, `uib_style`, `uib_form_action`, `uib_form_action_layout`, `uib_composite_definition`), Service Portal (`sp_portal`, `sp_page`, `sp_ng_template`, `sp_dependency`, `sp_angular_provider`, `sp_js_include`, `sp_css_include`, `sp_theme`, `sp_menu`, `sp_header_footer`, `sp_page_route_map`, `sp_css`, `sp_search_source`; `sp_widget` gains its angular-provider link table), Flow Designer (`flow`, `subflow`, `flow_action`, `flow_trigger_definition`, `flow_context`, `playbook`, `playbook_context`, `decision_table`) and legacy workflows (`workflow` now also reads `wf_condition` and `wf_stage`, `workflow_activity_definition`, `workflow_context`). All are `verified:false` until checked on a live instance, so `servicenow_list_artifacts` / `_get_artifact` / `_explain_artifact` serve them with a caveat and degrade when the table is missing. Nine of them carry script bodies and are **opt-in script types** (new registry flag `scriptToolsOptIn`): `uib_client_script`, `uib_client_script_include`, `uib_data_broker_transform`, `uib_data_broker_scriptlet`, `sp_ng_template`, `sp_angular_provider`, `sp_theme`, `sp_css` and `sp_search_source`. `servicenow_list_scripts`, `_get_script` and `_search_code` accept them as an explicit `type`, and `servicenow_search_code` gains `extended: true`, which adds them to an all-types sweep (LIKE path only, the Code Search API is skipped; an unreadable opt-in table is listed under `unreadable`). Without `type` or `extended` the script tools, lint, code health, snapshot, compare, where-used and the capability matrix read exactly the same tables as before. No new tools.
- **File-based delivery for large results (S-11).** `servicenow_query_table` takes `format: "file"` (with `fileFormat: "csv" | "jsonl"`, default `csv`): the rows are streamed page by page into `<SN_DOCS_DIR>/<profile>/exports/<table>-<timestamp>.<ext>` and the result is `{format, file_format, rows, total, path, file, bytes, preview}` plus the usual completeness fields — `path` is docs-relative, `file` absolute, `preview` the first 2,000 characters (`preview_truncated: true` when the file is longer). Rows are redacted (`SN_REDACT_FIELDS` / `SN_REDACT_PII`) before they are written; CSV uses the shared encoder with the formula guard and BOM (`SN_CSV_FORMULA_GUARD`, `SN_CSV_BOM`), writing the header and BOM once. `SN_MAX_RECORDS` still caps a `fetchAll` export. `servicenow_snapshot_instance` and `servicenow_compare_instances` take `format: "json" | "file"`; `file` writes the full redacted result to `exports/snapshot-<timestamp>.json` / `exports/compare-<a>-vs-<b>-<timestamp>.json` and returns its scalar fields, array counts (`<key>_count`) and the file fields. `servicenow_generate_er_diagram` and `servicenow_generate_table_flow` take `format: "inline" | "file"`; `file` writes raw Mermaid to `<profile>/diagrams/er-<tables>.mmd` / `flow-<table>[-<operation>].mmd` (replaced on a re-run, not listed in the docs index) and returns the other fields plus the file fields instead of `mermaid`. Export files are written through a temp file and renamed into place (a failed or cancelled export leaves nothing behind), go through the docs store's path confinement and are recorded in the write journal as `local_write`. A result over `SN_MAX_RESULT_CHARS` is never cut silently: the query truncation note now names `format:"file"`, and a snapshot, compare or diagram result over the budget is returned in full with a `note`; the new `SN_OVERSIZE_TO_FILE=true` writes such results to a file automatically. `fetchAll` gains an `onPage` callback that streams pages without accumulating them. Default outputs are unchanged. No new tools.
- **Core, server-logic and classic-UI artefact types, with explain enrichers (P-7, `artifacts` package, opt-in).** The artefact registry gains 31 types from SDK-PARITY §4.1 – §4.3, all `verified:false`: `table` (with its dictionary, dictionary overrides, field labels and choices), `choice_set`, `state_model` (states, transitions and transition conditions), `property`, `user_preference`, `role`, `cross_scope_privilege`, `user_criteria`, `field_style`, `schedule`, `event`, `relationship`, `ldap_server`, `js_module`, `rest_message` (functions, headers and function parameters), `graphql_api`, `alias`, `alias_template`, `retry_policy`, `data_lookup`, `email_notification`, `inbound_email_action`, `sla`, `data_policy` (with its rules), `workspace_form_action`, `form` (sections and elements), `ui_section`, `list`, `application_menu`, `ui_view` and `list_control`; `acl` now reads its required roles. Children can be linked by a value of the parent other than its sys_id (a table's dictionary by table name, a choice set's choices by table name and element); a parent value containing `^` or a line break is never put into a query. `servicenow_explain_artifact` adds a structured `explanation` for a state model (its states and `from -> to` transitions with their conditions), a choice set or table (a choice table per element, ordered by sequence) and a UI policy or data policy (the effect on each field when the condition holds and, with reverse-if-false, when it does not); an enricher that fails becomes an `error` explanation and never fails the call, and a large explanation is cut to the output budget. When a `verified:false` type's table is rejected, `servicenow_list_artifacts`, `servicenow_get_artifact` and `servicenow_explain_artifact` check `sys_db_object` and add `available: false` when the table does not exist on the instance (`true` when it does; left out when the check cannot answer). The script tools scan the same tables as before. No new tools.
- **Update-set awareness (S-6, `updatesets` package, opt-in).** Three read-only tools: `servicenow_list_update_sets` (filters `state`, `name`, `application`, `query`, paging; the user's current set is marked), `servicenow_get_update_set` (a set's `sys_update_xml` rows per artefact — type, target name, action, table — with `by_type` / `by_action` counts; payloads only with `include_payload`, parsed into capped field values with secret-looking fields masked) and `servicenow_compare_update_set` (each artefact against another profile, read live, or a profile's snapshot: `same` / `different` with field names / `missing` / `not_comparable` / `not_covered` / `unknown`). Write binding: `servicenow_create_record`, `_update_record`, `_upsert_record` and `_delete_record` take an optional `update_set` (sys*id or exact name), defaulting to the new `SN_UPDATE_SET` setting (`SN_PROFILE*<NAME>\_UPDATE_SET`per profile). The plan preview names the target set and says when the table holds data rows that update sets do not capture; apply switches the user's`sys_update_set`preference (plus`updateSetForScope<scope>`for a scoped set), runs the write, restores the previous value and reports`update_set: { bound, previous, restored }`; the journal entry records `update_set`. A set that is not in progress is refused with `UPDATE_SET_NOT_IN_PROGRESS`, an unknown one with `UPDATE_SET_NOT_FOUND`. Without the argument or the setting nothing changes. `servicenow_check_capabilities`reports`update_sets.detail.canRead`/`canSet`/`configured`.
- **Trace v2 lanes (S-5).** `servicenow_trace_table_event` and `servicenow_generate_table_flow` take an optional `lanes` array that adds opt-in lanes to the trace, each in its own phase and Mermaid subgraph: `transform_map` (active `sys_transform_map` rows targeting the table, drawn as an entry point), `scheduled_job` (active `sysauto_script` jobs whose script names the table — a text match), `client` (client scripts and UI policies of the table chain; a `query` keeps only `onLoad` scripts and on-load policies), `data_policy` (`sys_data_policy2`, not on delete or query), `sla` (`contract_sla` definitions with their start condition, not on delete or query) and `event_script` (`sysevent_script_action` rows for the events registered on the table chain in `sysevent_register`). Parent-table artefacts are included only when their inherit flag is set and carry `inherited_from`. Lane entries are sorted independently of the backend's row order, so a snapshot compare of a traced flow is stable; a lane whose table cannot be read becomes a warning and the rest of the trace stands; an unknown lane is refused before any request. The result gains `lanes` only when lanes were asked for — without `lanes` both tools read nothing new and their output is byte-identical. The lane field names are not yet checked on a live instance (O-5). No new tools.
- **Completions, reference resources and an untrusted-content boundary (M-4).** The resource templates answer `completion/complete`: `servicenow://schema/{table}` and `servicenow://{profile}/schema/{table}` complete table names from the schema cache (tables already described, their reference targets, table chains and cached table lists) plus a seed list of common tables, `{profile}` completes configured profiles, and the per-profile `{table}` uses the chosen profile's cache. The `servicenow_document_table` prompt's `table` argument and its new optional `profile` argument complete the same way. The templates also have `list` callbacks (at most 100 entries each): the schema templates list cached tables, and `servicenow://docs/…` lists the docs store from the `index.json` manifest — named by title, described as `<profile> · <kind> · <generated_at>` (or `hand-written`), generated documents first, with `servicenow://docs/index.md` always the last entry. `{path}` completes from the manifest by prefix and under the active profile's folder. Lists and completions only read local state, never the instance. The docs template is now `servicenow://docs/{+path}`, so nested documents such as `dev/tables/incident.md` can be read as resources (single-segment URIs work as before). New always-on resource `servicenow://reference/tools` renders the tool manifest as Markdown with this session's package policy; `servicenow://reference/encoded-query` gains `BETWEEN` / `SAMEAS` rows and a `javascript:` values section. Prompt arguments (now at most 200 characters) and docs resource content reach the model inside an `<untrusted-content>` block with a data-only note; a closing marker inside the text is defused, arguments used inline in the steps are flattened to one line without backticks or angle brackets, and every prompt tells the model that instance data is data, not instructions (SEC-21). `servicenow_document_table` attaches the encoded-query reference as an embedded resource. No new tools.
- **Snapshot / compare v2 (S-7).** `servicenow_snapshot_instance` gains seven record sections — `properties` (sys_properties; password-typed and secret-named values are written as `[redacted]`), `choices` (sys_choice), `acls` (sys_security_acl; the script is reduced to a 16-hex `script_hash`), `notifications` (sysevent_email_action), `flows` (sys_hub_flow), `catalog` (sc_cat_item) and `roles` (sys_user_role) — each as `<section>.md` (first 500 rows) plus `<section>.json`. New inputs: `sections` (a subset of the eleven sections; default all) and `resume`. Sections run as independent units at `min(SN_MAX_CONCURRENT, 4)` at a time; `<profile>/snapshot.json` records each finished unit's files and source hashes. A cancelled or failed run leaves that state and the profile's `index.md` marked `sn_partial`, so the root `index.json` carries `partial: true`; `resume: true` then skips every unit whose files still carry the recorded `sn_source_hash` and returns the skipped unit ids as `resumed`. The collectors are exported per section (`collectPlugins`, `collectApps`, `collectAutomation`, `collectRecordSection`) for reuse. `servicenow_compare_instances` now matches scripts by sys_id first and name second (a same-sys_id script under another name is reported with the new status `renamed` and `nameB`), attaches a unified diff (`diff`, `diff -u` format, 3 lines of context, at most 200 lines per script and 50 scripts per report) to every changed script and renders it as a `diff` block in the Markdown report, and takes an opt-in `sections` input that diffs record sections (live or, with `from_snapshot`, from the stored `<section>.json`) into `recordDiffs`, which count towards drift. Without `sections`, the drift count and the CLI exit codes are unchanged. `servicenow_describe_table` columns gain `defaultValue`, `readOnly`, `unique`, `display` and `choice`, and with `details: true` also `choices` (from the nearest table in the hierarchy) and `overrides` (sys_dictionary_override) plus a `warnings` list.
- **Structural where-used (S-9).** `servicenow_where_used` now also searches configuration structurally, not only script text, and returns the hits in a new additive `structural` section (`count`, `byKind`, `refs`, `sources`); `references`, `count` and `byType` are unchanged and still cover the text search only. Each structural hit carries its `kind`, `table`, `sys_id`, `name`, the matching `field` and the typed `target`. Sources: `sys_dictionary` reference fields and reference qualifiers (`reference_field`), `sys_ui_list` / `sys_ui_list_element` (`list_layout`), `sys_ui_section` / `sys_ui_element` (`form_layout`), catalog variables in `item_option_new` — reference, list and lookup tables, mapped fields, qualifiers and defaults (`catalog_variable`), flow and action inputs in `sys_hub_flow_input` / `sys_hub_action_input` (`flow_input`), and `sys_report` table, group-by and filter conditions (`report`). Every source read is policy-checked and degrades on its own: a missing, unreadable or denied table is reported `available: false` with a reason and listed in the "Not searched" caveat, a full page (100 rows) is reported `truncated` with its own caveat; the catalog, flow and report sources are `verified: false`. Rows are re-checked after the read, so a dropped query condition cannot produce a false hit. The `scope` filter applies to every structural read, the Mermaid diagram draws structural hits as dotted edges, and the new optional input `structural: false` skips the pass. The extractor is a reusable module (`src/api/references.ts`: `findStructuralReferences`, `extractReferences`, `REFERENCE_SOURCES`).
- **Observability (E-5).** `servicenow_get_status` gains an additive `observability` block: per-tool `count` / `errors` / `p50` / `p95` / `totalMs` (percentiles over a fixed 256-sample ring per tool, so memory is bounded), schema-cache counters, per-host retry counters, queue limits and occupancy, circuit-breaker state and the last `X-RateLimit-Limit` / `-Remaining` / `-Reset` headers per host (all runtime parts, reset on session dispose). The request loop publishes redacted metadata on `node:diagnostics_channel` — `servicenow-mcp:http.request.start`, `.end`, `.error` and `.retry` — with no headers, bodies or query strings, so OpenTelemetry can attach without a dependency. `SN_LOG_FORMAT=json|text` picks the stderr line format (default unchanged, JSON); `SN_LOG_FILE` adds a JSON Lines file sink (mode 0600) rotated by size (`SN_LOG_FILE_MAX_BYTES`, default 10 MiB, five generations kept). The logger now masks credential-named fields (`password`, `token`, `authorization`, `cookie`, …) and applies `SN_REDACT_FIELDS` / `SN_REDACT_PII` in every sink. With the HTTP transport, `SN_METRICS=1` serves Prometheus metrics at `GET /metrics` — only when `SN_HTTP_TOKEN` is set, behind the same bearer check. No new tools.
- **Explain artifact (P-6, `artifacts` package, opt-in).** `servicenow_explain_artifact({artifactType, sys_id | key})` reads one registry artefact (same policy, redaction and `verified:false` degrade rules as `servicenow_get_artifact`) and returns `{summary, when, fields, children, references, decoded}`: a one-line summary, the trigger fields that are set (a descriptor can name its own via the new optional `whenFields`; client scripts and catalog client scripts do), non-empty non-system fields, child records as compact `items`, `refFields` edges from the record and its children, and every descriptor `jsonFields` value run through its decoder. New decoder framework (`src/core/artifacts/decoders.ts`): a tolerant `json` decoder (BOM, whitespace, trailing commas, double-encoded strings) and `registerDecoder` for the P2 decoders; `flow-values` and `uib-composition` fall back to `json` (`via: "json"`) until they ship. An undecodable value comes back raw with `decoded: false` and a `reason` instead of failing the call. Output is capped against `SN_MAX_RESULT_CHARS` (per value one twentieth, overall four fifths; `truncatedFields`, `truncated` / `preview`, child `omitted`). Declares an outputSchema. 73 tools in 20 packages.
- **Generic artifact reads (P-5, `artifacts` package, opt-in).** `servicenow_list_artifacts({artifactType, scope?, query?, active?, limit?})` lists the records of any type in the artefact registry as summaries (sys_id, name, natural key, scope, active flag, SDK-managed verdict, metadata; no script bodies). `servicenow_get_artifact({artifactType, sys_id | key})` reads one record in full with its child records in registry order — a UI policy's `sys_ui_policy_action` rows, a portal page's containers → rows → columns → widget instances, a flow's instances — plus its scope and SDK-managed status. Every table read goes through the table policy (a denied child table is returned as `redacted: true`), descriptor `secretFields` are always masked and `SN_REDACT_FIELDS` / `SN_REDACT_PII` apply. Types not yet confirmed on a live instance (`verified:false`) carry a `caveat` and degrade to an empty result with a `degraded` reason when the instance rejects their table (400 / 403 / 404). Both tools declare an outputSchema. New resource `servicenow://artifact-types` lists the registry. 72 tools in 20 packages.
- **Log-notification rate limit (M-8).** Log lines mirrored to the client as `notifications/message` pass a token bucket per session: `SN_LOG_NOTIFY_RATE` per second (default 20, `0` turns the limit off), burst the larger of 50 and the rate. Lines over the cap are counted, not sent; one `warning` "N log messages suppressed" per minute reports them. Messages the client's level drops spend no token, and stderr is never throttled.
- **Script-intelligence widening (S-4).** The script tools (`servicenow_list_scripts`, `_get_script`, `_search_code`, `_where_used`, `_lint_script`, plus the snapshot and compare inventories and the code-health counts) now read fifteen more artefact types from the registry, appended after the nine original ones: `script_action` (`sysevent_script_action`), `transform_map`, `transform_entry`, `fix_script`, `email_script`, `processor`, `data_source`, `rest_message_fn`, `ui_script`, `ui_page`, `ui_macro`, `validation_script` (`sys_script_validator`), `sp_widget` (script, client_script, link, css), `catalog_client_script` (`catalog_script_client`) and `dictionary_script` (`sys_dictionary` calculations and `javascript:` defaults, narrowed by a registry base query). All fifteen are `verified:false` until they are checked on a live instance: an all-types search skips one whose table is missing or unreadable and lists it under `unreadable` (the original nine still fail loudly), and `servicenow_check_capabilities` keeps judging script intelligence on the original nine only. The original nine types read, search, lint, count and diff exactly as before. `servicenow_lint_script` takes client vs server rules per field from the registry and skips markup fields (HTML, Jelly XML, CSS, REST endpoint templates).
- **All hits per artefact, and a scope filter (S-4).** `servicenow_search_code` matches gain `hits` — every matching line across the artefact's script fields, with the non-blank line before and after, each cut to 200 characters, capped at 20 per artefact — and `hitCount`, the full total; `field` / `line` / `snippet` still describe the first hit. `servicenow_where_used` references gain `hits` (up to 5 lines, no context) and `hitCount`. Both tools take an optional `scope` (an application namespace such as `x_acme_app` or `global`, or a `sys_scope` sys_id); a scoped search always uses the LIKE path (the Code Search API cannot filter by scope), `where_used` scopes the attached automation of a table too and says so in its caveats, and lists unreadable artefact types as a caveat.
- **SDK-managed scope detection (P-3).** `detectSdkManaged(scope)` answers `yes` / `no` / `unknown` with evidence, in authority order: `SN_SDK_MANAGED_SCOPES` (scope names or sys_ids declared by the operator), then `now.config.json` files found under `SN_SDK_PROJECT_DIRS` whose `scope` / `scopeId` match, then instance heuristics — which can only raise `unknown` to `yes`, are always marked unverified and ship empty until O-5 confirms a marker. `no` is returned only when a deterministic source is configured, readable and does not match. The project scan is bounded (depth 4, 2000 directories, 100 configs, 256 KiB per file), never follows symlinks, skips hidden and build directories and reads nothing but `now.config.json`; its result is cached for 60 s as a runtime part. `get_status` and `servicenow_check_capabilities` gain an additive `sdkManaged` block (authority, declared scopes, project dirs, found scopes, truncation and warnings). The authority order is a default pending O-6.
- **SDK release tracking (P-4).** `npm run sdk:drift` (`scripts/sdk-drift.mjs`) compares `SDK_BASELINE` with the npm `@servicenow/sdk` dist-tags (`latest`, `next`) and diffs the SDK docs index (`llms.txt`, API Reference) against the registry's `sdkApi` values — new doc pages, registry APIs without a page, and known next-only APIs with their descriptor state. A weekly `sdk-drift` workflow (SHA-pinned actions, `issues: write`) opens or updates one "SDK drift tracking" issue. Not part of `npm run check` and not shipped. The registry gains `SDK_NEXT_APIS` and a `database_view` descriptor (`DatabaseView`, SDK 4.13.0, `verified:false`, no G tier); the registry validator rejects a next-only API on a verified or G-tier type.
- **Cancellation, progress and per-call log context (M-3).** A tool call now honours the client's cancellation (`notifications/cancelled`): the in-flight request, a wait in the per-host queue or a retry backoff is aborted at once, nothing is retried after it, and the call fails with code `CANCELLED` and a hint (writes that completed before the cancellation stay applied — see the write journal). When the client sends a `progressToken`, `servicenow_query_table` with `fetchAll` (records fetched against `X-Total-Count`), `servicenow_snapshot_instance` (one step per section and schema table), `servicenow_compare_instances` (nine steps) and `servicenow_batch` (sent / serviced) emit `notifications/progress` — at most one per 250 ms, always increasing, the final update always sent; without a token nothing is sent. Every log line written during a tool call carries `profile`, `requestId` (the JSON-RPC id, or `local-<uuid>`), `sessionId` (HTTP transport) and `tool`.
- **Docs store v2 (S-14).** Files written by a generator (`servicenow_snapshot_instance`, `servicenow_compare_instances`, `servicenow_code_health`) carry frontmatter — `sn_generated`, `sn_generator`, `sn_generator_version`, `sn_kind`, `sn_profile`, `sn_instance`, `sn_generated_at`, `sn_source_hash` (`sha256:` of the source data, timestamp excluded); JSON companions carry the same fields at the top level. Text between `<!-- sn:manual:start -->` and `<!-- sn:manual:end -->` survives regeneration byte for byte. `servicenow_docs_write` refuses to replace a generated file without `overwrite: true`, and a generator refuses a hand-written file (both `DOC_GENERATED`, 409); files the 2.x snapshot wrote are recognised and upgraded. A re-snapshot of an unchanged instance leaves the files alone and reports them `unchanged` in the new `changes` map. Every write rebuilds a root `index.json` manifest (kind, title, profile, generator, generated_at, source_hash, bytes, headings) and the `index.md` rendered from it, grouped by profile and kind with hand-written documents last; the snapshot's `<profile>/index.md` becomes a README with a notes block instead of a second file list.
- **Docs tool options.** `servicenow_docs_list` returns `entries` with per-file metadata (generated, generator, generated_at, profile, kind, bytes, `stale` past `SN_DOCS_STALE_DAYS`, default 30); `servicenow_docs_list` / `_read` / `_search` / `_write` take an optional `profile` (`current` or a profile name) that scopes paths to `<profile>/`; `servicenow_docs_search` filters by `kind` and `generated`, reports the nearest `heading` per match and stops at `SN_DOCS_SEARCH_MAX` (200) with `truncated: true`. The `servicenow_document_table` prompt now writes `<profile>/tables/<table>.md` (reading the legacy root path first) and forbids pasting record data.
- **Diagram depth (S-14).** Shared Mermaid primitives (`src/api/mermaid.ts`: one escaping rule set, a node cap `SN_DIAGRAM_MAX_NODES`, default 200, folding the rest into a `+N more` node) back the ER, table-flow, trace and where-used diagrams; default outputs are byte-identical and pinned by goldens. `servicenow_generate_er_diagram` gains `columns` (`all` / `own` / `keys`), `max_columns` and `depth` (0–2): any of them switches on PK / FK and required markers, `inherited from` comments, `extends` edges and referenced tables. `servicenow_generate_table_flow` gains `operation`, which draws the lanes from the event trace — business rules for that operation plus the flows, workflows and notifications it triggers.
- **VS Code extension published.** The extension is now live on the VS Code Marketplace as [`ivanbbaev.servicenow-mcp-ai`](https://marketplace.visualstudio.com/items?itemName=ivanbbaev.servicenow-mcp-ai) — install with `code --install-extension ivanbbaev.servicenow-mcp-ai`, or find "ServiceNow MCP" in the Extensions view. A new `publish-vscode` CI workflow ships it on each version tag (with a version-sync gate: the extension version must match the tag).
- **Journal-based revert (`revert` package, opt-in).** `servicenow_list_writes` lists the local write journal newest first (filters `profile`, `table`, `since`, `result`, `action`; each row says whether it is revertible and why not). `servicenow_revert_write` undoes one applied write by its entry id — an update writes its `before` values back, a create is deleted, a delete is re-created from its `before` record — under plan/apply. It refuses with `STALE_RECORD` when the record changed since the journaled write (`sys_mod_count`, or the written field values when no count is available) unless `force:true`, and with `NOT_REVERTIBLE` plus the reason when the line cannot be inverted (no or redacted `before`, already reverted, broken journal chain, or an origin such as email, attachments, import sets, catalog orders, batch sub-requests or CMDB creates). The revert is journaled with `reverts: <entry id>`, so reverting it redoes the write. Journal lines now also record the writing `tool` and, for Table API creates/updates, the record's `after_mod_count`. 69 tools in 19 packages.
- **Env-docs sync gate.** A documentation test now fails the build when an `SN_*` variable read in `src/` is missing from the README env reference or `.env.example`; `.env.example` gained the previously missing `SN_OAUTH_JWT_KID`, `SN_OAUTH_JWT_EXP_SEC` and `SN_CODESEARCH` entries.
- **`SN_ALLOW_UNCONFIRMED_CREDENTIAL_CHANGE`** — operator opt-out that lets `servicenow_set_credentials` proceed on MCP clients without elicitation support (see Security). Off by default; an explicit decline is still refused.
- **`SN_SCHEMA_CACHE_MAX`** — cap on the near-static schema reads cache (default 256, least-recently-used eviction); `get_status` now reports the cache size and its hit / miss / eviction counters under `schemaCache`.
- **Outbound proxy, and TLS settings that work without a client certificate.** `SN_HTTPS_PROXY` (falling back to the ambient `HTTPS_PROXY` / `HTTP_PROXY`, honouring `NO_PROXY`) routes every ServiceNow and OAuth request through one `undici` dispatcher; `SN_TLS_CA` / `SN_TLS_CA_FILE` and `SN_TLS_REJECT_UNAUTHORIZED` now apply on their own — earlier builds silently ignored them unless a client certificate was configured. Dispatchers are cached per TLS material and profile and closed on `servicenow_set_credentials` / `servicenow_use_profile`. `undici` remains optional; a clear install hint is raised when a proxy or custom TLS needs it.
- **Identifying `User-Agent`.** Every request, token requests included, carries `servicenow-mcp-ai/<version> (node/<major>; <transport>; <client>)` — the client name comes from the MCP initialize handshake — so the instance transaction log can attribute the traffic; `SN_USER_AGENT_SUFFIX` appends a team or environment token.
- **Request budget and back-pressure knobs.** `SN_DEADLINE_MS` (total wall-clock for one logical request across retries, backoff, queue wait and re-authentication; default `max(120 s, 2 × SN_TIMEOUT_MS)`), `SN_RETRY_AFTER_MAX_MS` (the `Retry-After` cap, previously fixed at 60 s), `SN_MAX_QUEUE` (64) and `SN_QUEUE_TIMEOUT_MS` (defaults to `SN_TIMEOUT_MS`) bound the per-host queue, and an opt-in per-host circuit breaker (`SN_BREAKER_THRESHOLD`, off by default; `SN_BREAKER_RESET_MS`).
- **Error codes and hints.** Tool errors now carry a machine-readable `code` where the failure is structural — `DEADLINE_EXCEEDED`, `BUSY`, `UPSTREAM_HTML`, `CIRCUIT_OPEN` — plus a one-line `hint`; `servicenow_get_status` reports the HTTP layer under `http` (user agent, proxy source and host — never its credentials —, TLS mode, queue depth per host).
- **Write journal v2.** Every journal line now carries `schema_version: 2`, a ULID `id`, the outcome (`result`: `applied`, `failed` or `refused` — failed and refused writes are journalled too, with the error), the record state before an update or delete (`before`, read best-effort just before the write), the HTTP session (`client`) and `prev`, the sha256 of the previous line, with the chain head in `write-journal.head` so an edited or truncated journal is detectable. The JSONL file rotates at `SN_JOURNAL_MAX_BYTES` (default 20 MiB) with the chain continued across files, and `write-journal.md` is re-rendered from the last 200 entries (new `Result` column). v1 lines stay valid and read as `schema_version: 1`, `result: "applied"`. A batch now journals one line per write sub-request (action, table, sys_id, body hash, status-code outcome) linked to its envelope by `batch_id`; files written under the docs directory (`local_write`, with size and hash) and env-file changes by `servicenow_set_credentials` / `servicenow_use_instance` (`config`, key names only — never values) are journalled as well.
- **`SN_CSV_FORMULA_GUARD` and `SN_CSV_BOM`** (both on by default). `format:"csv"` exports neutralise cells a spreadsheet would evaluate (`=`, `+`, `-`, `@`, tab, CR) with a leading `'` — note that a text value `-5` exports as `'-5` — and start with a UTF-8 BOM; the result reports `_meta.csv = { escaped, bom }`.
- **Outbound size and upload limits.** `SN_MAX_BODY_BYTES` (default 50 MiB) caps every response body — a larger declared `Content-Length` is refused before reading and a streamed body is cancelled at the cap (`RESPONSE_TOO_LARGE`); `SN_MAX_UPLOAD_BYTES` (10 MiB, checked before the base64 is decoded) and `SN_UPLOAD_MIME_ALLOW` bound attachment uploads (`PAYLOAD_TOO_LARGE`, `MIME_NOT_ALLOWED`); `SN_DOCS_MAX_FILE_BYTES` (5 MiB) caps local document writes, truncates oversized reads and skips them in search.
- **`SN_EMAIL_ALLOWED_DOMAINS`** — domains (and their subdomains) `servicenow_send_email` may address; `*` allows any recipient.
- **Attachment edge cases.** Uploads accept `data:` URLs and downloads report `sizeBytes`; UTF-8 file names, 0-byte files and the base64 size-limit boundary are pinned by tests.
- **Plugin state on 404.** When a plugin-backed namespace returns 404, the error now says whether the backing plugin is installed but inactive, missing, or active.
- **Caveats in comparison reports.** `compare_instances` and `where_used` results carry `caveats` (domain separation, visibility, cross-scope), and the drift report has a Caveats section.
- **Security scan extension (S-3).** The `servicenow_code_health` security dimension now pages through every active ACL (fetchAll, bounded by `SN_MAX_RECORDS` and a hard ceiling of 50 000 rows per read) and reports `truncated`, `truncatedReason` (`cap`, `scan_limit` or `ceiling`) and `filtered` instead of stopping silently at 500. ACL roles are joined from `sys_security_acl_role` and resolved through `sys_user_role_contains`, with the `*` wildcard and `public` treated as everyone. New finding rules: `acl-open` (no role, condition or script), `acl-public-role`, `acl-elevated-privilege` (with the roles that inherit the elevated role in `grantedBy`), `admin-overlap-role` (a role that contains `admin` or an elevated role), `public-rest-resource` (`sys_ws_operation` with `requires_authentication=false`), `public-ui-page` / `public-page` (`sys_public`, classified against `sys_ui_page`) and `table-no-acl` (custom `u_` / `x_` tables with no record ACL on themselves or a parent). Findings gain optional `kind`, `table`, `roles` and `grantedBy`; the scan gains `checks`, one entry per check with `available`, `unavailableReason`, `scanned`, `findings`, `truncated` and `note`. Each check degrades to `available:false` on its own when its table is unreadable (401/403/404, an `SN_TABLES_ALLOW` / `SN_TABLES_DENY` denial or any other error), so one unreadable table never fails the scan; a 404 on `sys_security_acl` now degrades like a 403. The report adds a per-check table, and its findings table header reads `Item` instead of `ACL`.
- **Upsert by key (S-8).** `servicenow_upsert_record({table, key, fields})` creates or updates one record matched by an exact `key` of field/value pairs: no match creates it (key + fields), one match updates it, more than one match — or a match the user cannot read — is refused with the new error code `AMBIGUOUS_KEY`. The action is decided in the plan, which returns `apply_with: {expected_action, expected_sys_id}`; passed back with `apply:true`, a changed decision fails with `STALE_RECORD` and writes nothing. The write is journaled as a create or an update (`tool: servicenow_upsert_record`) and `servicenow_revert_write` undoes it. 70 tools in 19 packages.
- **More Table API parameters (S-8).** `servicenow_query_table` takes `view`, `queryCategory`, `noCount`, `queryNoDomain` and `suppressPaginationHeader` (`sysparm_view`, `sysparm_query_category`, `sysparm_no_count`, `sysparm_query_no_domain`, `sysparm_suppress_pagination_header`); `servicenow_create_record`, `servicenow_update_record` and `servicenow_upsert_record` take `inputDisplayValue` (`sysparm_input_display_value`).
- **Encoded-query reference (S-8).** New resource `servicenow://reference/encoded-query` (table package) documents the encoded-query syntax and its limits — no escaping of `^`, URL length (HTTP 414), unknown fields silently ignored, ACL-hidden rows — and how `fetchAll` pages; the `servicenow_query_table` description summarises the limits.
- **Capability matrix (S-13).** `servicenow_check_capabilities` (and the `servicenow://capabilities` resource) now also returns a `matrix` with one read-only, policy-routed probe per group — `writes` (derived from `SN_READONLY` / `SN_WRITE_MODE` / `SN_PACKAGES_READONLY`, no request), `update_sets` (the user's current update set), `attachments`, `aggregate`, `import_sets`, `email`, `atf`, `version` (`glide.buildtag` and the release family) and `roles` (admin flag and notable roles). Each group reads `available`, `unavailable` (401/403/404), `plan-only`, `read-only` or `unknown` (a table or package denied by policy is not probed, a 5xx, a transport error, ACL-hidden rows). New optional inputs `groups` (narrow the matrix) and `refresh` (drop cached results). Results are cached per instance and user — successes for `SN_CAPABILITY_TTL_MS` (default 10 min), failures for `SN_PLUGIN_NEGATIVE_TTL_MS` (default 60 s); transport errors are never cached, and switching credentials or profile clears the cache. `servicenow-mcp-ai doctor` prints the matrix under Capabilities; it does not change the verdict or exit code.
- **Credentials model completeness (D-2).** `hasCredentials`, `doctor`, `servicenow_get_status` and `servicenow_list_instances` evaluate each profile against its own auth method (Basic, API key, bearer token, OAuth per grant, cert-only `none`) — an API-key or bearer profile no longer reports "not configured" for a missing password — and name what is `missing`. `servicenow_set_credentials` takes `auth`, `oauth_client_id`, `oauth_grant` and `request_secrets`: the API key and OAuth client secret are asked for through an elicitation prompt only, never accepted as tool arguments (refused on clients without elicitation), and never echoed, logged or journaled (the journal keeps key names). The H-2 instance-change rule now requires the material of the resulting auth method. A rotated OAuth refresh token is persisted to the env key it came from, kept in memory with a warning when the env file is read-only (L6-01). New `SN_TOKEN_FILE` (bearer token from a file, re-read once on a 401; otherwise the call fails with the new error code `AUTH_EXPIRED` and a hint) and `SN_TOKEN_EXPIRES_AT` (warning when under 24 h remain) (L6-02). `servicenow_get_status` gains `authWarnings`; each `servicenow_list_instances` profile reports `auth`, `grant`, `refreshToken` (`configured` / `rotated-in-memory` / `none`), `writeMode` and `missing`, never a secret (L8-01). The env writer single-quotes values with backslashes (Windows paths stay literal), keeps a CRLF file CRLF, refuses values with line breaks, and warns on Windows to restrict the env file's ACL with `icacls` (never run by the server) (L2-11).

### Changed

- **License: MIT → GPL-3.0-or-later.** From 3.0.0 the server and the VS Code extension are licensed under the GNU General Public License v3.0 or later (`LICENSE`, the `license` field of every manifest, README and the docs site). Versions up to and including 3.0.0-beta.1 stay under the MIT License they were released with.
- **Upgrade review names the web-service access setting (N-1, OD-6).** A 403 on `sys_store_app` or `sys_upgrade_history_log` now says in `unavailableReason` that the table's "Allow access via web services" (`sys_db_object.ws_access`) is probably off, which an admin can turn on. The read still degrades to `available:false`.
- **The instance comparison is filed under its first profile (ID-20).** `servicenow_compare_instances` now writes `<a>/compare/<a>-vs-<b>.md` (it was the store-root `_compare/<a>-vs-<b>.md`, which a profile-scoped `docs_list` did not show) plus a `.json` companion with the structured diff and caveats. Both carry `sn_compare_with: <b>` next to the standard `sn_*` fields, and the result gains `reportJson`. A generated report left at the old path is moved to the new one on the next run, keeping its manual blocks (journalled with `moved_from`); a hand-written file there is left alone.

- **P-20: the published flow snapshot path is checked on a PDI (O-5).** Child rows sit under `master_snapshot` for 562 of 563 flows, and every published flow has the same steps under its draft and its snapshot; only drafts with unpublished edits differ. The `servicenow_compare_instances` / snapshot warning no longer calls the path unverified: it says a draft with unpublished edits differs from the snapshot and that the snapshot encodes step `values` differently, so a flow read from its draft on the other side shows as changed. tools/list delta 0 B.
- **P-2: the `application` artifact type is verified.** Its child `sys_scope_dependency` (no such table, OD-3) is removed; the dependency rows come from the `sys_package_dependency_m2m` child, as for `app_dependency` (B18). PDI ven03019 confirms the type with 0 misses. `risk_assessment` (plugin inactive) and `uib_composite_definition` (no table) stay `verified:false` under OD-3 (1). 0 B tools/list.
- **OD-4: 25 UI Builder, Next Experience and workspace artifact types are verified (N-25, N-30).** Every `uib_*` row except `uib_form_action` and `uib_composite_definition`, the declarative action rows (`ux_declarative_action`, `_definition`, `_payload`), `aw_master_config`, `aw_list`, `workspace`, `ux_list_menu_config` and `ux_applicability` now carry `verified: true`: confirmed on a PDI with GETs and zero misses. They lose the unverified caveat and a failed read fails loudly (a role-limited user now gets `INSTANCE_HTTP_403` instead of an empty result), except a 400 / 404 on a table the `sys_db_object` probe reports absent (no UI Builder or Agent Workspace plugin), which degrades to `available:false`. `servicenow_explain_ui_experience` reports the registry flag of `workspace` instead of a fixed `verified:false`, and the domain analysers no longer call UI Builder rule fields unverified. The two `sys_ux_form_action` rows wait for the N-34b merge; `uib_composite_definition` stays unverified (no such table). tools/list delta 0 B.
- **OD-4: the 13 Service Portal artifact types are verified (P-2).** `sp_portal`, `sp_page`, `sp_ng_template`, `sp_dependency`, `sp_angular_provider`, `sp_js_include`, `sp_css_include`, `sp_theme`, `sp_menu`, `sp_header_footer`, `sp_page_route_map`, `sp_css` and `sp_search_source` now carry `verified: true`: confirmed on a PDI with GETs and zero misses. They lose the unverified caveat and a failed read fails loudly, except a 400 / 404 on a table the `sys_db_object` probe reports absent, which degrades to `available:false`. `servicenow_explain_portal` returns the registry flag of `sp_portal` / `sp_page` instead of a hard-coded `verified:false` (its markdown summary line prints the same value; an unreadable table still becomes a caveat), and the domain-analyser caveat of `servicenow_check_code_health` `extended` keeps `verified:false` for the UI Builder rule fields only. UI Builder and Next Experience rows, `application`, `app_dependency` and `risk_assessment` stay `verified: false`. `tools/list` unchanged (0 B).
- **OD-4: 48 plugin-backed artifact types are verified.** `state_model`, `user_criteria`, `knowledge_base`, `security_attribute`, `security_data_filter`, `js_module`, `graphql_api`, `graphql_resolver`, `graphql_type_resolver`, `alias`, `alias_template`, `retry_policy`, `data_lookup`, `sla`, `dashboard`, `database_view`, the Flow Designer and playbook rows (`flow`, `subflow`, `flow_action`, `flow_trigger_definition`, `flow_context`, `playbook`, `playbook_context`, `decision_table`), the legacy workflow rows (`workflow`, `workflow_activity_definition`, `workflow_context`), the catalog rows (`catalog_item`, `record_producer`, `variable_set`, `catalog_variable`, `catalog_ui_policy`), `atf_test`, `atf_test_suite`, `assessment`, the four scan checks (`linter_check`, `script_only_check`, `column_type_check`, `table_check`), `ai_agent`, `ai_agentic_workflow`, `now_assist_skill`, `source_control` and the five `pa_*` rows now carry `verified: true`: confirmed on a PDI with GETs and zero misses. They lose the unverified caveat and a failed read fails loudly, with one exception that now applies to every verified type: a 400 / 404 on a table the `sys_db_object` probe reports absent (the plugin is not installed) degrades to an empty result with `available:false` (and `requires` for a licensed family) instead of an error. A 403, or an error on a present table, still fails. The discovery artefacts document says "the instance has no such table (plugin not installed)" for such a type. Service Portal, UI Builder and Next Experience rows, `application`, `app_dependency` and `risk_assessment` stay `verified: false`. `tools/list` unchanged (0 B).
- **OD-4: 28 core-platform artifact types are verified.** `table`, `choice_set`, `property`, `user_preference`, `role`, `cross_scope_privilege`, `assignment_rule`, `field_style`, `schedule`, `event`, `relationship`, `ldap_server`, `rest_api`, `rest_message`, `email_notification`, `inbound_email_action`, `data_policy`, `form`, `ui_section`, `formatter`, `list`, `application_menu`, `ui_view`, `list_control`, `interceptor`, `customer_update`, `report` and `report_source` now carry `verified: true`: core tables that ship on every instance, confirmed on a PDI with GETs and zero misses. They lose the unverified caveat, and a failed read (400 / 401 / 403 / 404) is now an error instead of a degraded result. Plugin-backed types (portal, Flow Designer, workflow, catalog, ATF, UI Builder, GraphQL, PA, AI) and `database_view` (its SDK API reached `latest` only in 4.13) stay `verified: false`. `tools/list` unchanged (0 B).
- **OD-4: the 15 S-4 script types are verified.** `script_action`, `transform_map`, `transform_entry`, `fix_script`, `email_script`, `processor`, `data_source`, `rest_message_fn`, `ui_script`, `ui_page`, `ui_macro`, `validation_script`, `sp_widget`, `catalog_client_script` and `dictionary_script` now carry `verified: true` (their tables, fields and children confirmed on a PDI with GETs). They lose the unverified caveat, and a failed read (400 / 401 / 403 / 404) is now an error instead of a degraded result; the documents list a 401 / 403 as unreadable. A new registry mark, `capabilityRequired`, keeps `script_intelligence` in `check_capabilities` on the nine legacy script tables, and the all-types `search_code` / where-used sweep still skips an unreadable widened table, so neither changes. `tools/list` unchanged (0 B).
- **OD-3: `application` reads its package dependencies from `sys_package_dependency_m2m`.** The PDI has no `sys_package_dependency` table; the m2m table has the same `sys_package` / `dependency` columns (checked with GETs). `risk_assessment`, `app_dependency` and `uib_composite_definition` stay `verified:false`. `tools/list` unchanged (0 B).
- **OD-2: a scope filter on a type without a scope column is refused.** `user_preference`, `knowledge_base`, `flow_context`, `playbook_context`, `workflow_context` and `source_control` have no `sys_scope` column (checked on a PDI); their registry entry now has `scopeField: null`, so `servicenow_list_artifacts` with `scope` returns INVALID_INPUT instead of filtering on a column the table does not have. `document_app` and the discovery artefacts document skip them (discovery says "no application scope column"). `application` filters on its own `sys_id` or on its `scope` namespace (new registry key `scopeNamespaceField`), and reports itself as its scope. `tools/list` unchanged (0 B).
- **OD-5: the Instance Scan section names the newest run with findings.** When the latest `scan_result` has no findings, the `check_code_health` Instance Scan section (and its `instanceScan` key) still shows that latest run and adds a caveat naming the newest run that has findings (`newestWithFindings`: `sys_id`, `createdOn`), read through its newest `scan_finding`. Best effort: a failed read drops the caveat. Checked on a PDI. `tools/list` unchanged (0 B).
- **H-4: the Code Search LIKE path skips a denied script type.** An all-types `servicenow_search_code` used to refuse the whole search when one default script table was denied by `SN_TABLES_DENY` / `SN_TABLES_ALLOW`. It now skips that type, lists it under `unreadable` and never queries the table, as the Code Search API path already dropped such hits. An explicit `type` on a denied table is still refused. `servicenow_delete_attachment` apply reads the attachment metadata once instead of twice. `tools/list` +0 B.
- **N-61: a 48,000-character result budget, oversize reads to a file by default (O-21 (b)).** `SN_MAX_RESULT_CHARS` now defaults to 48,000 (was 100,000), under Claude Code's 50,000-character threshold, and `SN_OVERSIZE_TO_FILE` now defaults on: a `query_table` JSON result over the budget is written as JSON lines to `<SN_DOCS_DIR>/<profile>/exports/` and the tool returns `{format:"file", rows, path, file, bytes, preview, note}` plus the usual completeness fields, as snapshot, compare and diagram results already did. `SN_OVERSIZE_TO_FILE=false` restores inline truncation; `SN_MAX_RESULT_CHARS=100000` restores the old budget. An unfiltered `servicenow_list_tables` now returns the first 200 tables by name with `truncated: true`, `total` and a hint to pass `filter` (a fix: the full list could run to megabytes). `tools/list` +0 B.

- **P-4: `@servicenow/sdk` re-pinned to 4.13.6 (O-7).** `SDK_BASELINE`, the `fluent:verify` oracle and the generated Fluent action catalogue move from 4.12.2 to 4.13.6 (npm `latest`); generated Fluent now names 4.13.6 in its header and caveat. `DatabaseView` reached `latest`, so it leaves `SDK_NEXT_APIS` (now empty) for `SDK_APIS`; the `database_view` type stays `verified:false` until O-5. `npm run sdk:drift` is left with one finding: `JsInclude` / `CssInclude` have no docs page but are still exported.
- **N-60: shallow output schemas in `tools/list` (O-10 (c)).** A published `outputSchema` now carries only the top-level property names, their JSON types, `required` and the open `additionalProperties: {}`; nested shapes, enums and item types are dropped. The full shape stays in the tools manifest, the tool docs and `servicenow://reference/tools/{name}`, and the server still validates `structuredContent` against it. `all` −9,461 B (126,041 → 116,580), `core` −2,724 B (29,307 → 26,583); the budgets ratchet down to 26,624 / 116,736 B.
- **N-58: lean `tools/list` (O-10 (b), ADR 0006).** `tools/list` now goes out in a lean form: no `$schema`, no default `execution` (`taskSupport: "forbidden"`), no `maxLength` a bounded pattern already implies, no ±(2^53−1) integer bounds, no `propertyNames: {type: "string"}`, and only the annotation hints that differ from the MCP defaults (a read tool keeps `destructiveHint: false`). Names, descriptions, types, enums, required lists and every effective hint are unchanged. `core` 32,737 → 28,546 B (−4,191), `all` 135,957 → 119,447 B (−16,510); the budgets in `test/fixtures/token-budgets.json` drop to 28,672 / 119,552 B through the ratchet. The tools manifest and `servicenow://reference/tools/{name}` publish the same lean schemas.
- **BREAKING (B1): Node.js 22.12 or newer is required (E-1).** `engines.node` is now `>=22.12`; the `bin` launcher and the `src/index.ts` guard refuse older runtimes with a message naming the floor. CI runs Linux on Node 22 / 24 / 26, macOS and Windows on 22, and a Node 22.12.0 floor job (build + tests). c8 12 runs coverage on every supported Node, so `scripts/coverage-guard.mjs` (the Node >= 25 workaround) is gone. Sources, scripts and tests use `import.meta.dirname` instead of `fileURLToPath(new URL(...))`. `@types/node` stays on the 22 line to match the floor.
  - **Migration:** upgrade Node.js to 22.12 or newer (`nvm install 22`, or the 22.x / 24.x LTS installer). Older runtimes get the launcher's message instead of a crash; nothing else changes for MCP clients.
- **Toolchain majors (E-2).** zod 4 (`^4.6.5`), `@modelcontextprotocol/sdk` `^1.31.0`, ESLint 10 with `@eslint/js` 10 and `globals` 17, typescript-eslint 8.71, fast-check 4.10. TypeScript stays on 5.9: typescript-eslint 8.71 supports TypeScript < 6.1, so TypeScript 7 is out of reach for now. Prettier stays on 3.8: 3.9 restyles short unions and Markdown tables, which would reformat about 23 untouched files. `tsconfig.json` now sets `verbatimModuleSyntax` and `isolatedModules`.
  - **`dotenv` is removed.** The env file is read by Node's own `process.loadEnvFile`, which, like dotenv with `override: false`, never replaces a variable that is already set, so client-supplied environment still wins. The runtime dependency set is now `@modelcontextprotocol/sdk` and `zod`. Node's parser also fixes the two env-writer round-trip hazards found by the E-6 properties: a quoted value that ends in a backslash no longer swallows the next line (F1), and U+2028 / U+2029 inside a value survive (F2). Both are now regular tests.
  - **Tool schema drift (zod 4 JSON Schema).** The tool names, descriptions, annotations and the tool count (97) are unchanged. The generated JSON Schemas differ as follows.
    - Representation only (same meaning): an open object is written `additionalProperties: {}` instead of `true`; every record carries `propertyNames: {"type": "string"}`; the `key` of `servicenow_upsert_record` and `servicenow_upsert_artifact` is inlined instead of a `$ref`.
    - Nested input objects no longer say `additionalProperties: false`. This affects the `servicenow_batch` `requests[]` and `requests[].headers[]`, the `servicenow_identify_reconcile` `items[]` and `relations[]`, and the `servicenow_upsert_artifact` `children[]` entries. Unknown keys are still stripped at runtime, as before.
    - Integer inputs now declare `maximum: 9007199254740991`, because zod 4's `.int()` is a safe integer. This affects `expected_mod_count` on `servicenow_delete_record` and `servicenow_update_record`; `max_columns` on `servicenow_generate_er_diagram`; and `offset` on `servicenow_list_catalog_items`, `servicenow_list_changes`, `servicenow_list_cis`, `servicenow_list_scripts`, `servicenow_query_table` and `servicenow_search_knowledge`. It also affects `limit` on `servicenow_list_scripts` and `servicenow_search_code`, and `quantity` on `servicenow_order_catalog_item`.
    - The `tools/list` size budgets in `test/output-schema.test.js` rise to 151,000 bytes (all packages; measured 150,175) and 36,500 bytes (core; measured 36,224).
- **Test architecture: metadata-only guard everywhere, code-health golden, extension lint (E-6, ID-27, ID-28).** `withMetadataFetch` (`test/helpers.js`) now records a rejected URL and re-throws once the run settles, so a generator that swallows the fetch error into an `unreadable` section still fails; the allow-list gains the where-used layout and report tables, and a test enumerates every artefact-registry table plus the security-scan and document tables against it. The snapshot, compare, codecheck, security-scan, collectors, writer-goldens and P-18 / P-19 / P-20 suites run under the guard, each of the first four with a negative test. A code-health writer golden joins the snapshot and compare goldens (`test/fixtures/docs/writers/code-health/`). The VS Code extension has its own type-aware ESLint config (`extension/eslint.config.mjs`, toolchain from the root `node_modules`, no new dependencies) and a `lint` script, run by the CI `extension` job.
- **`servicenow_change_conflicts` with `calculate: true` is a plan/apply write (H-4).** In plan mode it now returns a preview (with the current conflicts it would replace) instead of recalculating; pass `apply: true` (or run with `SN_WRITE_MODE=apply`) to recalculate. The recalculation is journaled (`execute` on `conflict`) and is an H-3 destructive apply (`plan_token` under `SN_DESTRUCTIVE_CONFIRM`). Reads (`calculate` omitted or false) are unchanged.

- **CLI argument handling (D-1).** The entry point (`src/index.ts`) now only checks the Node version and hands `process.argv` to `src/cli.ts`; the server bootstrap moved to `src/server.ts` (`startServer`) and nothing runs at import time. An unknown command or option now prints the usage and exits 2 instead of starting the server, and `login` / `drift` are recognised only as the first argument. `doctor` prints the env file it used as its first line and falls back to ASCII glyphs when stdout is not a terminal.
- **Input bounds (M-8).** Tool inputs are built from typed zod builders in `src/mcp/define.ts` (`sysId`, `tableName`, `fieldName`, `encodedQuery`, `fieldList`, `shortText`, `longText`, `tableList`, `sysIdList`, `email`, `recipients`) with generous limits: sys*ids up to 32 characters of `[A-Za-z0-9*-]`, table names up to 80 of `[A-Za-z0-9_]`, encoded queries up to 8000 characters, field lists up to 200 entries, short text up to 255, `servicenow*batch`up to 1000 sub-requests. Record field values,`content_base64`, `servicenow_docs_write.content`and batch bodies stay unbounded — their run-time`SN*\*`caps govern them — and`servicenow_where_used.name` is left for S-9. A test walks every tool schema and fails on a new unbounded string or array.
- **Tool annotations (M-8).** Every tool declares all four hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`); the tool definition type now requires them. `servicenow_docs_write` is marked `destructiveHint: true` (it can overwrite a file) and `servicenow_docs_list` / `_read` / `_search` declare `openWorldHint: false`.
- **Strict input schema without a cast (M-8).** `buildInputSchema(spec)` builds the strict object schema (with the automatic `instance` parameter) that `registry.ts` registers, replacing the `.strict()` cast.
- **Plugin-availability cache (S-13).** A plugin API found missing (namespace 404) is now remembered for `SN_PLUGIN_NEGATIVE_TTL_MS` (default 60 s, was a fixed 5 min), and a plugin API seen available expires back to `unknown` after `SN_CAPABILITY_TTL_MS` (default 10 min). `servicenow_check_capabilities` with `refresh: true` clears both.
- **Policy-routed table preflight (S-13).** The `sys_*` table preflight in `servicenow_check_capabilities` no longer sends a request for a table denied by `SN_TABLES_ALLOW` / `SN_TABLES_DENY`; the probe reports `policyDenied: true` with the reason.
- **HTTP retry policy tightened.** `502`/`503`/`504` responses are now retried only for idempotent requests (a write may already have executed on the instance), and a server-supplied `Retry-After` is honoured but capped (`SN_RETRY_AFTER_MAX_MS`, default 60 s) so a misbehaving proxy cannot stall a call indefinitely.
- **Process lifecycle.** An unhandled promise rejection now ends the process the way an uncaught exception does: one structured, secret-free error line (pid, uptime, transport, error name and message — never a stack) is written, stderr is flushed and the server exits 1, instead of logging and running on with possibly corrupt state. SIGTERM behaves like SIGINT (graceful close, exit 0). On shutdown and whenever an HTTP session closes, the in-process singletons (schema cache, cached OAuth tokens, the per-host request queue and circuit breakers, the cached proxy / TLS dispatchers, HTTP telemetry, plugin availability) are reset through one `dispose()`, and the HTTP transport fails startup when its port cannot be bound instead of raising a stray error event.
- **Runtime container (internal).** The process state that used to live in module singletons — schema cache, cached OAuth tokens, per-host request queue and circuit breakers, proxy / TLS dispatchers, HTTP telemetry, the credential profile store and plugin availability — now lives in one runtime object built once at start-up (`createRuntime()` in `src/core/runtime.ts`) and handed to `registerAllTools`, which binds every tool call to it. `dispose()` clears it in place and is idempotent; the test-only `_reset*` hooks are gone (tests build their own runtime). No behaviour or `servicenow_get_status` shape change.
- **Artefact registry (internal).** A single typed registry (`src/core/artifacts/registry.ts`, SDK baseline 4.12.2) now describes each ServiceNow artefact type: table, child tables, name and key fields, script and JSON fields, references, SDK API and tiers. The script-tool type map and the capability probe list are derived from it with identical output, so no tool behaviour changes. Unverified descriptors are seeded for portal, UI page, UI Builder, flow, workflow, catalog, scripted REST, fix-script, email-script and transform-map artefacts, and are kept out of the script tools for now.
- **One request budget instead of per-attempt timeouts alone.** A logical request now stops at `SN_DEADLINE_MS` with `DEADLINE_EXCEEDED` even when every attempt fits in `SN_TIMEOUT_MS`; a retry that cannot fit in the remaining budget fails fast instead of waiting it out, and a caller-supplied abort is honoured and never retried.
- **Bounded per-host queue.** Requests beyond `SN_MAX_CONCURRENT` wait at most `SN_QUEUE_TIMEOUT_MS`, and at most `SN_MAX_QUEUE` wait at once — overflow fails fast with `BUSY` instead of piling up; `servicenow_get_status` and the `doctor` diagnostics bypass the queue so they still answer while the instance is saturated.
- **Upstream error pages are shaped.** A non-JSON error body (a proxy or login page, a hibernating instance) is classified `UPSTREAM_HTML`, tag-stripped and capped at 512 characters with a hint; JSON error detail is capped at 2 KB — a 300 KB HTML 502 no longer lands in the tool result.
- **OAuth requests share the HTTP client.** Token and code-exchange requests use the same primitive as API calls (dispatcher, `User-Agent`, deadline, the `auth` telemetry bucket, retry only for replayable requests) — under a proxy or mTLS they no longer bypass the configured transport.
- **`fetchAll` pages by `sys_id` cursor (S-8, C-2).** Without an `ORDERBY`/`ORDERBYDESC` (and without `^NQ`), a `fetchAll` read orders by `sys_id` and fetches each next page with `sys_id>last` instead of an offset, so rows inserted or deleted during the read no longer shift pages (skipped or duplicated rows). `sys_id` is requested for the cursor and stripped again when `fields` did not ask for it; a caller ordering keeps offset paging. `filtered` is exact when `X-Total-Count` is present and not reported otherwise in cursor mode.
- **`.env.example` is generated (D-3).** The file is regenerated from the settings manifest (every key, grouped by section, with defaults), and it now recommends `~/.config/servicenow-mcp-ai/.env` instead of the stale `sincronia-mcp` path. The `server.json` `environmentVariables` block is generated from the same source; its content is unchanged.
- **The README lists tools per package (N-42, TK-17).** The generated "Tools" table has one row per package instead of one per tool: a link to the package's `docs/tools/<package>.md` reference page, whether it is in the default `core` profile, and its read and write tool names. README.md shrinks from 135 KB to 125 KB; per-tool descriptions live in the generated reference only. `npm run docs:readme` renders it and `test/readme-sync.test.js` checks it.
- **Four confirmed artefact child tables report `verified:true` (O-5, OD-4).** A PDI check confirmed `sc_cat_item_category` and `sc_cat_item_user_criteria_mtom` (catalog items), `sys_ui_policy_action` (UI policies) and `sys_security_acl_role` (ACLs); `servicenow_get_artifact` now labels them with the type's flag instead of `verified:false`. Child reads still degrade on 400 / 403 / 404. `risk_assessment` and `app_dependency` stay `verified:false` (tables absent, OD-3). tools/list delta 0 B.
- **Flow, workflow, playbook and decision-table explanations report the registry `verified` flag (O-5, OD-4).** `servicenow_explain_flow` (every kind) and the `decision_table` explanation of `servicenow_explain_artifact` returned a hard-coded `verified:false`; they now return the flag of the explained registry type (`flow`, `subflow`, `flow_action`, `workflow`, `playbook`, `decision_table`), `true` since OD-4. The markdown summary line prints the same value and the caveats keep their PDI notes and open points. Reads are unchanged: an unreadable table still becomes a caveat. Service Portal and UI Builder explanations stay `verified:false`. tools/list delta 0 B.
- **The domain-analyser caveat of `servicenow_check_code_health` `extended` names what is verified (O-5, OD-4).** It said every Service Portal, UI Builder, legacy workflow and Flow Designer finding stays `verified:false`; Flow Designer and legacy workflow types are registry-verified since OD-4, so the caveat now says so and keeps `verified:false` for the Service Portal and UI Builder rule fields only. Rules and reads are unchanged. tools/list delta 0 B.
- **The UI Builder client script `includes` field is confirmed on a PDI (O-5, N-28).** It is a glide_list of `sys_ux_client_script_include` sys_ids, which is how `servicenow_get_artifact_dependencies` and `servicenow_where_used` already read it. The "unverified" notes are dropped. Reads are unchanged. tools/list delta 0 B.

### Fixed

- **`servicenow_get_flow` lists the steps of current Flow Designer flows (S-5, O-5).** It read only `sys_hub_action_instance`, so on a PDI every flow kept in the `_v2` step tables came back with no steps. It now reads the six action, flow-logic and subflow step tables that `servicenow_explain_flow` reads, and each step gains a tree `number` (`1`, `1.1`, …) and a `kind` (`action`, `logic` or `subflow`); `order`, `action` and `type` keep their meaning. The flow detail also gains `subflows` (the called subflows), `inputs` and `outputs` (name, label, type, mandatory), and `caveats` / `unreadable` when a step or variable table cannot be read. All new fields are optional; the tool description and output schema are unchanged (`tools/list` +0 B). Legacy workflows are unchanged.
- **`dictionary_script` now finds scripted defaults (S-4, O-5).** Its base query read `default_valueSTARTSWITHjavascript:`, and the instance evaluates a `javascript:` value as a script, so only 4 of a PDI's 5,475 scripted defaults matched; the script tools, snapshots and stats missed the rest. The colon is dropped, and the encoded-query reference resource explains how to match a stored `javascript:` prefix. tools/list delta 0 B.
- **Workspace dashboard widgets and form action layouts follow the PDI links (P-15, O-5).** `servicenow_explain_ui_experience` read dashboard widgets by a `tab` column that `par_dashboard_widget` does not have, so every tab listed no widgets; widgets are now read through `par_dashboard_canvas` (`dashboard_tab` → `canvas`). Form action layouts are now read through the experience's `actionConfigId` page property (`sys_ux_form_action_layout.action_config`), because one workspace's layouts span several application scopes; the scope read stays as a fallback with its own caveat. The dashboard and list menu links through page properties were confirmed, so their "unverified" caveat is gone.
- **Flow snapshots, compares and explanations include v2 triggers (O-5).** A PDI check showed that flows keep their trigger in `sys_hub_trigger_instance` or in `sys_hub_trigger_instance_v2` (21 of 120 sampled flows use the v2 table), but the flow and subflow registry children listed only the plain table. `servicenow_snapshot_instance`, `servicenow_compare_instances`, `servicenow_explain_artifact` and dependency reads now also read the v2 trigger and decode its `trigger_inputs`.
- **Plugin API errors name an inactive or missing plugin again (H-8, O-5).** A PDI check showed that a missing REST namespace answers 400, not 404, so the plugin hint, the availability cache and the plugin-state verdict never fired. A 400 with the missing-namespace body is now handled like the 404. The Code Search probe also checks `com.glide.code-search`, the id a PDI shows.
- **The upgrade review caveats name what one PDI confirmed (N-1, O-5).** `servicenow_review_upgrade` history and skipped-record output, and the upgrade document, said every table, field and choice name was unverified. A PDI dictionary check confirmed every column read on `sys_upgrade_history`, `sys_upgrade_history_log`, `sys_update_version` and `sys_update_xml`, so the caveat now says so; output stays `verified:false`. Store-update output keeps its own caveat: `sys_store_app` rows answer 403 to an admin. The `servicenow_upgrade_review` prompt says the same. tools/list delta 0 B.
- **Service Portal client scripts reach the AST lint engine (P-18, O-5).** A PDI check showed that every widget client script, link function and angular provider fell back to the regex engine, because a portal client source is a bare `function (...) {...}` expression that does not parse as a script; `servicenow_lint_script` and `servicenow_check_code_health({extended})` now parse it in parentheses, so comments and strings no longer flag. `sce-trust-as-html` also skips a value already passed through `$sanitize(...)` or `spUtil.sanitizeHtml(...)` (88 → 76 findings on the PDI).
- **Catalog explains name variable type 40 (O-5).** `servicenow_explain_artifact` on a catalog item, record producer, variable set or variable showed `type 40` for variables of the inactive `question.type` choice 40, which older variables still use (7 on the PDI). It now shows `Table Name`, as the catalog document already did. tools/list delta 0 B.
- **`servicenow_explain_ui_experience` lists every list of a workspace list menu (P-15, O-5).** A PDI check showed that `sys_ux_list` names its menu in `configuration` as well as its category, and that some lists have no category (3 of 207 on the PDI), so the list menu read through categories alone missed them. Lists are now read through both links, deduplicated, and the uncategorised ones appear in the menu's new `lists` array ("(no category)" in markdown); `counts.lists`, the audiences and the workspace tables include them. Lists and categories also carry `active`, and markdown marks inactive ones. tools/list delta 0 B.
- **The `servicenow_explain_portal` caveats name what one PDI confirmed (P-16, O-5).** The Service Portal caveat said the tables were unconfirmed on a live instance; it now says every table and field read exists on one PDI and names the shapes found (rows nested in columns, route map portals as a glide_list, widget_parameters literals returned raw). The options caveat now names the classic `sp_instance` columns that are not mapped instead of saying they may exist. Output stays `verified:false`. tools/list delta 0 B.
- **Workflow, playbook and decision table caveats name what one PDI confirmed (O-5).** The `servicenow_explain_flow` workflow and playbook caveats and the decision table caveat said their tables were unconfirmed on a live instance; they now say what was checked on one PDI (activities keyed by version and ordered by canvas position, playbook lanes and activities keyed as read, decision inputs and questions keyed by their decision) and what is still unconfirmed. Output stays `verified:false`. The workflow explainer no longer asks `wf_stage` for a `duration` column, which does not exist (it lives on `sys_hub_flow_stage`). tools/list delta 0 B.
- **Catalog document variable types match the instance (S-15, O-5).** A PDI check of the `question.type` choices showed that `document_instance({kinds:["catalog"]})` labelled type 14 "Macro" and 17 "Macro with label"; they are "Custom" and "Custom with label", and type 40 ("Table name", an inactive choice still used by 7 PDI variables) printed as a bare number. The catalog document also notes that an item's further categories (`sc_cat_item_category`) are not listed. The `sc_cat_item.sc_catalogs` list and the integrations fields were confirmed as read. tools/list delta 0 B.
- **`servicenow_explain_flow` `kind:"action"` accepts the published action snapshot (P-11 (e), O-5).** A PDI check showed that a flow's action steps reference `sys_hub_action_type_snapshot`, not the definition (2,779 of 2,789 `_v2` steps), so passing a step's `ref` with `kind:"action"` failed with "no record matches". The root read now falls back to the snapshot, whose steps, inputs and outputs hang off its own sys_id. `action` gains `state` and `master_snapshot` for a definition and `parent_action` and `master` for a snapshot. A draft with unpublished changes names the published snapshot in a caveat, and the caveat about reading only the draft is replaced. `tools/list` unchanged (0 B).
- **More secret column names are masked when the dictionary cannot be read (N-21, O-5).** A PDI check confirmed the `password` / `password2` types and the 12 fallback names, and found 12 more names that are `password2` wherever they occur (`sn_auth_token` on 703 tables, `auth_token`, `secret`, `secret_key`, `credential_value`, keystore / truststore / signing-key passwords, …). They now join the always-masked name list. `glide_encrypted` exists only with column-level encryption and stays in the type list.
- **ATF history counts step and suite membership edits as definition changes (N-16, NX-20, O-5).** A PDI check showed that editing a `sys_atf_step` or a `sys_atf_test_suite_test` row does not bump the parent test's or suite's `sys_updated_on`, so `with_results` could call a test flaky across an edit of its steps. The definition read now takes the newest `sys_updated_on` of the record and its steps or memberships (one more bounded query); a failed read falls back to the whole window. tools/list delta 0 B.
- **`servicenow_read_ops` `integrations` now reads the real `sys_outbound_http_log` columns (N-6, O-5).** A PDI check showed the HTTP method column is `method` (not `http_method`) and that the log has no `rest_message` column, so every group came back with an empty REST message. Groups are now host plus the calling table, and the group key `rest_message` is renamed `source_table`. The N-6 caveats no longer call the field names unverified, and `jobs` `by_state` labels the `sys_trigger` states `-1` (error), `-2` (permanent_error) and `-1776` (paused).
- **A graceful shutdown (SIGINT / SIGTERM) flushes stdout and stderr before exiting** (`flushStdio`, bounded by 250 ms). stdio pipes are asynchronous, so `process.exit(0)` right after the transport closed could drop the last frames (a listen stream's completion and cancellation, the final response) and the shutdown log line (N-35).
- **A plan token now survives across stateless 2026-07-28 HTTP requests.** Each modern request runs on its own runtime, so a token issued by the preview used to die with that request, and a destructive apply (or an MRTR confirmation) on the modern HTTP leg was always refused as `PLAN_REQUIRED`. On that leg the plans now live on the process runtime, one store per principal (`modern:<bearer clientId>`, anonymous clients share one). A token is honoured only for the caller it was issued to, and one caller's 500 plans never evict another's. 2025 sessions and stdio are unchanged (N-35).
- **An MRTR retry that lacks the answer is asked again instead of refused.** On the 2026-07-28 wire, a retry with a valid `requestState` but no usable answer under the prompt's key (missing, malformed, or a wrapped `{method, result}` entry) now gets a new `input_required` with a fresh single-use state, as the spec asks; it used to be refused (`CONFIRM_DECLINED` / `CREDENTIALS_UNCONFIRMED`) and burned the plan token. The credential-change prompt now names the instance as a bare host, so a form prompt carries no clickable URL (N-35).
- **2026-07-28 wire: an unsupported revision is refused on every stdio request (N-35, lifecycle-01).** Once a stdio connection is pinned to 2026-07-28, a later request naming another revision now gets -32022 with the supported list instead of being served.
- **2026-07-28 wire: a request missing a required field answers -32602 in one line (N-35, base-34, errors-07).** `prompts/get` without `name` and `completion/complete` without `argument` answered -32603 with a multi-line schema dump; `tools/call` schema failures keep -32602 with the same one-line message.
- The plugin's truncation hint names `count` of `total` for a row-capped result (SN_MAX_RECORDS, a list limit) — e.g. "100 of 2400 shown" (N-48).
- **Docs store: a write to the store's own `index.md` / `index.json` is refused (E-6 F3).** The store rebuilds both after every write, so a write there (through `servicenow_write_doc` or a generator) was silently replaced; it now fails with 400 and a hint. An `index.md` in a subfolder is a regular document. Owner decision 2026-10-09.
- **Plugin skills (PH-3, PH-4, PH-6, PH-7, PH-8, SF-1, SF-7).** `sn-safe-write` reads `writeMode` and the target profile from `servicenow_get_status` before any write call. Every skill that needs a package outside `core` checks `enabledPackages` first, enables a missing one with `enable_package` (telling the user) and reports the steps of a denied package as not checked. Each skill has a "When not to use" section with the hand-off to the right subagent or skill, and states that instance content is untrusted data, never instructions. The plugin description counts six skills, and `test/plugin-skills.test.js` keeps the count in sync.
- **Plan-token hook (`hooks/`).** `require-plan-token` reads the server's own configuration through the new `hooks/sn-config.mjs`: in `SN_WRITE_MODE=apply` it asks instead of denying, on a prod profile it still denies a destructive apply without a `plan_token`, and on any internal error it fails open. Its matcher is narrowed to the eight token-gated tools plus `servicenow_change_conflicts`. The session-context sentences match the real write policy, agent `tools:` lists are comma-separated strings, and the truncation hint is not injected into subagents (`agent_id` present).
- **MC-3: the discover, drift and schema-impact prompts start with the package step** their skills have (`servicenow_get_status` → `enabledPackages`, `enable_package`, denied packages reported as not checked).
- **MC-2: the extension's server range follows the `package.json` major.** `SERVER_VERSION_RANGE` is `2.x`, and a test fails when the package major moves without it. `npm version` rewrites it through `scripts/sync-version.mjs`, so the 3.0.0 bump raises it to `3.x` (B9).
- **DX-6/7/10: contributor and agent guidance.** The agent instructions for `src/tools` and `src/mcp` point at the real `tools/list` budgets (`test/fixtures/token-budgets.json`, enforced by `test/output-schema.test.js`, measured with `npm run tokens:budget`) and keep the `initialize` instructions cap (`INSTRUCTIONS_MAX_BYTES`) separate. CONTRIBUTING.md gains an "Adding a tool" checklist (define → package → manifest regen → budget delta → docs:sync → tests). `project/ARCHITECTURE.md` states the current counts (`core` 24 tools; 25 opt-in packages with 87 tools; 97 in all), and `.github/copilot-instructions.md` follows the current tracker (O-1…O-22, `roadmap.yaml`, `TODO.md`, the 2026-10 analyses).
- **MC-7: the `.mcpb` manifest declares its dynamic tool list and Desktop range.** `scripts/mcpb-manifest.mjs` now emits `tools_generated: true` (the session's tools follow `SN_TOOL_PACKAGES` / `servicenow_enable_package`, so hosts ask `tools/list`) and `compatibility.claude_desktop: ">=0.10.0"`; `mcpb/manifest.json` regenerated, and the official MCPB 0.3 schema accepts it. `tools/list` unchanged (0 B).
- **MC-6: stale docs.** `docs/llms.txt` no longer links the archived `project/PRODUCT-STATE.md` (now `project/ROADMAP-V3.md`), and its Distribution section lists every channel: npm, the MCP registry, the Claude Code plugin, the VS Code extension, the `.mcpb` bundle and the skills. The `servicenow_instance_overview` row in the README (and `docs/llms-full.txt`) no longer says "until H-11": the caution follows the profile's environment marker.
- **Stale tool descriptions (N-59, phase T1).** `servicenow_generate_fluent` no longer says the SDK target is "not type-checked" (it has been verified against `@servicenow/sdk` since P-29), and `servicenow_upsert_artifact` no longer exposes the internal gate id "O-5". `tools/list` `all` −34 B (135,991 → 135,957).
- **Stable list order across package toggles (N-38).** `resources/list` and `resources/templates/list` kept a different order after a package was disabled and enabled again: its resources were re-registered at the end. Package resources are now registered after the always-on ones, in manifest order, and re-registered in that order on every enable, so a fresh session and a toggled one list tools, prompts, resources and templates identically (`test/dynamic-packages.test.js`). `tools/list` unchanged (0 B).
- **Oversized results keep their JSON shape (N-61).** `runSpec` now caps every successful JSON object result at `SN_MAX_RESULT_CHARS` by shrinking its largest arrays (binary search), never by cutting text, so keys such as `plan_token` survive and capped structured results still pass the output schema; the payload gains `truncated: true` and a `note`. `okQueryResult` truncation uses binary search instead of halving, `servicenow_list_changes` defaults to 10 rows, and `servicenow_explain_artifact` charges the full serialized size of child rows to its budget. Results with no array left to shrink are capped too: a `format:"csv"` payload keeps its header and whole rows (quoted newlines respected), long strings keep a prefix (a Mermaid diagram is never cut), and a top-level array or plain-text result keeps its leading items or lines with the note as a second text block.
- **Lower `tools/list` budgets (N-57 step 1).** Budgets live in `test/fixtures/token-budgets.json`: all 136,192 B, core 32,768 B (were 151,000 / 36,500); the tasks and legacy-name surfaces are budgeted too (137,472 / 154,624 B).

- **Fluent emitters match the SDK 4.12.2 API (P-26…P-28, found by the P-29 oracle).**
  - Flow and action output:
    - `action.core.*` calls are typed from the generated input table. An action whose inputs cannot be typed falls back to the untyped sys_id form.
    - Action steps are called by their built-in name.
    - `endFlow` appears only inside blocks.
    - Unused callback parameters are prefixed with `_`.
    - A workspace always gets `tables`.
    - A `wfa.action` key is declared on `sys_hub_action_instance_v2`, the table the SDK writes. A key on the legacy table made the build mint a new sys_id.
  - `keys.ts` only declares keys that a `$id` references. Before, an unused key, such as a flow variable or a playbook timer the SDK gives no `$id`, built as a `DELETE` of that sys_id, so installing the app would have deleted the source record.
  - `SPPage` takes no `$id` in SDK 4.12.2, so the SDK mints the page's sys_id. Each page now carries a note: installing on the source instance can add a second page with the same `pageId`.
  - The SDK also mints its own ids for flow variables, stages, action inputs and outputs, and process inputs.
  - Generated files now say that the shapes are type-checked against the SDK; instance behaviour is still unverified (O-5).

- **`logging/setLevel` over HTTP (M-8).** The level a client sets is stored by the SDK under its session id; log notifications were sent without one, so the level was ignored on the Streamable HTTP transport. They now carry the call's (or the transport's) session id. stdio is unchanged.
- **Hibernating instances and login pages are no longer returned as data.** An API call answered with an HTML page and HTTP 200 (a hibernating developer instance, a login / SSO page) now fails with `INSTANCE_HTML_RESPONSE` and a hint to wake the instance at developer.servicenow.com, instead of returning the page as `{ raw }`.
- **`fetchAll` no longer stops at the first short page.** Row-level ACLs drop rows after paging, so it reads on until `X-Total-Count` or an empty page, bounded by a scan budget (`cap × 10` rows). Results report `truncatedReason` (`cap` or `scan_limit`) and `filtered` (rows withheld from this user), and the note names the real cause.
- **Redaction and diagrams understand `display_value=all`.** PII redaction now reaches inside `{ value, display_value }` pairs, and the diagram generators read pair and reference-link shapes.
- **Traces and table-flow diagrams now include inherited and global business rules.** `servicenow_trace_table_event` and `servicenow_generate_table_flow` walk the table's inheritance chain (`incident` → `task`) and add `global=true` rules, interleaved by `order` as the platform runs them; each entry says which table it comes from (`table`, `inherited_from`, `global`) and the diagram gives inherited / global rules their own lanes. Flow triggers, legacy workflows and notifications are filtered by the traced operation (a `record_create` trigger no longer shows up in an `update` trace; nothing record-triggered is listed for `query`).

### Security

- **Batched plugin APIs meet their backing tables (H-4, N-3).** Behaviour change: a `servicenow_batch` sub-request to a plugin API is now refused (`POLICY_DENIED`, before any request) when a table its own tool checks is denied: `sc_catalog`, `sc_category`, `sc_cat_item` (plus `sc_request` and `sc_req_item` for `order_now`), `kb_knowledge`, `change_request`, `sys_atf_test_suite` / `sys_atf_test`, and for Instance Scan `scan_result` plus a point scan's `target_table`. `/api/sn_cicd/instance_scan/...` is now governed by the `codecheck` package instead of `atf`, and a batched scan run (non-GET) is refused on a profile marked prod (`SN_ENV`), as `servicenow_check_code_health` refuses it. 0 B tools/list.
- **Type-based secret masking, always on (N-21).** Behaviour change: a column whose dictionary `internal_type` is `password`, `password2` or `glide_encrypted` is now masked as `[redacted]` in every tool result and in the write journal (`fields`, `before`, `error`), whatever `SN_REDACT_FIELDS` says, and no setting turns it off.
  - Every Table API read and write, direct or inside `servicenow_batch`, resolves its secret columns from one cached `sys_dictionary` query per profile and, only when a key matches a secret column name, the table's inheritance chain (`sys_db_object`). Both are schema-cached (`SN_SCHEMA_CACHE_TTL_SEC`). Dot-walked keys are matched on their leaf name, and `{ value, display_value }` pairs are masked whole.
  - The secret values a call read are also masked wherever the same call re-embeds them, so `servicenow_compare_instances` diffs, documents and exports cannot carry one under another key. A password-type system property's `value` is masked the same way.
  - A schema miss (an ACL-restricted dictionary, a denied table, an error) never blocks the read: a list of OOTB secret column names (`password`, `user_password`, `client_secret`, `api_key`, `private_key`, …) applies, and it applies always as a floor. This means a field named `password` is now masked even with redaction off, including in `servicenow_set_credentials` results that already showed `***` (they keep `***`).
  - Limits: other REST surfaces (CMDB, import set, plugin APIs) are masked by the OOTB names only; values shorter than four characters are masked by their key only, not where re-embedded.
- **Deep redaction at the result boundary.** `SN_REDACT_FIELDS` / `SN_REDACT_PII` now apply to every tool result — success and error, nested objects included, write tools' echoed records too — and to the `fields`, `before` and `error` values in the write journal, instead of only the top-level fields of query results.
- **Docs store hardened.** Writes through the docs store reject the journal's own files (`write-journal.*`) and any path that leaves the docs directory through a symlink (the nearest existing ancestor is resolved and must stay inside the real docs root). The `.env` writer uses a unique temp file per write (pid + random suffix) and removes it on failure, so concurrent `servicenow_set_credentials` calls never share a temp file and always leave a complete `.env`, and a failed write leaves no plaintext copy behind.
- **BREAKING (B7): credential host binding (H-2).** `servicenow_set_credentials` now refuses to move a configured profile to a different instance unless `user` and `password` arrive in the same call (`CREDENTIALS_INCOMPLETE`; nothing is written and no cache is touched), so the secrets stored for one host can never be sent to another. The elicitation confirmation also fails **closed**: a client that cannot confirm the change (no elicitation capability, no live server, or a prompt error) is refused unless the operator sets `SN_ALLOW_UNCONFIRMED_CREDENTIAL_CHANGE=1`; an explicit decline is always refused.
- **Dependency floors raised; the production audit is clean again.** `@modelcontextprotocol/sdk` now requires `^1.30.0` (installed 1.30.0) and `zod` `^3.25.0` (the SDK's peer range). The lock picks up the patched transitive `fast-uri`, `ip-address`, `hono`, `@hono/node-server`, `qs` and `body-parser`, so `npm audit --omit=dev --audit-level=high` — the last step of `npm run check` — exits 0 again, with no `overrides`.
- **`proxy-addr` 2.0.8 (GHSA-jqcg-44mw-7w3h).** The lock moves the transitive `proxy-addr` (via `@modelcontextprotocol/sdk` → `express`) from 2.0.7 to 2.0.8, which fixes IP spoofing through IPv4-mapped IPv6 trust subnets. Lock-only fix; no floor changed and no `overrides`.
- **Explicit ports and IPv6 literals need an allow-list entry.** An instance value with a non-443 port or a bracketed IPv6 address (`host:8443`, `[2001:db8::1]`) is accepted only when `SN_ALLOWED_HOSTS` names it, so a crafted host cannot steer credentials to an unexpected service. Proxy credentials in `SN_HTTPS_PROXY` / `HTTPS_PROXY` are never logged or returned, and `SN_TLS_REJECT_UNAUTHORIZED=false` logs a warning the first time a connection is built.
- **The OAuth browser open no longer goes through cmd.exe (N-56, MCP05).** On Windows the `login` command used to run `cmd /c start "" <url>`, and cmd.exe could split the authorization URL at `&`. It now passes the URL as one argument to `rundll32 url.dll,FileProtocolHandler`, so no shell runs on any platform. Only `http:` and `https:` URLs are opened; anything else is printed but never opened.
- **Outbound hardening (H-6).** HTTP redirects are never followed: a 3xx fails with `REDIRECT_BLOCKED` naming the target host, so a body or auth header cannot be forwarded to another origin (API, Jira and OAuth token requests alike). An `SN_ALLOWED_HOSTS` suffix entry no longer opens an internal or loopback host — only an exact entry does — and the internal-range check now covers IPv6 (loopback, unspecified, link-local, unique-local, IPv4-mapped and -compatible forms). `send_email` recipients (to, cc, bcc) must be single addresses (at most 50 per list) inside `SN_EMAIL_ALLOWED_DOMAINS`, or — **new default** — users in the instance's own `sys_user` directory; a failed directory lookup fails closed (`RECIPIENT_NOT_ALLOWED`). The OAuth loopback listener answers stray paths (404) and foreign `state` (400) without aborting the login. Upload file names are reduced to a sanitised leaf name (no path, control or bidi characters, 255 bytes). The docs store rejects Windows device names and `:` streams and refuses paths that leave the folder through a symbolic link. `SN_TLS_REJECT_UNAUTHORIZED=false` now also warns at startup and in `servicenow_get_status` (`http.warnings`).

### CI

- **O-14: prerelease tags (ADR 0011).** A version with a `-` (`3.0.0-beta.1`) is published to the npm `next` dist-tag and its GitHub Release is marked as a prerelease; `publish-mcp.yml` and `publish-vscode.yml` skip prerelease tags, so the MCP Registry and the extension move only on a GA version (`workflow_dispatch` still runs both). A GA tag keeps `latest`.
- **Path-scoped agent instructions (N-46).** One source per code area in `.github/agent-instructions/<area>.md` (api, core, mcp, tools, test, scripts, extension, plugin, docs, workflows) renders to `.github/instructions/<area>.instructions.md` with GitHub Copilot `applyTo` frontmatter, so an assistant editing `src/api/**` gets only that area's rules. `npm run docs:instructions` regenerates them; `-- --check` is part of `npm run check`, and `test/agent-instructions.test.js` fails on drift or on a glob that names a missing path. `.github/copilot-instructions.md` now points to them and drops two stale rules.
- **MCP bundle (N-51).** On the ubuntu / Node 22 leg, `ci.yml` builds the bundle (`npm run mcpb:pack`). `publish.yml` gains an `mcpb-asset` job that packs, validates and attaches the `.mcpb` to the tag's GitHub Release. The job is dormant until the owner enables publication (O-22) with the repository variable `MCPB_RELEASE=true`.
- **Tool-surface security scan (N-56).** `npm run scan:surface` (`scripts/scan-surface.mjs`) is now a step of `npm run check`. It loads the server from source and scans every tool description and parameter description, the server instructions, prompts, plugin skills, subagents, hooks and manifests for invisible or bidirectional Unicode, hidden-instruction phrasing, directives that name another tool or server (the plugin's own `mcp__plugin_servicenow-mcp-ai_servicenow__<tool>` names pass only for tools the server has), and URLs outside an allowlist; any finding fails the gate. SECURITY.md gains an "OWASP MCP Top 10 mapping" section that names the control (or the open gap) for each risk.
- **Skill evals (N-49).** `evals/<skill>/` holds a `claude plugin eval` suite for each of the five plugin skills (a trigger case and a negative case), answered by tool mocks under `evals/mocks/servicenow/` recorded from the test fetch double (`npm run eval:mocks`). `npm run eval:skills` runs the suites locally; the new `skill-evals.yml` workflow runs them on `workflow_dispatch` only, since a run needs a model API key. `test/skill-evals.test.js` checks that the suites are well formed and that the committed mocks match a fresh recording.
- **Distribution hygiene (D-6).** The VS Code extension now launches `npx -y servicenow-mcp-ai@3.x` — one constant (`SERVER_SPEC` in `extension/src/config.ts`) used by the stdio definition, the HTTP transport and the doctor, pinned to the server major it was built for (`3.x`, not `^3`, which `cmd.exe` would mangle). `publish-vscode.yml` packages one `.vsix`, publishes it to the Marketplace and, when the `OVSX_PAT` secret is set, to Open VSX (`ovsx` pinned). `publish.yml` gains a `github-release` job that creates the GitHub Release for the tag with the matching CHANGELOG section (`scripts/release-notes.mjs`; a version without a section fails the job). `package.json` declares `publishConfig` (`access: public`, `provenance: true`). `test/distribution.test.js` (10 tests) guards the pin, the launchers, the workflows, the SHA pins and the stale `LeassTaTT` namespace.
- **CodeQL** static analysis and **weekly dependabot** (npm root + `extension/` + github-actions); the Windows CI leg now runs lint + format as well.
- **Supply-chain pins (H-9):** every workflow action is pinned to a commit SHA with the version in a trailing comment (`actions/checkout` v4.4.0, `actions/setup-node` v4.4.0, `codecov/codecov-action` v5.5.5, `github/codeql-action` v3.38.1); Dependabot moves the pins.
- **Least privilege + hygiene (H-9):** every workflow declares a top-level `permissions: contents: read` (with `id-token: write` only where a publish needs OIDC and `security-events: write` only on the CodeQL job); `ci.yml` cancels superseded runs (`concurrency`), every CI job has a `timeout-minutes`, and a new `actionlint` job (1.7.12, pinned image) lints the workflows and their `run:` blocks.
- **New CI steps (H-9):** `npm run pack:check` (tarball guard), "No tracked env files" (fails when anything matching `.env*` other than `.env.example` is tracked) and "Prettier idempotency" (a `prettier --write` pass over a tree that passed `format:check` must change nothing).
- The Node 12 launcher probe derives the expected major from `package.json` `engines.node` instead of a literal, so raising the floor cannot hollow out the probe; `test/version-sync.test.js` pins the launcher's literal to `engines.node`.
- `publish-mcp.yml` downloads a **versioned, checksummed** `mcp-publisher` (1.8.1, verified with `sha256sum --check` against the release's `registry_<version>_checksums.txt`) instead of `latest`.
- **Extension job (E-6):** a new `extension` job in `ci.yml` runs `npm ci` and `tsc -p . --noEmit` in `extension/` on its own lockfile (same pinned `checkout` / `setup-node` SHAs, `contents: read`), so a broken VS Code extension cannot reach `publish-vscode.yml` unnoticed.

### Internal

- **Plugin eval hardening (EV-1, EV-2, EV-3, EV-4, EV-12).** The eval mocks pin volatile fields and `npm run eval:mocks:check` is part of `npm run check`. `evals/tool-selection/description-hashes.json` records the tool descriptions of the last tool-selection run and a test fails on drift (`npm run eval:tools -- --write-hashes` refreshes it). Negative cases carry hard write bans through guard mocks; new cases cover a destructive plan → token → apply in `sn-safe-write` and two read-only negatives (history, suggest).
- `test/harness-files.test.js` flags a tracked `CLAUDE.md` / `CLAUDE.local.md` at any depth, not only at the root.
- **N-18: the tool-selection eval measures the real `discovery` profile.** Since N-63 the default profile is `discovery`, so the eval's profiles are now `discovery` (the real default `tools/list`), `all` and `deferred` (the former simulated `discovery`: the discovery tools in full, every other tool as a stub). `core` is no longer an eval profile, since it names the same packages. `servicenow_find_tools` joins `enable_package` / `list_packages` as a correct first step when a case's tool is not loaded. Offline lexical baseline rewritten: discovery 16.1 % (core was 12.8 %), all 33.6 %, deferred 30.2 %. tools/list: +0 B.
- **N-18: tool-selection eval.** `evals/tool-selection/` (150 cases: one per tool, confusable clusters, writes, negatives) and `npm run eval:tools`, on demand and outside the gate. Profiles `core`, `all` and a simulated `discovery`; backends `lexical` (offline, deterministic), `recorded` and `anthropic` (Messages API via fetch); per-package and per-cluster top-1, confusion pairs, plan-first check, `--write-baseline` / `--max-drop`. Only the offline lexical baseline is committed; the model baseline waits for O-10 (d).
- The retry matrix, per-host concurrency semaphore and per-host telemetry moved into a shared substrate (`src/core/http-util.ts`). **Jira Cloud client scaffolding** (`src/core/jira/`, ADF helpers under `src/api/jira/`) landed fully reviewed and tested — **no Jira tools are exposed yet**; the surface stays dark pending the go/no-go decision (ARCH-14).
- Dedicated unit tests for the policy engine, the write journal, OAuth redirect parsing, the shared HTTP substrate and the Jira scaffolding, plus an **http-twin parity guard** (`test/http-twin-parity.test.js`) that pins the shared retry/semaphore behaviour of the ServiceNow and Jira HTTP clients so they cannot drift apart unnoticed; a credential host-binding suite (`test/admin-credentials.test.js`), a process-lifecycle suite (`test/lifecycle.test.js`, with child-process crash probes), a schema-cache suite (`test/cache.test.js`), an HTTP-resilience suite (`test/http-resilience.test.js` — dispatcher / proxy / TLS matrices on a fake `undici`, deadline, queue, breaker, error shaping, host policy) and a `User-Agent` suite (`test/identity.test.js`), plus inherited / global business-rule trace and diagram-lane tests and a version-sync / launcher-guard suite (`test/version-sync.test.js`); the suite is now 494 tests.
- The mTLS module is now a thin alias over the shared dispatcher factory (`src/core/dispatcher.ts`); the server identity (version, `User-Agent`) lives in `src/core/identity.ts`, and both HTTP twins call one `rawRequest` primitive in `src/core/http-util.ts` (the parity guard covers the new options).
- `getTableChain` is now cached with the other schema reads (the trace and table-flow generators resolve the inheritance chain on every call).
- **Tarball guard (H-9):** `scripts/pack-check.mjs` (`npm run pack:check`, part of `npm run check`) runs `npm pack --dry-run --json` and fails on any entry outside `LICENSE` / `README.md` / `package.json` / `bin/` / `build/`, on a `jira/`, `.map`, `test/` or `docs/instance/` entry, or on an unpacked size above 800 KB (raised from 600 KB on 2026-09-24 as 3.0 grew); `package.json` `files` gains `!build/**/jira/**` so the dark Jira client never ships.
- **One version everywhere (H-9):** `scripts/sync-version.mjs` (`npm run version:sync`, `--check`) copies the `package.json` version into `package-lock.json`, `server.json`, `extension/package.json`, `extension/package-lock.json`, `.claude-plugin/plugin.json` and `docs/index.html`; the `npm version` lifecycle hook runs it and stages the followers, and `test/version-sync.test.js` fails the gate on any skew. The existing skew is fixed (`.claude-plugin/plugin.json` was still 2.0.0).
- `.gitignore` now covers every `.env*` variant (per-profile `.env.<profile>`, the updater's `.env.tmp-*`) and tracks only `.env.example`.
- Prettier: `proseWrap: preserve` is explicit in `.prettierrc.json`, `npm run format` runs two passes (the formatter is not idempotent on a few Markdown constructs), and CONTRIBUTING documents the version-bump flow (`npm version` → tag → CI publish) and the Markdown gotchas.
- **Import cycles (E-7):** the Fluent emission primitives (run state, secret detection, sidecars, `Now.ref` / `Record()` building, file headers) moved from `src/api/fluent.ts` to `src/api/fluent-emit.ts`, which cuts the `fluent` ↔ `fluent-ui` runtime cycle. `test/import-cycles.test.js` fails on any runtime cycle in `src/api` and pins the modules still on a cycle in `core/` and `mcp/` (the list can only shrink). `tools/list` delta: 0 B.
- **Import cycles (E-7):** the tolerant `json` decoder and the `Decoder` / `DecodeOutcome` contract moved from `src/core/artifacts/decoders.ts` to `src/core/artifacts/json-decoder.ts` (still re-exported from `decoders.ts`), which cuts the `decoders` ↔ `flow-values` cycle; both modules leave the pinned list. `tools/list` delta: 0 B.
- **Import cycles (E-7):** the core settings/logging bootstrap cycle is cut: `activeProfile` and `PROFILE_RE` moved into `src/core/settings-manifest.ts` (`profile.ts` re-exports them), and the manifest reports an invalid setting through `setSettingWarn`, which `logging.ts` points at the logger when it loads. Six `core/` modules leave the pinned list. `tools/list` delta: 0 B.
- **Import cycles (E-7):** the package policy (package names, the `core` / `reader` / `developer` / `all` profiles, `resolveEnabledPackages`, `effectivePackages`) moved from `src/mcp/registry.ts` to `src/mcp/package-policy.ts`, still re-exported from the registry, which checks at load that the static package list matches its manifest. The status payload reads the policy from there, which cuts the `registry` → `resources` / `tools/admin` → `status` → `registry` cycle. That was the last pinned cycle: `test/import-cycles.test.js` now asserts that `src/` has no runtime import cycle at all. `tools/list` delta: 0 B.
- **Cancellation predicate (E-7):** `isCancelledError` and `rethrowIfCancelled` in `src/core/errors.ts` replace 27 local copies of the `CANCELLED` check (twelve module-private helpers among them) across 20 API modules and the SDK guard. Behaviour is unchanged. `tools/list` delta: 0 B.
- **Degrade status and IN chunk (E-7):** `degradeStatus()` and `IN_CHUNK` in `src/api/shared.ts` replace eight copies of the 400/403/404 degrade set (four module-private `degradeStatus` helpers among them) and seven copies of the 100-id `IN` chunk size. Behaviour is unchanged. `tools/list` delta: 0 B.
- **Settings manifest split (E-7):** `src/core/settings-manifest.ts` (1,888 lines) keeps the sections, the `<KEY>_FILE` derivation and every reader; the spec types, kind helpers and release markers are in `settings-model.ts`, and the 125 rows in `settings-data-server.ts` (connection … packages) and `settings-data-runtime.ts` (policy … external), in documentation order. The manifest's exports, the settings and their parse behaviour are unchanged, so `docs:env` output is identical. `tools/list` unchanged (0 B).
- **Artefact registry split (E-7):** `src/core/artifacts/registry.ts` (3,805 lines) is split into `registry-model.ts` (descriptor shapes, SDK API lists, shared building blocks) and four data files (`registry-scripts`, `-platform`, `-ui`, `-process`) that `registry.ts` concatenates in the original order; it keeps every export, so no importer changes. The 136 descriptors are identical, in the same order. `tools/list` delta: 0 B.
- **sys_id check (E-7):** `isSysId()` and `isSysIdAnyCase()` in `src/core/sys-id.ts` replace 31 copies of the 32-hex sys_id regex (19 module-private constants and 12 inline literals) across 26 modules; `isSysId` moved there from `src/api/uib-usage.ts`. Each site keeps its case rule. Behaviour is unchanged. `tools/list` delta: 0 B.
- **Dead re-exports:** 21 re-exported names that no module or test imports from the re-exporting module are dropped from `src/api/codecheck.ts`, `src/api/snapshot.ts`, `src/core/http.ts`, `src/core/lifecycle.ts`, `src/core/artifacts/decoders.ts` and `src/mcp/transport.ts`; the defining modules still export them. `tools/list` delta: 0 B.
- **Bounded reads (E-7):** `src/api/bounded-read.ts` (`boundedRead`, `boundedReadIn`, `noteMissing`, `ReadCtx`, `Unreadable`) replaces the three private copies in `src/api/ui-experience.ts`, `src/api/portal.ts` and `src/api/explain-flow.ts`; each explainer names what a cap leaves incomplete through `ReadCtx.scope`, so caveat texts are unchanged. `tools/list` delta: 0 B.
- **Document generators split (E-7):** `src/api/document.ts` (3925 lines) keeps the kind registry, the writer, `document_instance`, the README and discovery kinds (the README reads `DOC_KINDS` titles and discovery builds on the README, so both stay beside the registry); the table, app, security, catalog, integrations and artifact-types kinds move to `src/api/doc-table.ts`, `doc-app.ts`, `doc-security.ts`, `doc-catalog.ts`, `doc-integrations.ts` and `doc-artifact-types.ts`, and the caveats, Markdown helpers, guarded section reads and run-context types to `src/api/doc-shared.ts`. Code moved verbatim; `document.ts` re-exports every name it exported, so importers are unchanged. `tools/list` delta: 0 B.
- **Fluent UI specs split (E-7):** the P-28 property specs (the portal, workspace and catalog field → Fluent property tables, about 880 lines of data) and the `camel` helper they call while loading move from `src/api/fluent-ui.ts` to `src/api/fluent-ui-specs.ts`; the builders and the dispatch stay. Code moved verbatim; `fluent-ui.ts` keeps its exports. `tools/list` delta: 0 B.
- **explain_flow split (E-7):** `src/api/explain-flow.ts` (2,588 lines) keeps the module doc and the `explainFlow` dispatcher; the bounds, caveats and result shapes move to `explain-flow-model.ts`, the root read, degrade path, field accessors, data-pill labels and `values` decoder to `explain-flow-read.ts`, the flow / subflow / action readers and the call expansion to `explain-flow-hub.ts`, the legacy workflow and its migration report to `explain-flow-workflow.ts`, the playbook reader to `explain-flow-playbook.ts`, and `flowMermaid` / `flowMarkdown` to `explain-flow-render.ts`. Code moved verbatim; `explain-flow.ts` re-exports every name it exported, so importers are unchanged. `tools/list` delta: 0 B.
- **Flow Fluent emitter split (E-7):** `src/api/fluent-flow.ts` (1,829 lines) keeps `emitFlowFile`, `attachFlowTrees` and the test internals; the mapping tables and pure helpers move to `fluent-flow-maps.ts`, the `FlowEmit` base (per-file state, values, columns, stages, `Record()` fallbacks, leftovers, the file writer) and the hooks contract to `fluent-flow-base.ts`, and the flow / subflow / action body and the playbook body to the `FlowDefEmit` (`fluent-flow-def.ts`) and `PlaybookEmit` (`fluent-flow-playbook.ts`) subclasses. Methods moved verbatim; `fluent-flow.ts` keeps its exports, and `fluent:verify` and the Fluent goldens are unchanged. `tools/list` delta: 0 B.
- **Domain analysers split (E-7):** `src/api/domain-analysers.ts` (1,605 lines) keeps the module doc and `analyseDomains`; the rule table, result shapes, bounded reads and findings collector move to `domain-rules-shared.ts`, and the rule groups to `domain-rules-flow.ts`, `domain-rules-portal.ts`, `domain-rules-uib.ts` and `domain-rules-workflow.ts`. Code moved verbatim; `domain-analysers.ts` re-exports every name it exported. `tools/list` delta: 0 B.
- **UI experience split (E-7):** `src/api/ui-experience.ts` (2,413 lines) keeps the `explainUiExperience` readers; the option and result shapes and the `detail` levels are in `ui-experience-types.ts`, the Mermaid and Markdown renderers in `ui-experience-render.ts`. Statements moved verbatim and the module re-exports all 29 former exports. `tools/list` unchanged (0 B).
- **codecheck rules split (E-7):** the pure source-lint engine of `src/api/codecheck.ts` (the line-regex rules, the S-12 AST rules, `lintSourceRegex` / `lintSourceDetailed` / `lintSource` and the `Finding` shape) moves to `src/api/codecheck-rules.ts`; the script, table and artefact lints and `codeHealth` stay. Code moved verbatim; `codecheck.ts` re-exports every name it exported. `tools/list` delta: 0 B.
- **explain_portal split (E-7):** `src/api/portal.ts` (1,343 lines) keeps the module doc and `explainPortal`; the depth and page bounds, caveats and result shapes move to `portal-model.ts`, the row helpers, widget option mapping, layout, root, route-map and degrade reads to `portal-read.ts`, and `portalMermaid` / `portalMarkdown` to `portal-render.ts`. Code moved verbatim; `portal.ts` re-exports every name it exported. `tools/list` delta: 0 B.
- **Flow trace split (E-7):** `src/api/flows.ts` (1,278 lines) keeps the table-event trace (`traceTableEvent`, `traceTableFlow`, business rules, flows, workflows, notifications, Mermaid); the operations, lanes and chain-entry shapes with the label / origin / ordering helpers move to `flows-model.ts`, the S-5 opt-in lanes to `flows-lanes.ts`, and the FT-1 `listFlows` / `getFlow` and FT-3 `getFlowRuns` reads to `flows-designer.ts`. Code moved verbatim; `flows.ts` re-exports every name it exported. `tools/list` delta: 0 B.
- **Docs store split (E-7):** `src/api/docs.ts` (1,258 lines) keeps the writes (`docsWrite`, `docsReadRaw`, `docsWriteRaw`, `docFields`, `openDocStream`); the path confinement, directory walk and profile scoping move to `docs-paths.ts`, the S-14 frontmatter and manual blocks to `docs-frontmatter.ts`, `docsList` / `docsRead` / `docsSearch` to `docs-inspect.ts` and the `index.json` / `index.md` manifest to `docs-manifest.ts`. Code moved verbatim; `docs.ts` re-exports every name it exported. `tools/list` delta: 0 B.
- **Dependency graph split (E-7):** `src/api/dependencies.ts` (1,155 lines) keeps node loading and the walk (`artifactDependencies`); the bounds and node / edge / result shapes move to `dependencies-model.ts`, the canonical node ids and the `Graph` collector to `dependencies-graph.ts`, the descriptor-driven outbound edges to `dependencies-outbound.ts`, the reverse queries to `dependencies-inbound.ts` and `dependencyMermaid` to `dependencies-render.ts`. Code moved verbatim; `dependencies.ts` re-exports every name it exported. `tools/list` delta: 0 B.
- **Transport primitives split (E-7):** `src/core/http-util.ts` (1,228 lines) keeps the module doc and the `rawRequest` primitive; the per-host telemetry moves to `http-telemetry.ts`, the concurrency semaphore and circuit breaker to `http-limits.ts`, the retry matrix, backoff and signal helpers to `http-retry.ts`, the error-body shaping and `ErrorFactory` to `http-error-body.ts`, and the redirect checks and bounded body reads to `http-body.ts`. Code moved verbatim; `http-util.ts` re-exports every name it exported, so the ServiceNow, Jira and OAuth callers are unchanged. `tools/list` delta: 0 B.
- **Instance collectors (E-7):** the snapshot's reads moved into `src/api/collectors.ts` — `collectTables`, `collectSchema`, `collectPlugins`, `collectApps`, `collectAutomation` and `collectRecordSection`. Each takes `{ signal?, progress? }` and returns `{ data, unreadable, truncated?, capped, errors }` with no file I/O and no Markdown; only a cancelled call throws. `servicenow_snapshot_instance` is now composition plus rendering over them and `servicenow_compare_instances` reads its live tables, plugins, apps and record sections through the same collectors; writer goldens (`test/writer-goldens.test.js`, `test/fixtures/docs/writers/`) pin both outputs byte for byte. The one visible difference: a cancelled comparison no longer turns an in-flight plugin/app read into an "unavailable" warning, it stops as cancelled.
- `snParams()` in `src/api/shared.ts` builds `sysparm_*` query parameters from optional values; the Aggregate, Change, Knowledge, Catalog and CMDB clients use it instead of hand-written `if (…) params.set(…)` chains. `@typescript-eslint/no-unsafe-assignment` and `no-unsafe-member-access` are enforced for `src/` (one violation fixed).
- **Test architecture (E-6):** fetch double v2 in `test/helpers.js` (`createFetchDouble` / `withFetchDouble` — routes, headers, delays, streaming bodies and a call log with virtual timestamps — plus `fakeClock` over `mock.timers`), covered by `test/fetch-double.test.js`; the HTTP-resilience, retry and cancel/progress suites run on virtual time with no wall-clock assertions (this fixes an intermittent cancel/progress failure under load). Property tests with a fixed seed (`fcParams()`, `SN_FC_SEED` / `SN_FC_RUNS`) for the retry matrix, policy resolution, the query encoders, redaction, CSV, docs paths, the host / MIME allow-lists, env values and the write-journal chain (`test/property-{http,policy,data}.test.js`); three documented defects are kept as `todo` tests. Spawn-based CLI tests (`test/cli-spawn.test.js`) drive the bin launcher and `build/index.js` over stdio in a sealed environment: handshake, core tool list, JSON-only stdout, SIGTERM / SIGINT exit 0, `doctor` / `drift` exit codes and both Node guards.
- **Test layout (E-6, L9-03):** every test script (`test`, `test:coverage`, `test:full`, `verify`) runs `node --test "test/**/*.test.js"`, so a suite in a sub-folder of `test/` is no longer silently skipped; the c8 thresholds (lines 94 / branches 82 / functions 97) and reporters have one definition in `.c8rc.json`. `test/test-layout.test.js` pins both, and `test/layout/sub-folder.test.js` is the sub-folder sentinel. Existing suites stay where they are.
- **Policy property tests (E-6):** `test/property-policy.test.js` adds fast-check properties for the H-11 table-glob evaluator: a glob entry matches exactly what a reference matcher accepts (`.` and `_` are literal), and the policy order is exact deny, exact allow, pattern deny, then the allow-list patterns.

### Docs

- **Nested `AGENTS.md` files (N-46b, O-20, ADR 0011).** An area source in `.github/agent-instructions/` marked `nested: true` also renders `<dir>/AGENTS.md` (`src/api`, `src/core`, `src/mcp`, `src/tools`, `test`, `scripts`, `extension`), for agents that read the nearest `AGENTS.md`. `npm run docs:instructions` removes a generated nested file whose source is gone but never a hand-written one; `extension/.vscodeignore` keeps it out of the `.vsix`.
- **`AGENTS.md` for coding agents (N-43, O-20, ADR 0011).** The root `AGENTS.md` holds the commands, layer rules, conventions and no-go areas (the `tools/list` budget, unapproved breaking changes, `scripts/pack-check.mjs`, generated files, secrets and harness files) for Codex, Copilot, Cursor and other agents. `.github/copilot-instructions.md` is now generated from it by `npm run docs:instructions` (checked in the gate), and `npm run docs:sync` keeps its tool and package counts; the stale Copilot text (97 tools, the project `.env` fallback, B1–B13) is gone.
- **SF-4: `SECURITY.md` MCP06 states which writes need a plan token.** The eight token-gated tools are listed, and the thirteen apply tools without a token are named as such; `docs/llms-full.txt` is regenerated.
- **Generated tool reference and agent documentation index (N-42, N-53).** `docs/tools/<package>.md` documents every tool from the registry: its purpose, each parameter (type, required, description), the declared output fields, and how it writes (read-only, plan and apply, `plan_token`). `docs/tools/README.md` indexes the packages and lists the error codes. `docs/llms-full.txt` bundles README, SECURITY and the tool reference in one Markdown file, and `docs/llms.txt` gains a generated "Tool reference" link section. A new `context7.json` points a documentation index such as Context7 at `docs/` and keeps `project/`, `test/` and the fixtures out; registering the project is an owner step (O-22). `npm run docs:sync` writes all of it and `--check` (part of `npm run check`) fails on drift, on a stale page and on an invalid `context7.json`. `docs/index.md` is the landing page as Markdown (about 67 KB instead of 174 KB of HTML), rendered from `docs/index.html` by `scripts/landing-md.mjs` and linked from the page with `<link rel="alternate" type="text/markdown">` and from `docs/llms.txt`. Guard: `test/tool-docs.test.js` (10 tests). The slimmer README is still open.
- **Project corpus index, archive and generated roadmap table (N-41).** `project/README.md` indexes the planning corpus (purpose, live / reference / historical, owned ID ranges, ID prefix clashes). The historical documents moved to `project/archive/`, with every inbound link updated. The ROADMAP-V3.md sequencing table is now generated from `project/roadmap.yaml` by `scripts/roadmap.mjs` (no new dependency); `npm run roadmap:sync` renders it, `--check` is part of `npm run check`, and `npm run roadmap:status` prints counts by pillar and status, open items by phase and open owner gates. Adopting also fixed a seven-cell delimiter row that kept GitHub from rendering the table. `test/roadmap.test.js` (13 tests).
- **Architecture decision records (N-44).** `project/adr/` holds one record per decided owner decision: a README (numbering `NNNN-kebab-title.md`, the statuses proposed / accepted / superseded, how records relate to the owner gates), a template, O-4 (breaking-change register approved), O-7 (`@servicenow/sdk` 4.12.2 exact as a dev dependency), S-12 (`acorn` as a runtime parser), and ARCH-10 / ARCH-14 imported from ARCHITECTURE.md and TODO.md with their deferred status. ROADMAP-V3.md gate lines and ARCHITECTURE.md link to the records; `project/README.md` indexes the folder. `test/adr.test.js` lints the folder: file names, Status / Date / gate fields, the README index, relative links, and that every accepted owner-gate ADR is linked from its ROADMAP-V3.md gate line.
- **Security policy v2 and the community standard files (D-9).** `SECURITY.md` now has a supported-versions table (2.x current, 3.x planned, < 2.0 unsupported), private reporting through a GitHub security advisory instead of a public issue, response targets (acknowledge within 3 business days, triage within 7 days, a critical / high fix within 30 days — targets, not an SLA), the scope (server and CLI, VS Code extension, plugin skills, release pipeline; not the ServiceNow platform or instance configuration), a threat-model summary and the security rails at a glance. New: `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1), `SUPPORT.md` (where to ask, `doctor --json` and `support-bundle`, what not to paste), `.github/CODEOWNERS`, issue forms (the bug form requires the `doctor --json` output, version, client and transport; blank issues are disabled and the chooser links the private security form), a pull request template with the gate checklist and `.github/release.yml` release-note categories. `CONTRIBUTING.md` links them all. None of these files ships in the npm package. `test/community-files.test.js` (+4 tests).
- **Install matrix and one-click links (D-4).** The README ("Install in your MCP client") and the docs site (a new "Install in your client" section) cover VS Code and VS Code Insiders (`vscode:mcp/install` deeplinks, vscode.dev redirect buttons, `code --add-mcp`), Claude Code (`claude mcp add` and the plugin), Claude Desktop, Cursor (`cursor://` deeplink and a web button), Windsurf, Cline, Zed, JetBrains AI Assistant, Gemini CLI and Codex CLI (`codex mcp add`, `config.toml`). Every link and snippet launches `npx -y servicenow-mcp-ai` and carries no credentials; secrets stay in the env file, `login` or `servicenow_set_credentials`. `scripts/install-links.mjs` generates the deeplinks from `package.json`, and `test/install-links.test.js` fails when the README or the site drift from them.

### Known limitations

Recorded by the owner on 2026-10-14 (ADR 0012); each needs an instance the project's PDI cannot provide.

- **Flow activation toggle (P-25):** the effect of `{active}` on a flow's trigger at runtime is unverified; on the PDI the Table API `PATCH sys_hub_flow` is ACL-refused even for admin + `flow_designer`.
- **Domain separation (N-12):** on a non-separated instance a missing and an empty `sys_domain` are both read as "no domain"; only a separated PDI was checked.
- **Elevated roles (N-20):** elevation is reported (`ELEVATION_REQUIRED`), never performed; the EL-0 spike waits for a PDI with full admin.
- **Registry rows:** `risk_assessment` and `uib_composite_definition` stay `verified: false` and degrade instead of failing on a 401 / 403.

## [2.0.1] - 2026-06-27

No functional changes — a republish release after the GitHub account rename.

### Changed

- **GitHub account rename `LeassTaTT` → `IvanBBaev`:** all repository references updated; the npm package republished and the MCP Registry listing re-registered under the `io.github.IvanBBaev` namespace.

### Documentation

- The GitHub Pages site redesigned (terminal-grade dark theme, live hero terminal, the CLI reference mirrored into the site); the README Quick start reworked into an install grid with client-config tabs; status badges added (CI, Codecov coverage, Snyk, tools count, last commit).

## [2.0.0] - 2026-06-22

v2.0 makes the breadth of v1 **safe and self-aware**: writes preview before they run, the server reports which capabilities it can actually achieve on your instance, and `code_health` audits the access-control layer. (v1 = breadth; v2 = trust + depth.)

### Breaking changes

- **Writes are plan-by-default.** `SN_WRITE_MODE=plan` is the new default. The write tools — record CRUD (`create`/`update`/`delete_record`), change (`create`/`update_change`), CMDB CI (`create`/`update_ci`), import-set insert, **email send**, **catalog order**, **attachment upload/delete**, **batch** (when it contains writes), and the **ATF run** tools — now return a non-mutating **before/after preview** instead of executing.
  - **Migration:** to apply a change, pass `apply: true` on the call, or set **`SN_WRITE_MODE=apply`** to restore the v1.x "execute immediately" behaviour globally. Automations that relied on a write running on the first call must do one of these.

### Added

- **DF-0 — capability preflight.** New always-on `servicenow_check_capabilities` tool and `servicenow://capabilities` resource: probe which admin-restricted `sys_*` tables the connected user can actually read, and report which capabilities (schema reads, script intelligence, ACL audit) are achievable — so the script/codecheck tools never promise a read they cannot make. `servicenow_table_logic` now degrades a 401/403 on one artefact type to an `unreadable` flag instead of failing the whole overview. (67 tools / 18 packages.)
- **DF-2 — plan-and-apply + local audit journal.** `SN_WRITE_MODE` (plan|apply) and a per-call `apply` argument gate every record-style write tool; each applied mutation is appended to a per-profile, append-only journal at `<SN_DOCS_DIR>/<profile>/write-journal.{jsonl,md}` — a client-side audit trail where there is no AI Control Tower.
- **DF-1 — ACL security scan.** `servicenow_code_health` now folds in a security dimension over the active ACLs (`sys_security_acl`): `eval`/side-effects/`gs.getUser` in ACL evaluation scripts, and roles-only ACLs (no condition and no script). The `sys_security_acl` read is gated behind DF-0 — when it is unreadable the scan reports the role needed rather than a silently empty all-clear.
- **DF-4 — where-used / impact graph.** New `servicenow_where_used` tool: find where a table, field or script is referenced across the instance's code — textual references in every script source, plus (for a table) the business rules, client scripts, UI policies/actions and ACLs attached to it. Read-only; optional Mermaid reference graph. The IDE-grade "find usages" the platform lacks.
- **DF-5 — client-side field redaction.** `SN_REDACT_FIELDS` masks named field values before query results are serialised for the model; `SN_REDACT_PII` also masks email/phone/national-id patterns inside any string value. Opt-in (off by default); the redaction count is reported in the result.
- **DF-6 — HTTP transport.** `SN_TRANSPORT=http` (port `SN_PORT`, default 3000) serves the MCP server over **Streamable HTTP** so the official ServiceNow MCP Client app and remote clients can consume it; the default stays `stdio`. The endpoint binds to **loopback (`127.0.0.1`) by default** (`SN_HTTP_HOST` to widen) and supports an optional **`SN_HTTP_TOKEN`** bearer guard (constant-time check); broader TLS/network hardening remains the operator's responsibility.
- **CSV export.** `servicenow_query_table` accepts `format: "csv"` for a spreadsheet-friendly export — a dependency-free RFC-4180 formatter that reuses the DF-5 redaction.
- **Claude Code plugin (DX-1).** A `.claude-plugin/` bundle (plugin + marketplace manifest) makes the server installable with `/plugin marketplace add IvanBBaev/servicenow-mcp-ai` then `/plugin install servicenow-mcp-ai` — zero-config, the server wired up.
- **DF-3 — CI drift gate.** `servicenow-mcp-ai drift <profileA> <profileB>` promotes `compare_instances` to a release artifact: it prints the Markdown drift report and exits non-zero when the instances differ, so a pipeline can block a deploy on configuration drift.
- **VS Code extension.** An `extension/` sub-package registers the MCP server with VS Code Copilot Chat (agent mode) on install (via `registerMcpServerDefinitionProvider` → `npx -y servicenow-mcp-ai`), so VS Code users get the tools with no manual `mcp.json`. Publishable to the VS Code Marketplace.

### Changed

- The out-of-the-box posture for writes is now **safe-by-default** (preview first) — see Breaking changes.

### Documentation

- **DX-3 hero demo.** A "Quick demo" section in the README and the docs site shows the three
  signature moves in one call each: find-usages (`servicenow_where_used`), what-runs-on-save
  (`servicenow_trace_table_event`) and dev-vs-prod drift (`servicenow-mcp-ai drift`). The matching
  screen-capture GIF is the only remaining DX-3 piece.
- **CLI reference.** The README now documents the `servicenow-mcp-ai` command-line surface — the
  server, `login` and `drift <a> <b>` invocations — with their parameters and exit codes.

## [1.1.2] - 2026-06-19

### Fixed

- MCP Registry namespace casing: `mcpName` and `server.json` `name` now use the exact GitHub login `io.github.IvanBBaev/servicenow-mcp-ai` (the registry namespace is case-sensitive), and the registry `description` is within the 100-character limit.

## [1.1.1] - 2026-06-19

### Added

- Listed on the official **MCP Registry**: a `server.json` manifest and the `mcpName` field in `package.json`, plus an automated registry publish (GitHub OIDC) wired into the release workflow so a `v*` tag publishes to npm and registers the server in one run.

### Fixed

- `package.json` `repository`/`homepage`/`bugs` URLs now point at the renamed `servicenow-mcp-ai` repository — npm provenance verification requires `repository.url` to match the GitHub repository the publish workflow runs in.

## [1.1.0] - 2026-06-19

### Added

- **Full ServiceNow authentication coverage.** Added OAuth 2.1 **Authorization Code + PKCE** via a one-time `servicenow-mcp-ai login` (loopback + browser, stores a refresh token), the OAuth **JWT bearer** grant (`SN_OAUTH_GRANT=jwt_bearer`, RS256, no password), **API Key** auth (`SN_API_KEY` → `x-sn-apikey`), a **static Bearer token** (`SN_BEARER_TOKEN`), **`none`** (certificate-only), and **mutual TLS** (`SN_TLS_CLIENT_CERT`/`_KEY`/`_CA`, via the optional `undici` package). `SN_AUTH` now accepts `basic`/`oauth`/`apikey`/`token`/`none` and is auto-detected from the keys present. The `AuthProvider` returns a header map (so non-`Authorization` schemes fit), with no new runtime dependency.
- **Phase 8 — flow testing + code checking** (3 new packages, none in the default `core` profile; 65 tools / 18 packages total):
  - `flows`: `servicenow_trace_table_event` (deterministic, ordered simulation of what a table operation runs — business rules by phase, flows, workflows, notifications — with a Mermaid flowchart), `servicenow_list_flows` / `servicenow_get_flow` (Flow Designer + legacy workflows), `servicenow_get_flow_runs` (execution evidence from `sys_flow_context`).
  - `codecheck`: `servicenow_lint_script` / `servicenow_lint_table` (a local deterministic rule set — hard-coded sys_ids/URLs, unbounded/in-loop GlideRecord queries, `eval`, `gs.sleep`, `setWorkflow(false)`, client-side GlideRecord, sync `getReference`, …) and `servicenow_code_health` (script inventory + lint summary → `code-health.md`).
  - `atf`: `servicenow_list_atf_tests` / `_suites`, `servicenow_run_atf_test` / `_suite`, `servicenow_get_atf_result` via the CI/CD API. The run tools execute on the instance — `atf` is opt-in and never in the default profile.
  - `SN_CODESEARCH=true` makes `servicenow_search_code` use the Code Search API (`sn_codesearch`) when available, with a fallback to the LIKE iteration (FT-7).
- `servicenow_compare_instances` and `servicenow_snapshot_instance` now warn (per section) when a read hit the `SN_MAX_RECORDS` cap, so a partial `sys_dictionary`/script/plugin/app read can no longer be presented as a complete diff or snapshot. Backed by a new `QueryResult.truncated` flag set when the cap is reached while `X-Total-Count` shows more rows.

### Changed

- Every ServiceNow API module now unwraps the `result` envelope through the shared `expectResult`/`expectResultArray`, so a malformed response is a uniform error instead of `undefined` data (`aggregate`, `cmdb`, `catalog`, `change`, `knowledge` joined `table`/`attachment`/`meta`/`email`).
- The Batch API now enforces the package policy axis: a sub-request to a denied package (`SN_PACKAGES_DENY`) is refused, and a write to a read-only package (`SN_PACKAGES_READONLY`) is blocked — a batch can no longer bypass the package policy that filters the normal tool list.

### Fixed

- `fetchAll` reads that stopped at the `SN_MAX_RECORDS` cap were silently treated as complete by the instance comparison/snapshot tools (under-reporting drift on large instances).

### Security

- The `.env` file is now written owner-only (`0600`) instead of the default `0644` — it holds a plaintext password.
- Without `SN_ALLOWED_HOSTS`, only `*.service-now.com` instances are contacted; a redirected or mistyped host can no longer silently receive Basic credentials. Set `SN_ALLOWED_HOSTS` to opt in a custom or sovereign-cloud domain.
- The Batch API rejects non-canonical sub-request paths (`//`, `/./`, `/../`, and their percent-encoded forms), closing a path-traversal bypass that let a batch reach a denied table or package surface that ServiceNow's dispatcher would normalize and route.

## [1.0.0] - 2026-06-15

First public release of `servicenow-mcp-ai` — a ServiceNow MCP server with full
API coverage, multi-instance profiles, a layered architecture and an enforced
quality gate. (Nothing was published before this; the earlier internal `1.0.0`
cut and the subsequent work are consolidated here.)

### Added

- Full ServiceNow API coverage beyond the Table API: Aggregate (Stats), Attachment, Import Set, Batch, Service Catalog, Change Management, Knowledge, CMDB Instance/Meta (IRE) — behind `SN_TOOL_PACKAGES` (profiles `core`/`all`).
- Script intelligence (read-only): list/read/search scripts, `servicenow_table_logic`; Mermaid generators (ER diagram, table flow); local self-documentation (`docs` package) + MCP resources and prompts.
- **Multi-instance profiles (Phase 7 core):** named connection profiles in .env (`SN_PROFILE_<NAME>_*`), per-profile policy (prod read-only / dev full rights), an optional `instance` argument on every tool (AsyncLocalStorage routing per call), `servicenow_list_instances` / `servicenow_use_instance`; the status payload lists the profiles.
- **Instance snapshot & comparison (`instance` package):** `servicenow_snapshot_instance` writes the instance's structural picture (tables, schema of selected tables, plugins, apps, script-automation stats) into `SN_DOCS_DIR/<profile>/` as Markdown + JSON; `servicenow_compare_instances` diffs two profiles — tables only in one, column property drift, scripts by SHA-256, plugin/app inventory — into a `_compare/<a>-vs-<b>.md` report with a structured summary. **53 tools in 15 packages.**
- **Per-profile MCP resources:** `servicenow://instances` (profile inventory, no passwords) and `servicenow://{profile}/schema/{table}`; the old URIs stay bound to the active profile.
- Per-package policy: `SN_PACKAGES_DENY` and `SN_PACKAGES_READONLY` — control over the plugin APIs that the table policy cannot see.
- Capability cache for plugin APIs: a namespace 404 is cached (fail-fast), keyed per instance; availability is visible as `pluginApis` in the status payload.
- ConfigStore: credentials are an atomic in-memory snapshot (the environment is only the initial source).
- The README tools table is generated from the live registrations (`npm run docs:readme`) and kept in sync by a test; a drift test ties the `package.json` description to the live tool/package counts.
- In-memory MCP smoke tests (SDK Client + InMemoryTransport) with a contract snapshot of the `core` profile and a manifest fixture; property-based tests (fast-check) for the env codecs.
- `servicenow_test_connection` — diagnoses whether the configuration actually works (`{ok, status, latencyMs}`); failures are structured.
- **`npm run check` — a single full gate:** build, lint, format check, coverage-gated tests (lines 85 / branches 72 / functions 60), `npm audit --omit=dev --audit-level=high`; `prepublishOnly` runs it, so a publish cannot bypass the gates.
- **CONTRIBUTING.md and SECURITY.md:** dev setup + gates + conventions; the security model, reporting channel and the accepted-risk decisions. README gains a table of contents and links to both.
- **Release process (S2-4):** `.github/workflows/publish.yml` publishes from CI on a `v*` tag with `--provenance`, a tag↔version guard and the full `npm run check` gate; an `npm run release:dry` script and a "Releasing" section in CONTRIBUTING. Needs an `NPM_TOKEN` repo secret.
- CI: a Node matrix with a macOS leg, a Windows job and a Node-12 launcher probe, plus a production-dependency audit step.
- OAuth: a 401 with a cached token recovers with a single re-authentication; stable fetchAll pagination (automatic ORDERBY); schema cache with TTL; concurrency semaphore; telemetry in status; Node 20+ guard (launcher + engines).
- Token diets by default: compact JSON output and no reference `link` URLs (opt-in to include them).
- MIT license; npm metadata (`license` / `author`).

### Fixed

- **Encoded-query injection in `servicenow_list_tables` and `servicenow_list_attachments`** (full-review DEV-1/DEV-2): the `filter`/`table`/`sysId` arguments were embedded in the encoded query without the `^`-separator guard that K-5 added to the script tools, so a `^` could inject extra clauses. The guard (`assertNoCaret`) is now shared in `api/shared.ts` and applied by every query builder.
- **`servicenow_table_logic` rejects a `^` in the table** (full-review pass 2, DEV-4): `tableLogic` embedded the table name raw into two of its sub-queries, so a `^` fired injected clauses before the validated sub-requests rejected. Guarded at the entry, the same way as the other query builders.
- **Plugin-API availability cache is now keyed per instance** (full-review ARCH-1): a namespace-404 cached for one profile's instance could fast-fail a concurrent call to the same API on a different instance for up to 5 minutes. Keyed like the schema cache; the status payload scopes to the active instance.
- **`index.md` regeneration is serialized** (full-review DEV-3): concurrent doc writes could interleave a directory walk with another write and drop entries from the rebuilt index.
- `servicenow_describe_table` only saw the table's own columns — it now walks the inheritance chain (for `incident` the fields defined on `task` are returned too); `superClass` is a real table name, not a label.
- Batch policy also covers `stats`/`import`/`cmdb` sub-requests; invalid base64 on upload is rejected before the network; download no longer pulls the bytes before the size check; the OAuth cache is cleared on credential changes.
- `String()` over a ServiceNow field at `display_value=all` no longer produces `"[object Object]"` (`snString`).

### Changed

- **The npm package is named `servicenow-mcp-ai`** (R-10: the unscoped `servicenow-mcp` is held by an unrelated maintainer). The name is coherent across `name`/`bin`, the bin launcher, the MCP server handshake name, the XDG config dir (`~/.config/servicenow-mcp-ai`), `.vscode/mcp.json`, CI and the README. The GitHub repository stays `IvanBBaev/servicenow-mcp`.
- `package.json` metadata: `repository`/`homepage`/`bugs` point at the GitHub repo; the description states 53 tools in 15 packages.
- The published tarball ships build + bin + README + LICENSE only — no `build/**/*.map` (the maps referenced the unshipped `../src`): 110 → 57 files, 92 → 62 kB packed.
- Tool input zod schemas are strict — an unknown argument in `tools/call` returns a validation error instead of being silently dropped.
- Layered architecture `core/` → `api/` → `mcp/` → `tools/` with ESLint-enforced boundaries; tools are a declarative manifest (ToolSpec) — a package is added/removed with one line; per-host semaphore and telemetry; an email package; elicitation confirmation for credential changes; MCP logging capability; `outputSchema` for the diagnostic tools.
- TypeScript: `noUncheckedIndexedAccess`; ESLint: type-checked rules + `no-floating-promises`.
- Errors are structured (`{ status, message, snDetail }`); retry with exponential backoff + `Retry-After`; SSRF guard; result size guard.

[3.1.1]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v3.1.0...v3.1.1
[3.1.0]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v2.0.1...v3.0.0
[2.0.1]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v1.1.2...v2.0.0
[1.1.2]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/IvanBBaev/servicenow-mcp-ai/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/IvanBBaev/servicenow-mcp-ai/releases/tag/v1.0.0
