The flash model is `deepseek-flash` everywhere: the id it is served under
lost the generation it never needed. An installed entry is renamed rather
than left behind, and the rename moves every key that named the old id with
it — a price or an effort list stranded under the old key stops answering
for the model that replaced it, which reads downstream as a setting the
user edited reverting to its default.
A preset is now what settles which of its models read images. Reading a
name misses every Kimi, Qwen, MiniMax and Claude model these presets
declare, and `deepseek-flash` spells nothing at all; the spelling stays the
last resort for an address nothing has declared, and stays a correctable
suggestion rather than a claim. The connection that existed only to reach an
image-taking model folds into the entry beside it once that entry reaches
the same model on the same key — unless its user configured it, because
folding would answer their edit by deleting where it lived.
A preset changing shape reaches installs holding the old one through one
declared shape per generation and one upgrade that brings a matching entry
forward. The guard is the whole shape, because an entry holding what we
shipped is ours to move and anything else its user curated. That retires
nine hand-written migrations that each recognised one vendor by hand.
Official list prices live once, in `officialRates`, oldest generation first.
A price change appends rather than edits: the superseded rate is what lets
an installed config be recognised as holding one of ours and brought
forward, instead of quoting a retired rate for the rest of its life. `config`
reads them through `billing.CurrentRate` and `SupersededRates` and restates
none of them — a table written twice is one that drifts, which is how a
vendor's USD rate ended up published on one side only.
`make pricecheck` reads each vendor's own page and compares it against that
table, and says how long ago a person last confirmed each one. It reaches
the network, so it stays out of lint and out of CI. A page it cannot read
exits non-zero: unread is its own verdict and never counts as agreement.
The narrowed standards gate built its universe from `git diff --name-only
HEAD`, which answers what the worktree changed against HEAD and says nothing
about a file git has not started tracking. Three new files reached `make check`
as clean while the full sweep found a violation in one of them. Not a wrong
judgement — a measurement universe missing a piece, and a green meaning only
that the gate never opened the file.
The universe moves into Go as `-changed` rather than growing another expression
in the recipe, because the defect was in the universe and a universe computed in
a Makefile cannot be tested. `trackedChangedPaths` is kept as its own function
though nothing in production calls it: it is the incomplete universe, and a test
that cannot express the incomplete one cannot prove the complete one fixed
anything. That is what the second test does — one finding, two universes, two
verdicts — and the first fails on purpose if `git diff` ever starts reporting
untracked files, because the witness would be gone.
Deleted paths stay out. A path the change removed cannot be scanned, and handing
one to the scanner asks it to fail on an absence the change intended.
REASONIX.md now names `make check` as the owner of that universe rather than
naming the flags behind it. The rule being preserved is that the evidence entry
point is the make target; which flags implement it is not the contract, and
writing them down is how the document went stale the first time.
CI is unaffected: it runs the full sweep with no narrowing, so the hole existed
only in the local check cited as evidence that a change owed nothing.
Two spellings of the same version. `git describe` on this line answers
studio-v2.12.0, and that string went into main.version for every local build
while the release injected v2.12.0 — the tag with its line prefix stripped. The
prefix belongs to the release line, not to the version, and leaving it on broke
both readers of that field: the pattern that decides whether a version names a
published release rejects it, and ParseVersion splits on the first dash and
comes back with nothing. So a locally built CLI could not install from a
release it does in fact have, and installed on a remote it read as versionless
— which the floor waves through, since a version nothing stated is not a
version below it. The Makefile strips what the release strips.
The stray asset is mine. Staging picked the CLI archives out with
`-name 'reasonix-*'`, and the Windows signing flow leaves a
reasonix-studio-host.exe in the same directory: it matched, and 2.12.0 shipped
a 34MB internal component next to the archives, where it reads as a CLI someone
might download and cannot run. Taken from staging/cli-archives now — the job
that produced them — because origin is what was meant and the name was only
ever standing in for it.
The downloaders are bounded. All three had no timeout of any kind, which was
invisible while the routes that use them refused before reaching the network.
On a machine that reaches TCP 443 and never completes TLS — measured on a real
one here, against both GitHub and the Cloudflare mirror — the first route auto
tries would have held every connect for as long as the OS allowed before the
route that works was tried. curl bounds connecting and stalling but not total
time, so a 40MB archive over a slow link is still a download and not a failure;
wget takes the same pair; Invoke-WebRequest has no connect timeout at all, so a
short HEAD runs ahead of it.
`make studio` now builds the SPA and the kernel and opens the Electron
window. The path it replaces was macOS-only by construction -- a bare Go
binary is not a GUI app to LaunchServices, so the script bundled one
before launching and refused to run anywhere else. Electron's own dev
build is already an .app, so one script serves every platform.
Three things had to be true first, and none of them were.
The window carried no icon at all, so the taskbar drew Electron's. It is
named per platform now: .ico on Windows, where the taskbar reads the
small sizes out of it and a PNG would be one bitmap scaled; a PNG on
Linux; and nothing on macOS, which takes the icon from the bundle and
ignores what a window says.
`build:host` wrote bin/reasonix-studio-host with no extension, which
Windows will not spawn. `go build -o bin/` names the binary after the
package and adds .exe where it belongs.
That failure arrived as "the kernel sent no handshake" twenty seconds
later, because firstLine listened for exit and a program that cannot be
started emits error and never exits. It says what happened now, in
milliseconds. An error a caller must tell apart carries an identity, and
"could not be started" is not "said nothing".
Also fixes a test that could only pass on POSIX: it built an expectation
with a /tmp literal and compared it against path.join, which answers in
backslashes on Windows.
pnpm test 11/11.
Splitting the wallet onto its own route added the call to sse.ts and left
apiPaths behind, so the request fell through to the assets and the rail read a
404. The route parity test caught it and had been failing since — in a nested
module the root `go test ./...` cannot see, which is where a red test hides
longest. `make test` now runs the shell and the SDK too.
REASONIX.md calls these paths the ones where a wrong change is "a hole in a
boundary". Measured, they were the least-tested part of the kernel rather
than the best: shellsafe 59.3%, shellparse 63.6%, sandbox 69.4%, netclient
47.2%, against a tree median of 76.5%. The code deciding what the agent may
execute and reach had the weakest evidence that it works.
The gate reads the same `sensitive:` bullets the runtime reads for its
review requirement, so a path is declared once and gets both effects — there
is no second list to fall out of sync. Floors are per declared path and
measured from the coverage profile per file, not per package, because
`internal/control/approval.go` is sensitive while the package around it is
not; measured on its own it is 93.9%, which no package number would show.
Floors ratchet in the opposite direction to repolint's budgets and for the
same reason: raising one is free, lowering one needs -allow-drop and a
reason in the PR. They record what is there today rather than an aspiration,
so the gate is green on arrival and only fails on a regression.
Verified by removing one shellsafe test file: the gate reports 47.3% against
its 59.3% floor. Adding an unrelated `sensitive:` line puts that path under
the gate with no other change.
The Wails-bound app and its React frontend are gone. Studio reaches the
kernel through internal/serve's HTTP routes instead of bound Go methods,
so the shell that replaces ~320k lines is next/ plus the SPA it serves.
Three things the deleted tree owned move rather than disappear: the
WebView2 fork's patch guard, the macOS dock-icon safe-area assertion, and
the Windows ICO the resource stamper needs. The fork itself stays — its
mixed-DPI and proxy-isolation patches are Studio's too.
CI keeps one desktop Go gate per platform, now built against WebKitGTK
4.1 the way studio-build.sh links; the frontend legs, the Wails CLI pin,
and the 4.0 leg went with the app.
The desktop release line (release-desktop.yml and the desktop channel in
release-stable.yml) is untouched here: it still serves installs that are
already out there, and unwiring it is release-pipeline surgery of its own.
REASONIX.md told the model to run `go run ./tools/repolint -only ...`, which the
host classifies as an unknown program that might write — so a delivery run that
followed the instruction could not cite the result. One observed run was refused
for `gofmt -l`, reached for the repolint command the file recommends, and was
refused again for the same reason.
A `make` target is the entry the host already recognizes, and Makefile is where
this project declares its checks. Nothing in the classifier changes.
The alternative — letting a project name its own verification commands in config
— was rejected. Delivery deliberately refuses to escalate (see
mixedShapeClearedByEscalation) because it only accepts what the host can confirm
by itself; a config channel would let the audited party define the audit.
A bare binary draws its window fine, so nothing looks wrong until a native
panel opens: LaunchServices only treats a bundled process as a real GUI app,
and NSOpenPanel takes focus and closes in the same beat. "Add a folder…" reads
as broken, and the search goes to the frontend, where nothing is wrong.
make studio builds the frontend, builds the shell, wraps both in an .app and
opens it. The bundle symlinks frontend-next beside the executable — assetsFS
looks there first, which frees the launch from the working directory `open`
gives it (that is "/", and the binary exits before drawing anything).
Problem: A broken installed Wails CLI made prod_test exit before its repair path, while the regression suite copied the repository's current pin and the guard accepted alternate dynamic install sources.
Root cause: The version probe inherited errexit and pipefail, the integration assertion embedded v2.13.0, and the guard relied mostly on negative checks instead of proving each supported consumer read .wails-version.
Fix: Treat probe failures as an explicit version mismatch, derive real-repository expectations from the shared pin, validate stable pin syntax, whitelist supported install sources, and guard plus quote the Makefile installer. Add regressions for broken CLIs, alternate workflow sources, hard-coded Make variables, and invalid pins.
Verification: make lint; bash scripts/release-workflows.test.sh; isolated GOBIN make wails-install with wails version v2.13.0; broken Wails probe returns an actionable mismatch; git diff --check.
Co-authored-by: SivanCola <[email protected]>
Problem:
Local production builds and contributor setup could still bypass the shared Wails pin, so the old CLI/library skew remained possible even while the workflow-only guard passed.
Root cause:
The guard inspected only two workflow files, prod_test checked command presence instead of the installed CLI version, and the desktop guide recommended @latest.
Fix:
Add a make-based pinned installer, make prod_test repair and verify version mismatches, reject repository-wide hard-coded Wails installs, move the maintenance guidance out of the cache-stable prompt, and cover each drift path with regression tests.
Verification:
- make lint
- bash scripts/release-workflows.test.sh
- make wails-install installs v2.13.0 in an isolated GOBIN
- wails build -platform darwin/arm64 -clean -skipbindings -nopackage
Co-authored-by: SivanCola <[email protected]>
Dependabot bumped desktop/go.mod to wails v2.13.0. Nothing bumped the CLI: its
version is a string inside three workflow files, where Dependabot cannot see
it. So CI — and the desktop release job — have been building a v2.13.0 library
with a v2.12.0 toolchain.
The drift runs the other way too. `wails dev` rewrites desktop/go.mod down to
whatever CLI the developer has installed, so a local run quietly reverts the
dependency upgrade; that is how this was found, after reverting the same
downgrade several times in one afternoon.
.wails-version is now the single pin, read by the Makefile and all three
workflows, exactly as .golangci-version is. check-wails-pin.sh proves it held
and refuses a workflow that hard-codes a version again — it was written failing
first, and each of the three drift directions was replayed against it: a
downgraded go.mod, a library bumped past the pin, and a version written back
into a workflow.
The pin is v2.13.0, matching the library Dependabot chose. That moves CI's
toolchain forward by a minor version; the desktop, desktop-macos and
desktop-windows jobs are what verify it, and they are the reason this is worth
finding out now rather than at the next release.
Two things cost a full CI round trip each, both avoidable.
REASONIX.md told contributors golangci-lint was "not locally available", so
nobody ran it. It always was: lint-cross already shells out to it. What was
missing is a pinned version and one command. `.golangci-version` is now the
single pin the Makefile and the lint job both read, `make lint-install`
installs it (with CGO_ENABLED=0, so a stray clang on PATH cannot break the
install), and `make lint` runs the same two gates CI runs, in the same order.
Replaying a recent PR's diff through it reproduces the exact `modernize`
finding CI took ten minutes to report.
The Windows smoke step then failed on a run whose only change was two test
fixtures. Across four consecutive runs of near-identical code, internal/boot
measured 133s, 146s, 162s and 180s, and internal/agent 82s, 92s, 144s and
180s: a 2.2x spread from runner variance, four heavy packages sharing four
vCPUs. A 3 minute per-package timeout sat inside that spread, so the step was
one slow runner away from failing at any time. It is 8m now, still well inside
the step's own 15 minute budget, which is what actually catches a hang.
Neither change touches what is tested.
golangci-lint only type-checks the GOOS it runs under, so the 92 files behind
//go:build windows and //go:build darwin had never been linted by CI — the
Windows updater, installer, process reaping, sandbox, and the macOS PTY and
watchdog paths, which is exactly where bugs are hardest to reproduce and most
expensive to ship.
This is not theoretical: the modernization branch was verified clean on Windows
and still failed CI with six findings in !windows files, and a darwin-only run
then surfaced three more that neither Windows nor Linux could see.
Both modules cross-check without a toolchain except desktop under darwin, whose
main-thread watchdog is cgo; that leg runs natively in desktop-macos instead.
`make lint-cross` gives the same five-way sweep locally.
Existing findings are fixed: CloseHandle in a defer and LazyProc.Call join the
errcheck exclusions next to the Close entries they mirror (LazyProc.Call always
returns a non-nil errno; callers read r1), SetFileAttributes is explicitly
discarded, and updater_windows drops a redundant embedded-field selector.
* chore(standards): gate comments, file size, and layering in CI
The comment and file-size rules lived only in an untracked CLAUDE.md written
for a TypeScript project, so nothing ever checked them, and REASONIX.md told
editors to match the surrounding comment density -- which is what reproduced
the essays. tools/repolint makes the rules machine-checkable and ratchets
them: recorded debt passes, anything new fails the required lint check.
Weights are excess lines rather than counts, so trading a short violation for
a long one still trips the gate. Layering encodes the contract REASONIX.md
already claimed and the import graph already satisfies, with one recorded
exception (capdiag reaching into boot).
* chore(standards): point CLAUDE.md at REASONIX.md
Claude Code loads CLAUDE.md and Reasonix loads REASONIX.md, so a repo with
only the latter leaves Claude Code sessions with no standing instructions.
A committed one-line pointer keeps the single source of truth without the
two files drifting apart again; the negation un-ignores it for contributors
whose global gitignore drops CLAUDE.md.
Problem
- reasonix --version only printed a bare version string, which is hard to
correlate with source builds and user bug reports.
- No first-party bash/zsh/fish completion script existed.
- /mcp status did not show which config plane registered a server/tool.
Root cause
- Version path was a one-line println; no completion registry; ServerStatus
omitted Spec.ConfigSource.
Fix
- BuildInfo with version --verbose / --json (version, git commit, build time
UTC, target, go). --version/-v stay single-line. No config path, no CST.
- reasonix completion bash|zsh|fish from a shared registry with Run dispatch
drift guard (adapted from #7425).
- Plumb ConfigSource into ServerStatus and render source= on /mcp status and
tool lists (from #6175 scope: display only).
- Makefile ldflags inject version, gitCommit, buildTimeUTC.
Verification
- go test ./internal/cli -count=1
- go test ./cmd/reasonix ./internal/plugin -count=1
- TestBuildInfo*, TestCLICompletion*, TestRenderMCPStatusShowsConfigSource
Refs #6476#7425#6175#6578#6833
Session lease / REASONIX_HOME / bash session-allow already covered on main-v2;
no --home flag added.
Co-authored-by: h4rK8s <[email protected]>
Co-authored-by: Zhiyao Wen <[email protected]>
Stage 10 of the unified extension kernel / Extension Protocol v1 plan.
- New docs: EXTENSIONS.md/.zh-CN.md (capability map, install/manage,
fail-atomic reload semantics, development guide, compatibility,
full-trust security model) and EXTENSION_PROTOCOL.md/.zh-CN.md (prose
companion to the generated index: transport, lifecycle, content refs,
interception, streaming providers, structured UI, errors, v1 stability
contract, security model).
- Updated docs: PLUGIN_PACKAGES (Manifest v1 chapter: strict parsing,
merge rules, prompts/themes, runtime declaration, FULL TRUST disclosure
and --link trust inheritance), CAPABILITY_DIAGNOSTICS (additive v1
prompts/themes/runtime fields, schema_version stays 1), CLI (/reload),
ACP (_reasonix.io/session/reloadExtensions, extensionSurface
negotiation, /<plugin>:<action> discovery), RELEASING (sdk/go/vX.Y.Z
tag convention, three-product tag contract unchanged), THEME_PACK
(plugin theme kind and fallback-preserve, from stage 4).
- CI: new sdk job on the three-OS matrix (gofmt, vet, stdlib-only guard,
test, race on pushes) triggered by sdk/ or internal/extension/ changes;
root conformance tests run in the existing root matrix.
- Makefile: sdk-test / sdk-test-race targets.
- Bundle budget: initial raw JS+CSS 2250 -> 2265 KiB with a comment
recording the extension UI surface as the cause; every gzip budget
(the user-visible transfer cost) is unchanged and passing.
Compatibility audit (kept green by tests added in stages 1-9): legacy
manifests parse byte-identically, plugin-packages.json schema unchanged,
session JSONL untouched with extension state isolated to
<session>.extensions.json, desktop theme state preserves plugin: IDs
across uninstall while legacy files load unchanged, install-plan JSON is
omitempty-additive, plugin/... refs degrade to model-unavailable on old
versions, and the two new event kinds append at the enum end with
text fallbacks for old clients.
make build hardcoded -o bin/reasonix, so on Windows it produced an
extensionless binary; add GOEXE from go env so the suffix is correct
cross-platform, and note bin/reasonix(.exe) in the build docs.
Co-authored-by: Qxy0Happy <[email protected]>
The v1 simple-git-hooks shim runs npm run lint/verify, which enoent-fails on the Go main-v2 branch (no root package.json) and blocks every commit/push. 'make hooks' points core.hooksPath at versioned .githooks/; pre-push runs go vet (cross-platform — full tests stay in CI). .gitattributes pins the hook to LF so it executes on every platform.
Co-authored-by: reasonix <[email protected]>
Wire CodeGraph (tree-sitter + SQLite symbol/call graph) in as a built-in
stdio MCP server so the agent gains codegraph_* search/context/explore/
trace/node tools — replacing the old embedding semantic search with a
zero-API-cost, cross-language code-intelligence layer.
- internal/codegraph: Resolve (config path > bundle beside the executable >
PATH) and EnsureInit. Init is a bare `codegraph init` (~100ms, size-
independent); serve's daemon indexes in the background, so startup never
blocks even on a large repo.
- plugin.Spec gains Dir; the stdio transport honors it so the cwd-aware
server is pinned to the project root (also fixes a latent cwd issue).
- boot injects the server when [codegraph].enabled (default true) and the
bundle resolves; a missing bundle is a silent no-op (falls back to grep).
- mcp list reports the built-in server's resolved status, doubling as a
headless preflight.
- .gitignore the per-project .codegraph/ index.
Tested: unit tests for Resolve/EnsureInit, plus an env-gated end-to-end
(make e2e-codegraph) that handshakes a real CodeGraph bundle over MCP and
calls codegraph_search. Distribution (bundling the binary in the release
archive) is intentionally left as a follow-up.
Bring in the finished Go rewrite (developed under the placeholder name
"duo") as the v2 codebase. Code-only import — no upstream git history.
Config- and plugin-driven coding agent: registry core, OpenAI-compatible
providers, builtin tools, MCP (stdio + HTTP) plugins, permission + sandbox,
custom slash commands, @-references, two-model collaboration.
Renamed throughout: duo→reasonix, Duo→Reasonix, DUO→REASONIX — module path,
imports, CLI name, cmd/ dirs, reasonix.toml, .reasonix/commands, and
$REASONIX_* env prefixes. Kept the v2 MIT LICENSE (the Go project had none).
Verified: go build ./... · go vet ./... · go test ./... all green; gofmt clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>