# agent-tools manifest

The repo is the single source of truth. Compare these against an installed copy's
`SKILL.md` frontmatter (`version` / `updated`) to detect drift.

**Verify an install with `nexus_connect.mjs diagnose`** (offline, no token needed) rather than by
eye — since v1.4.2 `install.sh` writes an `.install-stamp.json` recording the pairing it laid down,
and `diagnose` compares it against what is actually on disk.

**Caveat — `SKILL.md` alone under-reports drift.** It tracks the engine, not the shipped MCP
server, so a copy whose `mcp/` was replaced on its own reads as merely "old" while actually being
an untested pairing. One real install carried `SKILL.md` 1.3.0 + `lib/` 1.3.0 alongside `mcp/`
0.8.0 — the two `mcp/` files had been overwritten in place, without `install.sh` (which always
moves the previous copy to `.bak.<ts>` and lays the tree down whole, so it cannot produce this
state). Check **both** versions and whether the pair is a row in this table.

| Component | Version | Updated | Path | Owner status |
|---|---|---|---|---|
| nexuscrm-connect (engine + MCP + canonical semantics) | 1.9.8 | 2026-08-03 | `nexuscrm-connect/` | here |
| nexuscrm-mcp (server, shipped inside nexuscrm-connect) | 0.11.3 | 2026-08-03 | `nexuscrm-connect/mcp/` | here |
| nexuscrm-connect-cloner (SFDC report/dashboard → HTML) | 1.0.0 | — | `nexuscrm-connect-cloner/` | **to be added by its owner** |

Canonical business-semantics doc (consumed by both):
`nexuscrm-connect/references/nexuscrm-semantics.md`.

## Changelog
- **2026-08-04 — v1.9.2 (the recorder stopped interpreting).** Seven review rounds landed on this
  change, and re-reading them together they were one shape, not seven problems: the recorder
  deciding a code at the moment it had the least context, and having to reach the same verdict as
  the other surface *independently*, so any two mappings could drift. Each round added a class, a
  classifier call site, or a parity guard — the exception list growing is the signal to change the
  model, not to keep paying the patch tax.
  The missing invariant: **record observations, derive interpretations.** The CLI now writes what it
  saw — HTTP status, the engine's own errorCode, the thrown error's TYPE, the redacted query — and
  `diagnostics_advice.mjs` turns those facts into an action at READ time. Its rules key on the facts
  rather than on a code, so the MCP server's `query_failed`/`describe_failed` and the CLI's
  code-less entry land on the same line by construction; a test drives four failure shapes through
  both and asserts identical advice.
  What that deleted: `CLI_CODES`, `codeForStatus`, `codeForThrow`, the `access_denied` and
  `transport_failed` recorder-side codes, and **every one of the 13 MCP server edits** — that file's
  diff is now two lines, both adding `surface: 'mcp'`. The cross-surface parity problem does not
  have a fix here; it has no place to occur.
  Two things the model makes honest rather than clever. A `whoami` with no token records
  `no_usable_token`, because `refreshToken()` returns null both when there is nothing to refresh and
  when the network is down — the entry says what is known and the advice covers both. And a throw
  records its type and stops: network-versus-defect cannot be told apart from the stored facts (the
  message is deliberately never persisted), so the advice offers both and says how to tell, instead
  of asserting one. Two review rounds had been spent asserting one.
  A guard pins the vocabulary: the only codes the CLI may write are `threw` and `no_usable_token`,
  both observations. Anything that is a verdict fails the build with a pointer to the rule table.
  Entries also stop being frozen: a better rule now improves records written months earlier.
  **The error classes did not name themselves**, so every OAuth failure recorded `detail: "Error"`
  and was advised as "probably the network" — and every unit test passed, because they fed the rule
  table a hand-written `detail: 'AuthError'` that the real path never produced. Fixed at the class
  (`this.name`), which also repairs every log line and crash report that identifies an error by
  name, with the extractor preferring `constructor.name` so the next subclass that forgets is still
  carried. The tests now run the real class through the real extractor through the same
  `summarizeResult` the recorder calls, and a smoke test drives it end to end by occupying the
  callback port — which rejects at `listen`, before any browser opens.
  **The pivot was half-done, and the half that was left froze what it claimed to unfreeze.** Rules
  moved to read time, but a record-time verdict stayed cached on the entry as `rule` and
  `adviseEntry` trusted it FIRST — so an MCP 401 recorded before `status` reached the classifier
  advised "check your field names" permanently, while the CLI's identical 401 said
  "re-authenticate". The stored value is a HINT now, not a verdict: it carries what the facts
  cannot (some rules discriminate on the server MESSAGE, which is deliberately never persisted),
  and `adviseEntry` takes whichever derivation is MORE SPECIFIC — array order is specificity.
  Facts-only entries improve as rules improve; message-discriminated ones keep their precision.
  **And the generic rule was reached from status alone, which was one step too far.** `status >= 400`
  is not a fact meaning "this was a query" — it means the server said no — so applied to every entry
  it swallowed `describe_failed`, `create_failed`, `opp_rollup_failed` and `endpoint_unavailable`,
  each of which knows what it was doing and has its own advice. The status path is now reachable
  only for a CLI observation carrying no code at all, which is the case it was added for.
  **The parity test was comparing payloads I wrote by hand**, which proves the table agrees with my
  imagination. Three divergences shipped past it, the last because the MCP throw path recorded a
  bare `threw` with no error type while the CLI recorded `AuthError` — same failure, one advised as
  a sign-in problem and the other as "probably the network". Both throw paths use `thrownFacts` now,
  and a guard fails the build if either hardcodes the code again.
  The matrix replaced the handcrafted pairs: entries are built through the same `summarizeResult`
  and `thrownFacts` the recorders call, the engine's message texts are copied from the rule
  predicates rather than paraphrased, and **every rule must be exercised across both surfaces or
  declared single-surface with a reason** — a new rule that is neither fails the build. It found two
  gaps in its own first draft (`nulls-ordering` and `aggregate-in-order-by` were silently falling
  through because I had invented their messages) and one false positive against its own comment.
  **One principle replaced two patches, and one missing property explained why neither was caught.**
  A fact may make an entry MORE specific and must never make it less: a 403 beside a generic read
  code says WHY the read failed, but the same 403 beside `insufficient_scope` or `create_failed`
  would replace a specific true answer with a generic wrong one. The generic catch-all takes an even
  narrower gate — it is a fallback, so it may only claim an entry carrying no code at all.
  The matrix missed this because REACHABILITY and PARITY are different properties and one exemption
  covered both: `insufficient-scope` was correctly declared single-surface, which exempted it from
  the parity comparison and silently exempted it from being checked at all while the rule above it
  swallowed every one of its entries. Every rule now needs a representative entry and must actually
  be reached by it; a rule nothing can reach is dead code wearing a test. It caught the over-broad
  gate in this very change on the first run.
  **Two shapes existed and mixing them was silent.** A tool payload names the code `error`; a stored
  entry names it `code`, and `record()` writes whatever it is handed. The MCP throw path spread a
  payload straight in, so every MCP throw stored no code and rendered as `raw:unknown` — the exact
  distinction I had written into a test comment one round earlier and then made in production code.
  Neither caller composes it now: `entryForThrow()` is the one definition and both use it, so there
  is no composition left to get wrong.
  The guard for it took two attempts, and the first is the more useful lesson: scanning `record({`
  call sites for a literal `error:` key passed the defect completely, because a SPREAD contains no
  such key. A static scan cannot see through `...thrownFacts(e)`, so the producers permitted to be
  spread are named instead — anything else fails with the reason.
- **2026-08-04 — v1.9.0 / MCP v0.11.1 (the CLI records too, and every entry names its surface).**
  Only the MCP server recorded, which made an entire documented population invisible: `AGENTS.md`
  routes ALL Codex and non-Claude CLI work through `scripts/nexus_connect.mjs`, and that path
  called `record()` nowhere. `diagnose` reported an empty activity section for them and we could
  not see a single failure they hit. Production could not size the gap either — both surfaces mint
  tokens through the same connected app and scope, so the OAuth tables cannot tell them apart.
  `query` / `auth` / `whoami` now record; `diagnose` and `selftest` deliberately do not (reading
  the log must not append to it). Entries carry `surface: 'cli' | 'mcp'`, and the report splits
  usage by (tool, surface) plus a per-surface rollup — folding them would answer "which tool
  fails" while hiding "which population is failing", which is the entire reason for the work.
  Pre-1.9.0 entries have no surface and read as `unknown`; they all came from the MCP server, but
  the report does not back-fill facts the data never carried.
  **One classifier, not two dialects**: the CLI builds a payload in the MCP tools' shape and hands
  it to `summarizeResult`, so redaction, error-code extraction and rule classification are the
  same code for both surfaces — asserted by a test that runs the same failure through both paths
  and compares the entries.
  **The first coverage guard for this was empty.** Extending the scanner to the CLI looked right
  and caught nothing: the CLI picks its code with a ternary, and the scanner's
  `error: '<literal>'` pattern matched none of it — so a CLI code with no advice passed. An
  expression can take any shape, so the codes moved into an exported `CLI_CODES` that the test
  IMPORTS, plus an assertion that the CLI contains no inline literal bypassing it. Three
  mutations, each proven to land before reading the result: unknown code, inline bypass, empty map.
  **The code must name the failure CLASS, because the class selects the advice**, and the first cut
  got that wrong twice: a `whoami` probe recorded a bare `query_failed` for a 401 (telling the
  reader to check field names when the token was dead), and the outer catch recorded every
  non-auth throw as `connect_failed` (telling the reader their browser sign-in failed when the
  network did). Both now classify by HTTP status and by error type (`AuthError` → `connect_failed`, otherwise `transport_failed`, a new code with
  advice that explicitly says re-authorizing will not help). The classifiers live in the library,
  not the script, so they can be imported and asserted rather than only reached by running a CLI.
  Verified end to end by occupying the callback port: a real `auth` failure records
  `{"tool":"connect","surface":"cli","code":"connect_failed"}`.
  Three further blocks, all the same root: **classify where the class is known, and share the
  classifier.** (1) `CLI_CODES` was referenced without being imported — a no-token `whoami`, the
  first thing a new user runs, crashed with a ReferenceError; the smoke test had a token and never
  reached that branch. (2) The CLI mapped 403 to `insufficient_scope` while the MCP read path
  recorded `query_failed`, so identical failures got different advice by client; parity is now by
  construction — the MCP read path calls the same `codeForStatus`, and a test fails if it hardcodes
  a code again. (3) A failed OAuth token exchange threw a bare `Error`, so it read as
  `transport_failed` and the report said the token was fine — about a token that was never issued.
  It is thrown typed now, at the throw site rather than guessed at the catch.
  **The 403 mapping itself was then blocked twice with OPPOSITE demands** — first for using
  `insufficient_scope` (diverging from the MCP path), then for folding into `query_failed` (whose
  advice is "check field and object names"). Both were right, because neither code fits. Production
  says what a read 403 actually is:
  `403 [{"errorCode":"FORBIDDEN","message":"permission denied: cannot read …"}]` — object
  permission on the USER, not OAuth scope and not a malformed query. So it became its own class,
  `access_denied`, whose advice deliberately names none of the three neighbouring wrong turns
  (check the field names / that needs a write scope / re-authorize). Both surfaces read the same
  table, so they cannot diverge. The HTTP status is persisted now too — dropping it was the other
  half of that finding, and it is what makes two entries with the same code distinguishable.
  **Parity then had to mean EVERY site, not one function**: `codeForStatus` was wired into
  `readQuery` only, while four other MCP read paths still hardcoded their code — so the guard
  reported parity while a 403 on `pipeline_by_stage` or `get_account` still gave query/rollup
  advice. All five now classify through it, and the guard scans every `.status !== 200` in the
  server rather than one function it was told about. `codeForStatus` takes an optional fallback so
  `get_account` keeps `opp_rollup_failed` for the ordinary case — the account WAS found, the rollup
  query is what failed — while 401 and 403 are never the caller's to name.
- **2026-08-04 — v1.8.2 (the bundle stopped contradicting itself about how to install).**
  v1.8.1's manifest said "no installer here, get the CLI artifact" while `SKILL.md` in the SAME
  bundle said "run its `install.sh`" — and the sweep found `README.md` and `AGENTS.md` saying it
  too, only one of which review had named. A reader following the packaged instructions ends up
  with no local MCP server, which is the whole point of installing.
  All four now name the artifact that actually carries the installer
  (`nexuscrm-connect-cli.tgz`), and a pipeline assertion enforces it at the FILE level on the built
  bundle: any packaged `.md` mentioning `install.sh` must also name that artifact. File-level and
  anchored on two literal tokens on purpose — every earlier attempt to police a claim like this
  keyed on phrasing and missed the next paraphrase. Mutation-tested: restoring the old wording in
  `SKILL.md` fails the build test by name.
- **2026-08-04 — v1.8.1 (documentation caught up with v1.8.0, and the version had to move with it).**
  Four docstrings plus the plugin description still said the plugin manifest is stamped with the
  environment's hosted `/mcp` URL. It no longer carries a server to stamp; `--mcp-url`'s only
  consumer is the local `mcp/mcp.json` snippet.
  The description was the damaging one: it said "run its install.sh" about a bundle that has never
  contained one (0 in the plugin zip, 1 in the CLI tarball) — the same "looks self-sufficient"
  claim v1.8.0 existed to remove. A pipeline assertion now fails if an installer ever appears in
  the bundle, so the sentence cannot go stale in silence.
  **Why a version bump for a docs change**: `plugin.json` is PACKAGED into both downloads and the
  catalog maps a version to immutable `release-artifact/nexuscrm-connect/<version>/…` keys. Editing
  it under an already-published version means the next deploy either rejects the conflicting
  publish or keeps serving the old bytes. "Docs only" is not a reason to skip the bump when the doc
  ships inside the artifact.
  Also recorded, because a review comment claims otherwise: a root-level `SKILL.md` with no
  `skills/` directory is a supported single-skill plugin layout (Claude Code >= v2.1.142, name from
  frontmatter). Adding a `skills` key to "fix" it would REPLACE the default scan for a marketplace
  source resolving to the root.
- **2026-08-04 — v1.8.0 (the plugin ships skills only; it no longer declares an MCP server).**
  `.claude-plugin/plugin.json` declared the hosted `/mcp` endpoint. That endpoint is L0 platform
  code and serves FOUR schema-agnostic tools — `list_objects`, `describe_object`, `query_records`,
  `query_my_records` — with no business semantics, no `get_semantics` and no diagnostics, by design
  (L0 must not carry L1 schema knowledge). Every business rule lives in the local stdio server that
  `install.sh` registers, so anyone onboarding via the plugin got the semantics-free half.
  Production said it had never once been used: `_System_OAuthDynamicClientRegistration` **0 rows**,
  and every consent and refresh token belonging to `public-bi-connected-app` / `external.records.read`
  (58 tokens, 17 users) — the stdio path. All the entry did was register a second server named
  `nexuscrm` and leave a standing authorization prompt.
  **Bundling the stdio server instead was considered and rejected**: `EXCLUDED_NAMES` keeps
  `node_modules` out of both artifacts, and the plugin format has no install step, so a bundled
  server would fail at startup with `ERR_MODULE_NOT_FOUND` for every user. Shipping the modules
  would break the byte-identical build guarantee; a `SessionStart` install hook reintroduces the
  "did the install work" failure class the install stamp exists to detect. That is a dependency
  delivery problem, not a manifest problem, and it is not solved here.
  PKCE and OAuth are untouched, verified rather than reasoned: the auth engine has zero references
  to any plugin artifact, the loopback redirect is registered on the Connected App server-side
  (`RedirectUris ["http://127.0.0.1:1717/callback"]`, `RequirePkce=1`), and a full fresh browser
  sign-in was run against this exact tree — new token, HTTP 200, and the dynamic-registration count
  stayed at 0, proving the flow never touches the removed path.
  `--mcp-url` keeps its name and shape for deploy callers but now has exactly one consumer, the
  local `mcp/mcp.json` snippet, so per-environment isolation is unchanged.
- **2026-08-04 — v1.7.0 / MCP v0.11.0 (field traps reach the agent, and are bound to the doc).**
  Measured over 123 real `run_soql` statements: rules carried in a TOOL DESCRIPTION were obeyed
  (`Amount` 1 violation, `CloseDate` 0), while the rule that lived only in
  `references/nexuscrm-semantics.md` — never trust `IsClosed`/`IsWon` — was violated repeatedly.
  Nothing errored; it was documented on a surface the agent never reads. `get_semantics`, the tool
  built to carry business rules, was called 2 times out of 172.
  So `Opportunity.IsClosed`/`IsWon` now enter the dictionary as a new `unreliable` status, reaching
  agents through `describe_object` (per-field, scales with the dictionary) and one clause in
  `run_soql` (always-on, because run_soql outnumbers describe 123:31).
  **The wording also had to change, not just the placement.** These fields were largely empty when
  the old caveat was written; on prod 2026-08-04 `IsClosed` is populated on 3652 of 3734 rows and
  merely DISAGREES with `StageName` — `Closed Won` 872/872 agree, `Closed Lost` 1770/1818, but
  `Launched` only 13/214. A present-and-wrong value is worse than a null: `WHERE IsClosed = true`
  returns cleanly while dropping ~249 closed deals, nearly all realized ARR. Upstream
  `nexuscrm-app-packs#322` and `nexuscrm-pingcap-pack#3716` are both still open.
  New `lib/semantics_coverage.test.mjs` binds the document and the dictionary in BOTH directions so
  neither can drift alone, and `--dump-semantics` exposes the DERIVED map for it to read — that
  distinction immediately caught a real gap (see the commit).
- **2026-08-03 — v1.6.1 / MCP v0.10.1 (the `_System_*` allowance is per-call-site, not global).**
  v1.6.0 relaxed `OBJECT_RE` so `describe_object` could see `_System_*`. `assertObject` is SHARED,
  so that also admitted those names to `create_record`, which POSTs to `/sobjects/<object>` — a read
  fix that widened the WRITE surface. Reads now opt in explicitly (`allowSystem: true`); everything
  else, including create, keeps the original character class and is denied by default so a future
  call site cannot inherit write access to system objects. The rejection message now says system
  objects are readable but not writable here, so the fixed thing is not re-filed as a bug.
  Injection rejection verified in BOTH modes, `_System_X WHERE 1=1` included.
  The first cut of this fix tagged only `describe_object` and left `query_records` and
  `query_field_history` rejecting `_System_*` — a read regression, in a commit whose own message
  claimed otherwise. The rule is not a list of call sites, it is *every read allows system, the one
  write does not*, so the guard now enumerates call sites and fails on either direction: a read
  without the opt-in, or a write that gains one.
  **This shipped to main for roughly an hour**: the fix existed but auto-merge took the approved
  head before it was pushed. Same mechanic as #4796 and #4808; the difference is that this one had
  a security consequence rather than a documentation one. Standing rule from here: **once a PR is
  approved its branch is frozen — any further fix opens a new PR.**
- **2026-08-03 — v1.6.0 / MCP v0.10.0 (`describe_object` accepts `_System_*`; NULLS gets advice).**
  `OBJECT_RE` excluded a leading underscore, so `describe_object` refused every `_System_*` name
  while `run_soql` had always been able to query their rows — and the error said "Use the SObject
  API name", i.e. it read as *you typed it wrong*. The platform describes them fine (verified live:
  `_System_ValidationRule` returns 13 fields); the restriction was an accident of a character class,
  not a limit. Callers were pushed into guessing column names for the one family of objects where
  guessing is hardest, which is exactly the failure the advice table then documented as normal.
  Both advice rules built on that false premise are corrected and the claim is retired. Injection
  protection is unchanged — `Account WHERE 1=1`, `a;b` and empty names are still rejected.
  Separately, `ORDER BY … NULLS FIRST/LAST` was listed as unsupported in the tool description but
  had no advice rule, so a real run put it in the generic bucket; it now has its own. Null ordering
  verified live rather than assumed: this engine sorts nulls as the smallest value, so `DESC` already
  places them last and `ASC` first — half the callers need only drop the clause. The other half get
  a two-partition recipe, **not** `!= null`, which deletes the null rows rather than moving them.
  The `describe_failed` advice likewise stops asserting a cause: every non-OK response from the
  describe endpoint maps to that one code, so 403 and 5xx would have been read as "you typed the
  name wrong".
  **Dropped from this change after checking: the write-tool "unconditional registration" item.**
  The premise was that agents are not told; they are, since 2026-06-23 — titles say "(needs write
  scope)" and descriptions say what happens with a read-only token. The observed call came *after*
  that warning existed. What remains is a design preference with one instance in 163 calls, which
  belongs with the other frequency questions, not with the verified defects.
- **2026-08-03 — v1.5.3 (scope and retention of the activity summary, stated accurately).** Two
  corrections to prose that shipped in v1.5.2. **The activity summary covers MCP tool calls only** —
  the recorder hangs off the MCP server's registerTool wrapper and `nexus_connect.mjs` never calls
  it, verified by running a real CLI query and watching the log stay empty. This matters most in
  `AGENTS.md`, where the CLI *is* the documented flow, so the Codex audience was pointed at a
  troubleshooting tool whose activity half can never have data for them. **And "successful queries
  are not stored at all" was wrong in a privacy disclosure**: a successful call stores tool name,
  row count and duration — only the query TEXT is omitted. Now stated per outcome. Worth recording
  how the second one happened: I verified "a successful call stores no soql", which is true, then
  wrote a broader sentence I had not tested. Verifying a narrower proposition than the one you
  publish is, from the reader's side, the same as not verifying.
- **2026-08-03 — v1.5.2 (`diagnose` is documented where people will find it).** The command shipped
  in v1.5.0 and appeared in exactly one of the four user-facing documents — not `SKILL.md` (what
  Claude reads), not `AGENTS.md` (what Codex reads), not the top-level `README.md`. Three releases,
  and nothing noticed; a command nobody is told about is a command nobody runs. Now in all three,
  each with the detail that actually trips people: **the readable report goes to stderr** so
  `diagnose > file` writes an empty file, and `--json` goes to stdout. A test now asserts every CLI
  subcommand appears in all three audience documents, so the next command cannot ship invisible.
  Every factual claim in the new prose was checked against the implementation rather than written
  from memory — stream, log path, 0600, the opt-out variable, the absence of any upload path, and
  that successful calls store no query text.
- **2026-08-03 — v1.5.1 / MCP v0.9.1 (SOQL capability claims re-verified against prod).** The
  advice table and the `run_soql` description both carried engine facts collected on 2026-07-04..08
  and never re-checked. Re-ran every claim against prod today: **GROUP BY and ORDER BY on
  relationship fields now WORK** (`GROUP BY CreatedBy.Name` returns groups), so the rule advising
  people to group by the Id and resolve names in a second query was telling them to do twice the
  work — wrong advice is worse than none, and it shipped one release earlier. Rule removed, and its
  absence pinned by a test. Two real limitations that were NOT documented are now both listed and
  classified: GROUP BY on more than one column (`unexpected token in query: ,`) and function
  expressions in ORDER BY (`ORDER BY COUNT(Id)` → `function expressions are not supported in ORDER
  BY`). Still true and left as-is: `NULLS FIRST/LAST`, `LAST_N_DAYS:N`. Not re-tested, so not
  restated as verified: the semi-join >1000 cap. Upstream gaps reported on NexusCRM#186.
  **On keeping it honest:** the mechanism already existed and I had not used it —
  `references/nexuscrm-semantics.md` is a capability matrix carrying per-row verification dates
  (`fixed NX#3035, live 2026-06-23`). The drift happened because the tool description and the advice
  table each restated those facts instead of deferring to it. All rows touched here now carry
  `verified live 2026-08-03`, and the `run_soql` description says outright that the matrix wins.
  A replay harness against a live instance is still the stronger answer for detecting when the
  ENGINE moves, and is still the owner's call. What is now mechanical is the other half: three
  consecutive review rounds all said "you corrected some occurrences of this claim and left others",
  which is one missing relation rather than three defects. `lib/retired_claims.test.mjs` holds each
  retired claim with its date and reason, greps every shipped text surface for the affirmative
  phrasing, and separately asserts the surface list still covers everything the package ships — so a
  new file cannot become the one place that restates it. **Scope is the INSTALLER's, not one
  package's** — a retired claim survived in `nexuscrm-connect-cloner`, a sibling skill the same
  `install.sh` lays down, because both the scan and its self-check were scoped to
  `nexuscrm-connect/`. Components are now discovered the way install.sh discovers them.
  It is a regression guard over known wordings, not a semantic detector: matching by ingredients
  ("GROUP BY" + a dotted field + a negation) was tried and abandoned after two rounds of false
  positives on corrected text that legitimately contains all three. Each pattern is instead proven
  against the wording that really shipped.
- **2026-08-03 — v1.5.0 / MCP v0.9.0 (`diagnose` answers why calls fail, not just whether the copy is intact).**
  The MCP server now records every tool call locally and `diagnose` renders it with an action per
  failure class, so "why does this keep failing" is answered from evidence rather than memory.
  **Local only, by construction — there is no upload path.** Failures store the tool, the error
  code, and a FINGERPRINTED query (every literal replaced with `?`); successes store only the tool
  name, row count and duration, never query text, because successes are the bulk of both the volume
  and the business detail while the questions they answer need only counts. Field and object names
  survive fingerprinting — without them "unknown column" is not actionable — which is a stated
  trade-off, not an oversight. Opt out with `NEXUS_NO_DIAGNOSTICS=1`; wipe by deleting
  `~/.nexuscrm-connect/diagnostics.jsonl`. Instrumentation is ONE seam wrapping `registerTool`, so
  a new tool cannot be added un-instrumented, and it swallows its own errors: a diagnostics feature
  that can take down the tool it observes is worse than none. The log is capped and trimmed, and the
  reader skips torn or foreign lines, because two servers (editor session and desktop session)
  append to the same file and a process killed mid-write leaves a partial line. The advice table is
  built from failures actually observed in transcripts, and the two MALFORMED_QUERY shapes
  (multi-column GROUP BY vs a relationship field) are kept apart deliberately — same code,
  different fix, and collapsing them makes both lines useless.
- **2026-08-03 — v1.4.3 (install.sh keeps replaced copies OUT of the skills dir).** A backup at
  `$SKILLS_DIR/<component>.bak.<ts>` still contains `SKILL.md`, so the agent host loads it as a
  second skill with the same name and a byte-identical description — two indistinguishable
  candidates for every invocation. Seen on a real machine immediately after an ordinary install,
  and the `~/.claude/skills-archive` already sitting there shows someone had been moving them out
  by hand. Backups now go to `"<SKILLS_DIR>-archive"` (override with `SKILLS_ARCHIVE_DIR`), and any
  backup a previous version left inside the skills dir is swept out on the next run — fixing only
  new installs would leave every already-affected machine broken. If the archive cannot be written
  the install falls back to backing up in place, loudly: losing the previous copy would be far
  worse than one duplicate skill. **Version bumped even though no installed component file
  changed:** `install.sh` ships at the root of `nexuscrm-connect-cli.tgz`, whose object-store key
  embeds this package version and is immutable, so shipping an installer change under an unchanged
  version means the release either fails to publish or keeps serving the old file. The artifact is
  the unit that has to be versioned, not the skill directory. Collision-safe (`mv` into an existing directory would otherwise nest one backup inside
  another and hide the older one); verified under bash 3.2, which is what macOS ships.
- **2026-08-03 — v1.4.2 (install stamp + `diagnose`).** `install.sh` now writes
  `.install-stamp.json` into each component it installs, recording the engine and MCP-server
  versions it laid down together **plus a sha256 per installed file** — a version number is only a
  proxy for content, and it goes blind exactly where this feature is aimed (overwrite
  `mcp/server.mjs`, leave `mcp/package.json` alone, and a version-only check sees two equal numbers
  and says "fine"). Stamping runs last, after the `mcp.json` path rewrite and `npm install`, so it
  describes the tree as it finally rests. New offline `nexus_connect.mjs diagnose` compares that
  stamp against what is on disk and reports `ok` / `drifted` / `unverifiable`, naming the changed,
  missing, or added files. On a machine with no sha256 tool the installer stamps `"files": null`
  and `diagnose` says outright that contents were not verified rather than implying a full pass.
  `ok` is reachable only by passing a positive gate: the stamp must validate against the format
  install.sh writes (`stamp_version`, required keys present — values may be null) before any
  comparison result is trusted, so a truncated or hand-edited stamp lands on `unverifiable` by
  construction rather than by having been individually anticipated. `diagnose` exits non-zero on
  the two anomalous outcomes — `drifted`, and a stamp that is present but invalid — and zero when
  a stamp is simply absent, which is the expected reading on every install predating stamping.
  Error paths fail CLOSED: only ENOENT counts as an absent stamp (permission denied, EISDIR and the
  like mean it is there and unreadable — an anomaly), a file that cannot be hashed at install time
  degrades the whole component to version-only rather than being dropped from a stamp that still
  claims full hashes, and a file that cannot be read at verify time is reported as `unreadable`
  rather than silently contributing nothing. This targets the MIXED
  install specifically — not "old", which is a tested combination merely missing fixes, but a
  component replaced in place so the pairing was never tested by anyone. No network, no upstream
  lookup, no table of which versions "should" pair.
  Known limitation, stated rather than papered over: a stamp only describes installs made after
  this ships, so this cannot retroactively audit machines that are already mixed.
- **2026-08-03 — v1.4.1 / MCP v0.8.1 (needs_auth points at the in-band recovery).** A `needs_auth`
  hint that named only the CLI (`node scripts/nexus_connect.mjs auth`) read, to an agent, as "you
  can fix this by running a shell command" — but the non-interactive paths that raise it are
  reached only from the MCP server, whose caller has no terminal and no human waiting at one.
  Observed consequences in real transcripts: one agent backgrounded the interactive flow, hit
  `Port 1717 is already in use`, retried, then wrote a 40-iteration poll loop waiting for a
  sign-in nobody had asked a person to complete; another re-sent the same tool with three
  different argument sets against an error that had nothing to do with its arguments. Both hints
  in `lib/nexus_core.mjs` now name the in-band `connect` tool first and keep the CLI as the human
  fallback, and `mcp/server.mjs` funnels all eight non-connect `needs_auth` returns through one
  `needsAuth()` seam that adds a machine-readable `recover` block (`tool`, `precondition`,
  `if_browser_available`, `otherwise`, `do_not`). `connect` is offered as a CONDITIONAL recovery,
  not an unconditional one: it signs in over a loopback redirect, so it cannot work in a headless
  container or against the hosted HTTP server, and `recover` states that precondition plus an
  explicit else-branch instead of guessing the environment. The `connect` tool's own failure path
  deliberately does NOT use that seam — pointing a failed connect back at `connect` would loop.
  Covered by 6 offline selftest checks and 3 `node:test` cases exercising the real throw sites;
  every check was mutation-tested (reverting any part turns them red). **Also corrects the stale 2026-07-21 note below:** the
  server-side refresh phase has shipped — production sessions minted by the `refresh_token` grant
  have been observed, so silent refresh is a live path, not a stub. (Whether a given client holds a
  refresh token still depends on the authorized scope; the built-in default omits `offline_access`.)
- **2026-07-23 — v1.4.0 (distributable packaging + release download pipeline).** Added the
  package manifest (`nexuscrm-connect/package.json`) and the Claude Code plugin manifest
  (`.claude-plugin/plugin.json`, hosted `type: "http"` server at
  `https://nexuscrm-new-prod.pingcap.com/mcp` — URL only, no OAuth client settings: the platform
  supports RFC 7591 dynamic client registration). `mcp/package.json` now declares its direct
  `zod` dependency. New `agent-tools/build_artifacts.py` deterministically builds
  `nexuscrm-connect-cli.tgz` (install.sh + package) and `nexuscrm-connect-plugin.dxt`
  (plugin zip); every release deploy builds + publishes both into the backend object store at
  immutable versioned keys (`release-artifact/nexuscrm-connect/<version>/…`) so the platform's
  `GET /downloads/{cli|plugin}/latest` serves them.
- **2026-07-22 — MCP v0.8.0 (Opportunity money-field deprecation, owner D1).** describe_object marks `Amount` deprecated (flagged, agents must not call) and drops `ACV_Amount_USD__c` from the default field list (hidden — agents default to Net New ARR); the policy is code-fixed in `productChain.deprecatedFields` defaults (semantics override files cannot change it; edits go through a reviewed PR).
- **2026-07-21 — MCP v0.7.0 (silent token refresh + in-chat `connect`).** `lib/nexus_core.mjs` now
  persists a `refresh_token` and refreshes it silently on the non-interactive path and once on a 401
  (single-flight + atomic token writes; idempotent-only retry so create POSTs are never replayed).
  New `connect` MCP tool opens the OAuth+PKCE browser sign-in in-chat. See PR #4224.
  ~~NOTE: the upstream token endpoint does not issue/accept refresh tokens yet
  (unsupported_grant_type) — this client is a forward-compatible no-op until the server-side
  refresh phase ships.~~ **SUPERSEDED 2026-08-03: the server-side phase has shipped and the
  `refresh_token` grant is accepted in production — see the 2026-08-03 entry. Treat silent refresh
  as a working path, not a no-op.**
- **2026-07-19 — MCP v0.6.0 (Field History discovery and reads).** Added opt-in history-object
  discovery and a record-scoped `query_field_history` tool with Account, Opportunity, and custom-object naming.
- **2026-06-23 — v1.3.0 (SOQL fixes NX#3035/#3036 landed).** Upstream fixed multiple-aggregates-per-SELECT
  (NX#3035) and parent-field WHERE for non-polymorphic lookups (NX#3036), both verified live on prod.
  `pipeline_by_stage` now uses ONE multi-aggregate GROUP BY query (two-query fallback kept); `run_soql`
  + `query_records` descriptions and canonical semantics doc §3 updated to stop steering away from these.
  Semi-join >1000 (NX#3037) remains capped (fix merged upstream but NOT yet on prod). MCP 0.4.0.
- **2026-06-23 — v1.2.0 (externalized semantics + version visibility).** Business semantics (口径:
  fiscal-year start month, current stage sequence, closed/realized stages, stage aliases) are no
  longer hardcoded — they resolve from built-in defaults overridable by a JSON file
  (`NEXUS_SEMANTICS_FILE`, default `~/.nexuscrm-connect/semantics.json`). IT/MDM can push that file
  to change 口径 fleet-wide with NO binary rebuild/re-sign. Added `get_semantics` (active 口径 +
  source + version) for drift visibility; the MCP handshake + startup log now report the real
  version. Field names (ARR_Amount_USD__c/Launch_Date__c) intentionally NOT externalized (stable
  schema). See `docs/distribution-and-upgrade.md`. MCP server 0.3.0.
- **2026-06-23 — v1.1.0 (stage ordering).** Stage order is now canonical data in the MCP, not
  per-report. Added `list_stages` (funnel-ordered stages + flags) and `pipeline_by_stage`
  (funnel-ordered count + Net New ARR per stage, one call) so "opportunities by stage" charts render
  in funnel order (Prospecting→…→Launched) instead of amount order. `query_opportunities` description
  now steers stage breakdowns to funnel order. MCP server 0.2.0.
- **2026-06-23 — initial centralization (v1.0.0).** Moved the previously offline-distributed
  skills into the repo as the single source. Includes: MCP server (11 tools incl. generic
  query_records / describe_object / list_objects / query_activities / create_record); verified
  business doctrine (Net New ARR = ARR_Amount_USD__c, time = Launch_Date, FY starts Apr 1,
  open/closed rules, stage normalization); verified SOQL dialect limits; cloner reconciled to the
  canonical semantics with the fidelity + doctrine-banner rule.
