CLAUDE.md · diff
git:20260714.0582550 to git:20260802.e35bec8
81 added, 12 removed. Audit A to A.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What we're trying to build
**toolnexus** — a small, **vendor-neutral** library that gives *any* LLM the dynamic
capabilities [opencode](https://github.com/anomalyco/opencode) has, ported **byte-identically
- across six languages** (`js/`, `python/`, `golang/`, `java/`, `csharp/`, `elixir/`):
+ across seven languages** (`js/`, `python/`, `golang/`, `java/`, `csharp/`, `elixir/`, `clojure/`):
1. **Dynamic MCP servers** — read an `mcp.json`, connect to every server (local stdio +
remote streamable-HTTP), expose each server tool as a uniform `Tool`.
2. **Dynamic agent skills** — glob a `skills/` folder (`**/SKILL.md`) and expose one `skill`
tool that loads a skill's instructions + resources on demand (progressive disclosure).
The insight: MCP tools, agent skills, your own functions (`defineTool`), remote HTTP endpoints, the
built-in shell/file tools, and remote **A2A agents** are *the same thing* to an LLM — a named,
described, schema'd callable. toolnexus unifies all these tool sources behind one `Tool`
interface, emits the schema in **OpenAI / Anthropic / Gemini** formats, and ships a **unified
client** with a built-in tool-calling loop (system prompt, skills injection, parallel + chained
tool calls, hooks, streaming, retries, conversation memory, observability metrics).
It is also an **inbound** endpoint: `toolkit.serve(addr, …)` exposes the toolkit to the outside
world — today as a remote **A2A agent** (Agent Card + JSON-RPC over the client loop; `SPEC.md §7B`).
The whole point of the ports is **parity**: the same `examples/` fixtures must produce the
same behavior in every language. That is the product. Protecting it is the prime directive
below.
## Repo layout
| Path | What |
|------|------|
| `SPEC.md` | **The shared contract.** The one-page conformance spec (§0) every port must satisfy, plus reference detail (§1–§9). Source of truth for behavior. |
| `js/` | TypeScript port — `toolnexus` (npm). MCP SDK: `@modelcontextprotocol/sdk` (same as opencode). |
| `python/` | Python port — `toolnexus` (PyPI), Python ≥ 3.11. MCP SDK: `mcp`. |
| `golang/` | Go port — `github.com/muthuishere/toolnexus/golang`, Go 1.23. MCP SDK: `mark3labs/mcp-go`. Also ships the `toolnexus` CLI under `cmd/`. |
| `java/` | Java port — `io.github.muthuishere:toolnexus`, Java 21. MCP SDK: official `io.modelcontextprotocol.sdk:mcp`. |
| `csharp/` | C# port — `Toolnexus` (NuGet), .NET. MCP SDK: `ModelContextProtocol`. |
| `elixir/` | Elixir port — `toolnexus` (Hex), Elixir ≥ 1.16/OTP 26. MCP client: **in-house** (`Toolnexus.Mcp.*` — no mature Elixir SDK exists); coverage gate ≥ 95%. |
| `examples/` | **Shared cross-language fixtures** — `mcp.json` + `skills/hello-world/`. Every port runs against these; outputs must match. |
| `openspec/` | **Spec-driven change workflow (OpenSpec).** `changes/` = active proposals (`proposal.md` + spec deltas + `tasks.md`), `specs/` = canonical capability specs, `changes/archive/` = shipped changes. Run the `openspec` CLI from the repo root. |
| `.github/workflows/ci.yml` | Runs all six suites (JS/Python/Go/Java/C#/Elixir), hermetically (no network, no live LLM). |
- ## The prime directive: spec-driven, six-language parity
+ ## The prime directive: spec-driven, seven-language parity
- `SPEC.md` is the contract; the six ports are implementations of it. Two rules govern every change:
+ `SPEC.md` is the contract; the seven ports are implementations of it. Two rules govern every change:
1. **Behavior is defined in the spec before it is written in code.** `SPEC.md §0` is the
conformance contract — if a change alters observable behavior (tool naming, config parsing,
the `skill` tool's byte-exact output, adapter mapping, the client loop), it changes `SPEC.md`
*first*, then the code. Any change of real substance goes through an **OpenSpec change** first
(below) — the proposal's spec deltas are where intended behavior is pinned before code.
- 2. **A behavior change lands in all six ports, or it is not done.** Do not ship a capability
+ 2. **A behavior change lands in all seven ports, or it is not done.** Do not ship a capability
in `js/` and leave `python/`/`golang/`/`java/`/`csharp/` behind without explicitly saying so
and tracking it in an in-progress spec. The ports are meant to be substitutable; silent drift
is the one bug this repo exists to prevent.
> "Correct" is defined by §0: run a port against the shared `examples/` (same `mcp.json` + skill)
> and its outputs must match the others. When in doubt, make the spec authoritative and bring
> code to it — not the reverse.
## Required Workflow — OpenSpec
**Every change of substance runs through [OpenSpec](https://github.com/Fission-AI/OpenSpec).**
Slash commands (Claude Code): `/opsx:propose`, `/opsx:apply`, `/opsx:archive` (plus
`/opsx:explore`, `/opsx:sync`). CLI: `openspec` — use `npx @fission-ai/openspec@latest …` until
it is installed globally.
1. **Propose** — `/opsx:propose "<idea>"` scaffolds a change at `openspec/changes/<name>/`:
`proposal.md` (why + what), `design.md` (how), `tasks.md` (checklist), and **spec deltas** at
`specs/<capability>/spec.md`. This is where intended behavior is pinned, *before* code. If the
change moves the cross-language contract, edit `SPEC.md` in the same change.
2. **Validate** — `openspec validate <name>` (catches malformed deltas / missing scenarios).
3. **Apply across the ports** — `/opsx:apply`. Implement the tasks in **every affected language**
and tick them off. Each behavior change carries a **per-language parity checklist** (js /
python / golang / java / csharp) in `tasks.md`; if a pass covers only a subset, the rest stay
as unchecked tasks — never let parity drift silently.
4. **Verify** — run the narrowest useful suite per touched port (see Commands) and check parity
against the shared `examples/`. Call out anything unverified.
5. **Open the PR** with the change folder + code in one diff, so reviewers see intent and
implementation together.
6. **Archive on merge** — once the PR **merges**, run `/opsx:archive` (or
`openspec archive <name>`). This folds the spec deltas into `openspec/specs/` and moves the
change to `openspec/changes/archive/`. **Archive only after merge** — never when the PR is
merely opened, so `openspec/specs/` always describes shipped behavior, not work under review.
Default to a change for: a new tool source, a change to MCP/skill discovery or naming, the
`skill` tool output, adapter (OpenAI/Anthropic/Gemini) mapping, the client loop (hooks,
streaming, resilience, memory), the Go CLI surface, publishing, or a new language port. Keep
ceremony minimal for typo-only fixes; tightly scoped bug fixes should still update the capability
spec that owns the behavior.
### How the two spec layers relate
- **`SPEC.md`** — the hand-maintained **cross-language conformance contract**, the byte-level
porting obligation (`§0` is the one-page contract). Source of truth for "are the four ports
identical."
- **`openspec/specs/`** — the **capability specs** that accrue from archived changes: the
growing, validated record of *what each capability does*, in requirement/scenario form.
- A behavior change updates **both** — the spec delta in the OpenSpec change, and the relevant
`SPEC.md` section when the cross-language contract itself moves.
### Spec delta format (the part that bites)
- Requirement: `### Requirement: <name>`, SHALL/MUST wording.
- Scenario: **exactly four** hashes — `#### Scenario: <name>` — with `- **WHEN** …` / `- **THEN** …`.
Three hashes or bullets fail silently at archive time.
- Every requirement needs ≥ 1 scenario. A `MODIFIED` requirement must paste the **full** updated
block, not a fragment.
## Common Commands
Each port is self-contained and hermetic — `cd` into its directory first. CI runs exactly these.
| Port | Build | Test |
|------|-------|------|
| `js/` | `npm install && npm run build` | `npm test` (builds, then `node --test`) · run a demo: `npm run example` |
| `python/` | `pip install -e ".[test]"` | `python -m pytest -q` |
| `golang/` | `go build ./...` && `go vet ./...` | `go test -race ./...` |
| `java/` | `./gradlew build --no-daemon` | `./gradlew test --no-daemon` |
| `csharp/` | `dotnet build` | `dotnet test` |
| `elixir/` | `mix deps.get && mix compile` | `mix test` · coverage gate: `mix coveralls` (min 95%) |
- All ports are versioned together (see the per-port manifests for the current number). Publishing runs through the **`release.yml`**
- GitHub Actions workflow: cut a GitHub Release `vX.Y.Z` (or `workflow_dispatch`) and each port
- publishes if its repo variable `ENABLE_*` is `true`. npm / PyPI / NuGet use **OIDC Trusted
- Publishing** (no stored tokens); Go is a tag push (`golang/vX.Y.Z`); Maven Central (Java) uses the
- `prod` environment secrets (`CENTRAL_USERNAME` / `CENTRAL_PASSWORD` / `GPG_*`); Hex.pm (Elixir)
- uses the `prod` environment secret `HEX_API_KEY` (no OIDC support on Hex). A `preflight` job fails
- the run unless all five manifests (js/python/csharp/java/elixir) match the release version — so
- bump them together. Never bake registry tokens into code, config, or CI; they are use-only env vars.
+ ## Releasing — the runbook, so nothing is missed
+ **All ports are versioned and released together.** One tag, one set of versions, every registry.
+ A partial bump is the failure this process exists to prevent.
+
+ ### The one-time setup an owner controls
+
+ - Each registry publishes **only** if its repo variable is `true`: `ENABLE_NPM`, `ENABLE_PYPI`,
+ `ENABLE_GO`, `ENABLE_NUGET`, `ENABLE_JAVA`, `ENABLE_ELIXIR`, `ENABLE_CLOJARS`. A leg that is
+ off is skipped silently — **check the variables before concluding a release "worked"**.
+ - Secrets live in the **`prod` GitHub environment**, never in the repo, never in code.
+
+ | registry | mechanism | credentials |
+ |---|---|---|
+ | npm / PyPI / NuGet | **OIDC Trusted Publishing** | none stored; the trusted publisher is scoped to the `prod` environment |
+ | Go | tag push `golang/vX.Y.Z` | none — `contents: write`; idempotent, skips if the tag exists |
+ | Maven Central | Gradle `publishAndReleaseToMavenCentral` | `CENTRAL_USERNAME`, `CENTRAL_PASSWORD`, `GPG_PRIVATE_KEY`, `GPG_PASSPHRASE` |
+ | Hex | `mix hex.publish --yes` | `HEX_API_KEY` |
+ | Clojars | `clojure -T:build deploy` | `CLOJARS_USERNAME`, `CLOJARS_PASSWORD` — a Clojars **deploy token**, not the account password |
+
+ Maven Central, Hex and Clojars use stored secrets only because none of the three offers OIDC.
+ Never bake a registry token into code, config, a test or CI; they are use-only env vars read at
+ the point of use.
+
+ ### Cutting a release
+
+ 1. **Bump every manifest to the same version** — js, python, csharp, java, elixir, and
+ `clojure/build.clj`. Go has no manifest; it releases as a tag. The `preflight` job **fails the
+ run** if any manifest disagrees with the tag, which is the guard against a partial bump.
+ 2. **Move the changelog**: `## Unreleased` becomes `## X.Y.Z — YYYY-MM-DD`, with a fresh empty
+ `## Unreleased` above it.
+ 3. **Cut a GitHub Release `vX.Y.Z`.** That is the trigger. `workflow_dispatch` also works for a
+ re-run. **The release body IS the changelog section** — not a second, divergent account.
+ 4. **Watch the run and read the job list.** A green workflow with three skipped legs is not a
+ release; confirm each enabled registry actually published.
+ 5. **Verify from the outside, as a consumer would** — install the published artifact from the
+ registry in a clean directory and run something. ADR-0012 exists because NuGet and Maven
+ publishes have **reported success without landing**, so a green CI job is not evidence that a
+ package is installable.
+
+ ### Do not
+
+ - Do not tag or publish unless the owner asked. Cutting a release is outward-facing and
+ irreversible: a version number can never be reused on any of these registries.
+ - Do not bump one port "to unblock" — that is exactly what `preflight` refuses.
+ - Do not hand-write a GitHub Release body that disagrees with `CHANGELOG.md`.
+
## Coding conventions
- **Match the local style of each port.** Idiomatic TS, idiomatic Python, idiomatic Go,
idiomatic Java, idiomatic C# — not a transliteration of one language into another. Same
*behavior*, native *shape*.
- **The `examples/` fixtures are shared and authoritative** — don't fork per-language copies;
if a fixture must change, change it once and re-verify every port.
- **Secrets are use-only.** Remote MCP `headers` values expand `${ENV_VAR}` from the environment
at call time and are **never logged**. Never write a real key into a spec, test, example, or
comment — read it from the environment; use an obvious fake (`YOUR_KEY_HERE`) for placeholders.
- **Conventional commits**: `feat:`, `fix:`, `docs:`, `test:`, `chore:`, `ci:` — scope by port
where it helps, e.g. `feat(go client): …`, `test(js): …`. Do **not** add `Co-authored-by:`.
- Touch only what the task needs; note unrelated issues rather than fixing them inline.
+ ## CHANGELOG.md — required, and it is not the git log
+
+ `CHANGELOG.md` at the repo root is the **one place a user learns what changed**. Keeping it is
+ part of the change, not a release chore: the person who made the behaviour is the only one who
+ still knows *why*, and a changelog written later from `git log` reliably records what was
+ touched rather than what a user gets.
+
+ **Every change of substance adds to `## Unreleased` in the same PR that makes it.** A change is
+ of substance if a user could notice it: new capability, new option, changed default, changed
+ output, a fixed bug they could have hit, a new port, a moved contract. Typo and internal-only
+ refactors do not qualify.
+
+ **Write what a user gets, not what a file did.** `feat(clojure): add :on-error` is a commit
+ subject; the entry says what the option is for, what happens without it, and what it now makes
+ possible. Follow the existing entries — they lead with the user-visible change, then the
+ reasoning, then the detail.
+
+ **Say what is NOT done.** Every entry that ships a partial capability names the gap, and names
+ where it is tracked (`openspec/changes/<name>`). A port at `core` tier lists its debt. This is
+ the same rule as the option gate printing permitted absences by name: an omission that stops
+ being mentioned is indistinguishable from something that was finished.
+
+ **On release**, `## Unreleased` becomes `## X.Y.Z — YYYY-MM-DD` and a fresh empty `## Unreleased`
+ goes above it. The version must match the GitHub Release tag and every port manifest — the
+ `preflight` job in `release.yml` fails the run on drift. The GitHub Release body should be the
+ changelog section, not a second, divergent account of the same work.
+
+ **Per-port notes** belong in the shared entry, named inline (e.g. "golang only"), never in a
+ per-port changelog — six parallel changelogs is exactly the drift this repo exists to prevent.
+
## Documentation map
| Document | Purpose |
|----------|---------|
| `CLAUDE.md` | Project guidance for Claude Code (this file) |
| `SPEC.md` | The shared cross-language contract — start here for any behavior question |
| `README.md` | End-user pitch + "zero to agent in 3 steps" |
| `js/README.md`, `python/README.md`, `golang/README.md` + `golang/GUIDE.md`, `java/README.md`, `csharp/README.md` | Per-language end-user docs |
| `openspec/changes/` | Active OpenSpec change proposals — feature/change work starts here (`/opsx:propose`) |
| `openspec/specs/` | Canonical capability specs (accrue from archived changes) |
+ | `CHANGELOG.md` | What changed, for users — updated in the SAME PR as the change (see above) |
| `PUBLISHING.md` | How each port is published |
Consult `SPEC.md`, `openspec/`, and the relevant port's README before making behavior changes.
Run `/opsx:propose` to start any non-trivial change.