Skip to content

Releases: Koniverse/Senti-MCP

v2.8.1

Choose a tag to compare

@github-actions github-actions released this 26 Aug 04:10
96eeb4b

[2.8.1] — 2026-08-25 — the API Keys dashboard URL, and what the default base URL actually pairs with

The startup error for a missing SENTI_API_KEY sent people to
https://stage.sentitrade.xyz/account/api-keys. It now sends them to
https://app.sentitrade.xyz/account/api-keys — the host the Senti landing pages and
docs already use — and README.md and SETUP.md follow. Functionally the
two dashboards are interchangeable for this purpose, which is the point of the
second half of this entry.

What makes the swap safe is that the two hosts are the same front door: the served
production bundles of app.sentitrade.xyz and stage.sentitrade.xyz both carry
REACT_APP_API_URL: "https://be-dev.sentitrade.xyz" (checked 2026-08-25), so a key
from either dashboard is issued by the same backend. The environment-pairing
warnings in README.md and SETUP.md now say that, replacing the older
"keys are currently issued from the staging dashboard" phrasing, which read as
though the dashboard host picked the environment.

The warnings otherwise stand as they were: keys are environment-bound, and a 401
on a valid-looking key is more often a mismatched SENTI_API_BASE_URL than a bad
key. Whether the default https://api.sentitrade.xyz accepts a dashboard-issued key
is still not established — from outside the deployment it answers every
unauthenticated probe byte-identically to be-dev, which is consistent with one
origin behind two hostnames but does not prove it. The default is unchanged;
settling it is a deployment call, not this server's.

No tool, schema or response changed. 673 unit tests pass.

v2.8.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 11:25

[2.8.0] — 2026-08-21 — compile_draft, and EPIC-8's close

The tool that closes the loop. compile_draft runs the static-safety scan and the MQL5
compiler over a draft and every indicator attached to it, and returns the verdict, the
diagnostics and the compiler log (US-8.4).
It is the seventh and last tool of EPIC-8, and like the other six it
is registered only when SENTI_ENABLE_AUTHORING_WRITE is set.

A failed build is not an error. The API returns 200 with ok: false and diagnostics; the
tool returns a success result carrying them. Marking it isError would tell a model its
call malfunctioned, and a model's correct response to that is to retry — against a globally
serial compile slot, for a build that will fail again identically.

Nothing retries, and the abort message says why that matters here. The compile slot is one
per account and the compile server is serial across all accounts, so a 409 means someone
else's compile is running and a 503 carries a wait this server reports rather than sleeps on.
The 15-second client timeout is not raised — but aborting the fetch does not cancel the
compile
, so the account's slot stays busy and the next call would be a 409. The message says
exactly that and sends the model to get_draft for lastCompileStatus.

It is the only tool in this repo that parses diagnostics strictly, because
POST /drafts/{draftId}/compile is the only route in the whole document that declares their
shape. get_draft and list_drafts parse theirs loosely and will keep doing so — see below.

The write smoke test, and the two things it settled

npm run test:smoke with SENTI_SMOKE_WRITES=1 now creates a real draft, attaches an
indicator, compiles it and deletes it, cleaning up in a finally. It exists to discharge two
gaps EPIC-7 closed with, both of which needed a write:

1. The GET's diagnostics match the compile response's — and the parse stays loose anyway.
No live draft had ever carried a non-empty lastCompileDiagnostics, so get_draft's
loose-parse/tight-render pair had never met real data. Measured on a deliberately broken draft:
the two arrays are identical, same six keys, same values. The render path is confirmed. The
parse does not tighten, because the observation is about the service and the parse is a bet
on the contract — which still declares the array untyped (CONTEXT D44).

2. An attachment existed for the first time. The smoke account has held zero attachments
since 2.2.0, so every attachment branch in list_draft_attachments was test-covered and none
was live-covered. The write smoke creates one.

Two undocumented behaviours found on the way, recorded in CONTEXT D44: a
draft's name derives its .mq5 filename with non-alphanumerics replaced by underscores — so
diagnostics[].file will not string-match the draft name — and the compiler log carries the
compile host's absolute Windows path, C:\MT5\compile_jobs\<draftId>\…, CRLF-terminated.

EPIC-8 closes

Seven of the Authoring tag's eight write operations now have a tool. What the close does
not claim is in EPIC-8 §What this close does not claim:
register is unimplemented, and the two delete tools are unusable on a host without MCP
elicitation support.

v2.7.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 11:23

[2.7.0] — 2026-08-21 — the three attachment writes

add_draft_attachment, update_draft_attachment and delete_draft_attachment complete the
indicator sub-resource (US-8.3), whose read
half US-7.4 shipped in 2.4.0. All
three are registered only when SENTI_ENABLE_AUTHORING_WRITE is set.

Filenames collide case-insensitively, and the message says why. MyInd.mq5 and
myind.mq5 are the same file to this platform, because the compile host writes every
attachment into one flat Windows directory. A 409 from add_draft_attachment reports that
rather than a bare "already exists", so a model does not try the same name in another case.

The filename is immutable, so update_draft_attachment does not accept one. An EA embeds
an indicator by name — #resource "MyInd.ex5" — and a rename would orphan every reference and
turn a working draft into a static-safety violation. To rename: delete, re-add, and update the
EA source. Accepting a filename the API would ignore is worse than not accepting one, so the
input schema is draftId, attachmentId, sourceCode and nothing else.

Attaching does not wire up, and deleting does not unwire. add_draft_attachment names the
exact #resource "<stem>.ex5" and iCustom(_Symbol, _Period, "::<stem>.ex5", …) lines the EA
still needs, derived from the filename you gave it. delete_draft_attachment says the opposite
thing: the EA still references a file that is gone, and the next compile_draft fails on it
unless update_draft removes those lines first. A draft that compiles a file it never
references reads as a success otherwise, and one that references a file it no longer has reads
as a compiler problem.

delete_draft_attachment is the second and last tool in EPIC-8
that pauses for a human confirmation, on the same reasoning as delete_draft
(CONTEXT D42).

A 404 on either attachment-id tool carries a cause the draft 404 does not: the
attachment may exist and belong to a different draft. That is an easy mistake to make with
two ids in one path, and a message that only said "not found" would send the reader to check
the wrong one.

v2.6.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 11:20

[2.6.0] — 2026-08-21 — update_draft and delete_draft

The two draft writes that can destroy work, and the first tool in this server that pauses for
a human (US-8.2). Both are registered
only when SENTI_ENABLE_AUTHORING_WRITE is set.

update_draft is a FULL REPLACE, and it is annotated destructiveHint: true despite its
name.
The API declares no partial-update verb: name and sourceCode are both always
written, so a model that sends only the function it changed deletes the rest of the file. The
warning is in the tool's description, where a model reads it before choosing the argument,
which is the only place a warning about a destructive argument can still help.

It reports the bytes it wrote, not the bytes it replaced. A before/after delta would need
the pre-write size, and the PUT response carries only the new draft — a hidden GET would
double the latency of every edit and race any concurrent writer. When a previous compile no
longer matches the new source, the text says so and names compile_draft.

delete_draft asks first, and a "no" is not an error. It is one of two tools in
EPIC-8 that pause for an explicit human confirmation; the other
five do not. That is a deliberate line, not an oversight: update_draft fires on every save
in an edit loop, and a confirmation a user sees fifty times in a session is one they stop
reading — a rubber-stamp laundered into the appearance of consent is worse than no prompt
(CONTEXT D42). The two deletes are the only operations in this epic that no
other tool in it can undo.

A declined confirmation returns a success carrying { id: null, deleted: false } and a
note saying no request was sent. isError: true would tell a model something malfunctioned
and invite a retry, and a user saying no is neither.

Two mechanics worth knowing if you build on the seam. It identifies the round by an opaque
requestState it mints, not by the answer: acceptedContent() reports a decline and a first
entry identically — both undefined — so branching on the answer alone re-asks on every
decline and spins until the client's round cap. And a forged requestState cannot skip the
confirmation, because only accepted content reaches the request.

A host without elicitation support cannot use delete_draft. That is accepted rather than
worked around: a silent fallback to deleting without confirmation would make the guardrail a
function of the client, which is the one property a guardrail must not have. Every other tool
in this release works normally on such a host.

core/tool.ts now imports two runtime values from @modelcontextprotocol/server
(inputRequired, acceptedContent), so it joins src/server.ts and src/index.ts as the
third file that does. AGENTS.md said server.ts was the only one; corrected.

v2.5.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 11:18

[2.5.0] — 2026-08-21 — create_draft, and the write path opens

The first tool in this server that changes something. create_draft calls
POST /api/v1/drafts under a seventh scope, authoring:write, and is the first of
seven tools EPIC-8 opens over the Authoring tag's write
operations (US-8.1).

It is not registered unless you ask for it. SENTI_ENABLE_AUTHORING_WRITE=1 (or
true) registers the authoring write tools; anything else — including 0, false, no
and off — leaves them unregistered, and a host that never sets it sees the same fourteen
read tools 2.4.0 shipped. The flag is authoring-only: it enables no trading write at
any setting, because closing a position is a different surface with a flag of its own that
does not exist yet (EPIC-3). Keeping them separate is the point —
enabling an agent to edit MQL5 must never be the same act as enabling it to close a
position.

The response does not echo your source back. POST /drafts returns the complete draft,
up to 192 KiB of source plus five attachments at 64 KiB each — content the model supplied in
the same call. Returning it would bill it a third and fourth time, through content and
structuredContent both (CONTEXT D34). The tool returns the new id, the
byte count written, the compile state and an attachment summary, and notes points at
get_draft for a read-back (CONTEXT D39). A note records loss, so creating
an empty draft writes none.

The Idempotency-Key is a fresh UUID per call, not one derived from the body. The
design specified a content-derived key so that an identical repeat would replay the original
201 instead of colliding with a 409, and left the retention window as the open question
that would decide it. Measured against be-dev on 2026-08-21: an idempotency record
outlives a delete.
Create → delete → byte-identical create replayed the original response
and returned a draftId that no longer existed. Since create, delete, create again is what
iterating on a draft looks like — and delete-then-recreate is the API's own prescribed way to
rename an attachment — the derived key was replaced with randomUUID(), which still gives the
protection the header is actually for: one request delivered twice by the transport creates
one draft (CONTEXT D43, revising D41).

Nothing retries anything. The write path adds 413, 422, 502, 503 and 504, and
none of them is retried. Retry-After is read and quoted in the message, never slept on: a
tool call that waits holds the host's turn open for an interval the server chose. A 503
without Retry-After is reported as meaning a retry cannot help, which is what the API
documents (CONTEXT D40).

A 403 no longer assumes it is about a scope. On every read it was; on create_draft it
also means the draft cap is full, and against that cause the old wording — "the key is
missing that scope, not that the account is off limits"
— sends the reader to mint a key
they already hold. The new forbiddenMeans option lets each endpoint say what its 403
means, and the read tools keep the old wording byte for byte by passing nothing.

registerWriteTool is a second registrar, not a flag. registerReadTool is unchanged
and still pins readOnlyHint: true as a constant, so no call to it can produce a write
whatever its arguments (CONTEXT D38). A test asserts it directly.

Also corrected in this release: three files in this repo stated that
POST /drafts/{draftId}/register puts an EA into a real trading account. It does not — it
creates a permanently private EaDefinition, and deploying is
POST /accounts/{accountId}/strategies under the separate strategies:write scope. The
claim had been inferred from the operation's name rather than read from its description
(CONTEXT D36). register is still out of scope, for a different reason: it is
the only write in the tag that creates a resource the tag cannot then delete.

What this release does not do: compile, update, delete or attach anything. Those are
2.6.0 through 2.8.0. create_draft writes a draft and stops; nothing is compiled until
compile_draft exists.

v2.4.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 08:09

[2.4.0] — 2026-08-20 — list_draft_attachments: the fourteenth tool, and EPIC-7's close

list_draft_attachments reads GET /api/v1/drafts/{draftId}/attachments under
authoring:read and returns the indicator source files a draft's EA embeds via
#resource — the source US-7.2 deliberately
leaves out of get_draft. It is the fourth and last tool over the Authoring tag
(US-7.4), and this release closes
EPIC-7: all 14 of the Senti Quant Public API's GET
operations now have a tool.

A budget, not a truncation. At maxAttachmentsPerDraft: 5 × maxAttachmentBytes: 65536 the endpoint's ceiling is 320 KiB ≈ 82,000 tokens; counting both content and
structuredContent, which both reach the model, the tool's own worst case is ≈ 33,000
tokens (CONTEXT D34). With filename supplied, the tool returns at most one
attachment whole, whatever its size, and cuts nothing else — filenames are not guaranteed
unique within a draft, so if more than one attachment shares the requested name, only the
first is returned and notes says how many were skipped, rather than returning every
match and bypassing the budget entirely. The note states how many were skipped, one
fewer than the number that share the name. A filtered read also says it is filtered —
Filtered by filename "X" — 1 of N attachment(s) on this draft — so a host that surfaces
content alone cannot read the filtered count as the draft's whole attachment set.
With filename omitted, attachments are returned whole, in the API's own order, while
the running total including the one just added stays within 65,536 bytes — the first
attachment is always returned whole regardless of its own size, and once one attachment
is cut, every attachment after it is cut too — and the note says exactly that, rather
than claiming each cut file exceeded the budget: a 1-byte attachment that follows a breach
is cut without exceeding anything, and a model told otherwise will not re-read it.
Checking the total after inclusion rather than before is what caps the response at 64 KiB
instead of admitting 127 KiB. A cut
attachment keeps its metadata and reports sourceCode: null; source is never partial —
an empty file is '' with sourceBytes: 0, and the two are never confused. A filename
that matches nothing returns an empty result and names, in the text, every filename that
does exist, which is also how to discover what a cut left out.

The API has no way to request one attachment by id. GET /drafts/{draftId}/attachments/{attachmentId} does not exist, though PUT and DELETE
on that exact path do — raised with the API as an asymmetry. The filename filter is a
client-side workaround: the set is always fetched whole, so it bounds the model's
context, not the wire.

Untested live. The smoke account holds 4 drafts and 0 attachments in all 4 — measured
again this release, unchanged since 2.2.0 and 2.3.0 — so the budget, the cut rule and
the filename filter are proven only against synthetic sizes in
list-draft-attachments.test.ts. EPIC-7's close states this rather than letting a green
suite imply otherwise; see
EPIC-7 §What this close does not claim.

Added

  • list_draft_attachments — a byte-budgeted attachment read
    (src/tools/authoring/list-draft-attachments.ts). ATTACHMENT_BUDGET_BYTES,
    AttachmentsOutputSchema, parseAttachments, shapeAttachments, formatAttachments.
    Registered after get_draft in src/server.ts, so the authoring/ group reads
    conventions → list → read → attachments.

Notes

  • EPIC-7 closes done. All 14 of the API's GET operations have a tool. Its close
    names four branches that never ran against the live service — every attachment code
    path across all three draft tools, this tool's budget and filename filter, get_draft's
    DiagnosticSchema render path, and DRAFT_NOT_FOUND's 404 — and what would discharge
    each. The open question on whether a GET's lastCompileDiagnostics element really
    matches the compile response's diagnostic shape is still open.
  • A second fix wave, C1-C9, corrected what the four authoring tools say about their own
    payloads
    — seven sentences that were not true of the payload in hand, and the missing
    per-tool coverage in src/server.test.ts that let them through. See
    CONTEXT D35. No version moves: none of 2.1.0-2.4.0 has been published.

v2.3.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 08:07

[2.3.0] — 2026-08-20 — list_drafts: the thirteenth tool, and the largest payload in the API cut four ways

list_drafts reads GET /api/v1/drafts under authoring:read and lists every MQL5 draft
the API key owns, most recently updated first, with each draft's compile status, size,
attachment count and registered-EA id — the tool that answers "what am I working on" or
"which of my drafts are broken". It is the third tool over the Authoring tag
(US-7.3), and its DraftSummarySchema is
derived from 2.2.0's DraftSchema by .omit() and .extend() rather than hand-written,
so a field the API adds upstream cannot silently bypass the cut.

The largest payload the API can produce. The route takes no query parameters at all —
no ?include=, no ?fields=, no pagination, no filter — and returns every draft's full
sourceCode, every attachment's full sourceCode, and every draft's lastCompileLog
whole. At the US-7.1
ceilings that arithmetic is 20 × (192 KiB source + 5 × 64 KiB attachments + 16 KiB log) = 10.3 MiB ≈ 2.7 million tokens. Measured live on 2026-08-20 against an account holding 4
drafts and 0 attachments: 19,853 B → 1,898 B, 90.4% removed — matching the design
spec's 2026-08-19 measurement exactly. See CONTEXT D32.

Four cuts, one note. sourceCode (→ sourceBytes), attachments[].sourceCode (→
sourceBytes), lastCompileLog (and logTruncated, which describes a field that is no
longer there), and lastCompileDiagnostics (→ diagnosticsCount). All four lose
information, so all four are reported — but as one sentence naming the draft and attachment
counts, the KiB removed, and both get_draft and list_draft_attachments as the way to
read what was cut, rather than four notes a reader would learn to skim past. notes stays
empty on an empty collection, so its presence never implies a cut occurred.

No parameter turns the cut off. The ceiling is two orders of magnitude past
get_performance_breakdowns', the API's next-largest payload, so an opt-out here would be a
footgun with a documented safety catch rather than a real escape hatch — get_draft already
is one, and it returns one draft rather than twenty.

Added

  • list_drafts — every draft, shaped (src/tools/authoring/list-drafts.ts).
    DraftSummarySchema, DraftsOutputSchema, parseDrafts, shapeDrafts, formatDrafts.
    Registered between get_authoring_conventions and get_draft in src/server.ts, so the
    file reads conventions → list → read. Text output marks a draft SUCCESS-and-up-to-date as
    "ready to register" and renders a null lastCompileStatus as "never compiled" rather
    than printing null.

Notes

  • The API's Authoring GET surface is now three of four tooled.
    list_draft_attachments remains, tracked in EPIC-7.
  • src/smoke.test.ts now parses GET /api/v1/drafts through parseDrafts rather than
    casting it, so the live smoke path exercises the schema instead of bypassing it.

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 08:06

[2.2.0] — 2026-08-19 — get_draft: the twelfth tool, and the schemas the last two EPIC-7 tools import

get_draft reads GET /api/v1/drafts/{draftId} under authoring:read and returns one MQL5
draft's full source code, its compiler log, its diagnostics, and whether the last compile still
matches the current source — the tool that answers "why did this fail to compile" or "show me
the code". It is the second tool over the Authoring tag
(US-7.2), and the module that owns DraftSchema and
AttachmentSchema: list_drafts and list_draft_attachments import both from here rather than
redeclaring them, so one shape drifting only breaks in one place.

One cut. At the US-7.1
ceilings, one draft is worth up to 192 KiB of EA source + 5 × 64 KiB of attachment source + 16
KiB of compile log — roughly 135,000 tokens, more than most context windows. Attachment
sourceCode is replaced with sourceBytes; everything else, including the EA's own source,
returns whole — this is the tool a caller reaches for because they want the source, so
truncating it here would leave no tool able to return it.

Read live on 2026-08-19 against the account holding drafts: the key set matched the design
spec's twelve exactly, four drafts existed, none FAILED and none carrying an attachment, so
no real lastCompileDiagnostics element was available to check against DiagnosticSchema — a
shape transcribed from POST /drafts/{draftId}/compile, which types that field where the two
GET paths do not. The parse stays z.array(z.unknown()) regardless: parseOrThrow is
all-or-nothing, so transcribing the compile response's shape onto an untyped GET field would
take both draft tools down the day they diverge. See
US-7.2 §Implementation notes for the observed key
set.

Added

  • get_draft — one MQL5 draft, full fidelity (src/tools/authoring/get-draft.ts).
    DraftSchema, AttachmentSchema, DiagnosticSchema, AttachmentSummarySchema,
    DraftOutputSchema, byteLength, parseDraft, shapeDraft, formatDraft. Diagnostics
    render opportunistically — DiagnosticSchema.safeParse per element, a readable
    file:line:column line on a match, the raw element otherwise — so a shape mismatch costs a
    less readable line, never a failed call. The text also composes the API's documented
    register-readiness question (lastCompileStatus === 'SUCCESS' && compiledUpToDate) as a
    derived sentence rather than a stored field, since both operands are already in the output.
    • Attachment source is cut, not truncated. attachments[].sourceCode never reaches
      either channel; sourceBytes replaces it, and a notes entry — repeated in the text —
      names list_draft_attachments and the draftId that undoes the cut.
    • No parameter reopens the cut, and the EA's own source is never truncated. Half an
      MQL5 file reads as a complete one to a model that did not write it.

Notes

  • The API's Authoring GET surface is now two of four tooled. list_drafts and
    list_draft_attachments remain, both importing this release's schemas rather than
    redeclaring them (EPIC-7).

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 08:03

[2.1.0] — 2026-08-19 — get_authoring_conventions: the eleventh tool, and a new tag opens

The Senti Quant Public API grew a new Authoring tag, and with it from 17 operations to
29
— 14 of them now GET, not 10. get_authoring_conventions is the first tool over that
tag: it reads GET /api/v1/authoring/conventions under a new authoring:read scope and
publishes the platform's own MQL5 authoring contract — the hard-safety constraints, the
trading-safety requirements, the static analyzer's forbidden-construct list, and the five
limits ceilings the API enforces on drafts, attachments and registered EAs. Read live on
2026-08-19: maxDrafts 20, maxAttachmentsPerDraft 5, maxAttachmentBytes 65536 (64 KiB),
maxSourceBytes 196608 (192 KiB), maxRegisteredEas 10. Those numbers matter beyond this
release — they are what size the cuts EPIC-7's remaining three
tools make against a payload the API can otherwise grow past a context window
(US-7.1).

This release also lands the substrate the rest of EPIC-7 stands on:
draftPath/DRAFT_NOT_FOUND in src/core/client.ts, extracted from accountPath over a
shared private segmentPath guard rather than copied
(CONTEXT D33) — no path-traversal guard now has a second copy to drift out of
sync with the first.

Added

  • get_authoring_conventions — the platform's MQL5 authoring contract, as data
    (src/tools/authoring/conventions.ts). ConventionsOutputSchema, parseConventions,
    formatConventions. No path parameter, no 409client.get is called with a scope
    only, no notFoundMeans or conflictMeans. The tool description tells the model to call
    this before generating any MQL5 source: code that violates these rules is rejected by
    a static scan before it reaches the compiler, and compile slots are globally serial, so
    discovering a rule by failing a compile is expensive and still fails.
    • Every rule is rendered whole, never summarized. These are instructions a model must
      follow, not records it might skim — formatConventions numbers every constraint and
      requirement and reproduces every forbiddenConstructs[].pattern verbatim, escapes
      intact.
    • The patterns are reported, not evaluated. pattern values are regular expressions
      the API's own static analyzer applies; this tool does not compile or run them, and says
      so — the document types them as bare strings and never names the regex dialect, so
      building a RegExp from an undeclared dialect would be a crash or a silent mismatch.
    • No cuts and no notes field. The whole response is roughly 2 KB and static per
      deploy, so unlike get_performance_breakdowns and get_equity_timeseries there is
      nothing here to shape.
  • draftPath and DRAFT_NOT_FOUND in src/core/client.ts — the substrate the other
    three EPIC-7 tools build their paths on. A private segmentPath(prefix, segments, hint)
    now owns PATH_SEGMENT, encodeURIComponent and the traversal-rejection loop;
    accountPath is re-expressed over it with its signature and error message unchanged, and
    draftPath is a second prefix over the same guard rather than a second copy of it
    (CONTEXT D33).

Notes

  • The API's operation count is now 29, not 17 — a new Authoring tag adds 12 operations,
    four of them GET. get_authoring_conventions is the first of those four;
    EPIC-7 tracks the remaining three (get_draft, list_drafts,
    list_draft_attachments).

v2.0.1

Choose a tag to compare

@github-actions github-actions released this 14 Aug 11:43

[2.0.1] — 2026-08-14 — the floor gets a gate, and the toolchain catches up

Nothing that runs has changed. All 17 dist/**/*.js files are byte-identical to
what 2.0.0 published — verified by checksumming every file in both tarballs, not by
reading the diff. No tool was added, removed or altered; no payload, argument or
structuredContent shape moved. A consumer on 2.0.0 gains no behaviour by upgrading.

What this release actually carries is the work of
EPIC-5's last three stories, none of which cut a version of its
own: a gate that stops the Node floor drifting, a dependency bot, and a compiler decision.
Against the published 2.0.0 tarball exactly four things differ — three .js.map files
(source-position attribution, from the new compiler), two reworded sentences in
README.md, and the devDependencies block of package.json, which npm does not install
for consumers.

It is a patch because docs/RELEASE.md §Step 1 measures semver against the
tool surface rather than the diff size, and the tool surface is untouched.

Added

  • release:check now guards the Node floor. It compares package.json
    engines.node — the canonical value — against every floor claim in README.md and
    docs/SETUP.md, and fails when they disagree or when an artifact states no floor at
    all
    . The floor was stated in three places and compared by nothing, which is the
    LESSONS 4 shape that let package-lock.json sit eight releases behind its
    version string (US-5.2).

    A floor claim is defined narrowly and the narrowness is the contract: a semver
    immediately preceded by >= or , on a line mentioning Node. The operator is what
    separates the floor from the other Node versions in the same prose — AbortSignal.any's
    20.3.0 is written "landed in 20.3.0", never ">= 20.3.0", so it is excluded without
    being special-cased. The practical consequence when the floor next moves: prose about
    an old floor must not use the operator form — "the old 20.6.0 floor", not "the old
    >= 20.6.0 floor". Two README sentences were rephrased accordingly; no claim changed
    meaning. CI pins in .github/workflows/ are deliberately not checked, because they bind
    nobody outside CI and publish differs from the floor on purpose
    (LESSONS 7).

    Ten tests, watched go red before the implementation existed, plus a mutation of the real
    docs/SETUP.mdgrep-confirmed on disk before the red result was believed
    (LESSONS 1) — proving the gate names the exact file, line and both values.

  • .github/dependabot.yml — weekly npm updates with minor and patch grouped into one
    PR, so currency stops depending on someone remembering to run npm outdated
    (US-5.3). Two ignore
    entries, each carrying the reason it exists and the condition under which it should be
    removed: @types/node majors (see below) and typescript majors (TypeScript 7 is a native
    compiler rewrite and gets its own decision in
    US-5.4, rather than riding into main
    inside a grouped refresh).

    Its header states plainly what a green Dependabot PR does not prove: no workflow runs
    on a pull request here — release.yml fires on a v* tag and nothing else — so those PRs
    arrive with no typecheck, no test run and no tarball verification, and the file lists the
    commands to run locally instead. Enabling the bot without saying so would ship a false
    signal.

Changed

  • Development toolchain brought current: vitest 3.2.7 → 4.1.10 and tsx 4.23.6 →
    4.23.12 (in-range, lockfile only — package.json's ^4.19.0 did not move). The suite is
    unchanged at 20 files / 439 tests, 1 skipped, measured against a baseline taken
    immediately before the bump: a count that dropped would have meant a silently-skipped
    suite rather than a clean upgrade. vitest.config.ts's src/-anchored include
    (CONTEXT D13) was re-proven under the major by planting a decoy test under
    .claude/worktrees/ and confirming it is still not collected — a major version is exactly
    when a default changes underneath you.

  • @types/node deliberately stays on the Node floor's major and is now a written rule
    rather than a pin that looks like neglect (CONTEXT D28). Types newer than the
    floor let tsc accept calls to APIs the supported runtime does not have: the build stays
    green and the failure lands on the user, at run time — the same shape
    CONTEXT D5 raised the floor to fix. npm outdated will keep reporting
    @types/node as behind, and that output is now expected.

  • typescript 5.9.3 → 7.0.2 — the native compiler port
    (CONTEXT D29,
    US-5.4). tsc is this repo's build,
    not only its typechecker — bin points into dist/ and files publishes it — so the
    emit was compared before the decision rather than after: all 17 dist/**/*.js files are
    byte-identical
    to the 5.9.3 build, verified by checksumming every file in both trees.

    Three .js.map files differ — core/client, core/errors and server, which are
    exactly the three sources using a parameter default or a parameter property — and only in
    which source positions the generated defaults and field assignments are attributed to. The
    JavaScript at those sites is character-for-character the same. .js.map ships, so the
    tarball does change in debug metadata; it stays at 54 entries and nothing that runs is
    affected.

    Typecheck is clean on both tsconfigs, and each was mutation-tested to confirm it still
    catches errors — a new compiler exiting 0 proves it ran, not that it checked. The reason
    to move is measured rather than assumed: typecheck ~1428 ms → ~503 ms, full build
    ~1412 ms → ~393 ms (~3.6×). Without a number like that the decision would have been
    to stay, since a compiler emitting the same JavaScript buys nothing on its own.

  • .github/dependabot.yml drops the typescript majors ignore added one story earlier,
    which is what its own comment instructed the deciding commit to do.

  • docs/RELEASE.md §Step 2 and §Step 5 describe the floor as a second set of
    files that must move together, and the release:check failure table gains three rows.
    docs/README.md's pre-commit checklist gains a floor item — a floor change is
    not a version bump, so nothing else in that list would have caught one.