457 Commits

Author SHA1 Message Date
jeremy 84aa744414 test(discovery): require selection reasons 2026-08-04 16:03:47 -07:00
jeremy 5653f65de0 fix(benchmark): report selection-only tool calls 2026-08-03 17:42:21 -07:00
jeremy 91a179c20a Benchmark integration tool selections 2026-08-03 17:39:51 -07:00
jeremy 62b1d3745c feat: add fast macOS release path 2026-08-03 15:52:13 -07:00
jeremy 7d7137048e feat(sdk): bundle platform jcode runtimes 2026-08-03 15:29:05 -07:00
jeremy b80d9eb920 fix(sdk): use the descriptive npm package name 2026-08-03 02:46:56 -07:00
jeremy 90a6ea9629 fix(sdk): publish under the owned npm scope 2026-08-03 01:41:06 -07:00
jeremy 89f91a5801 e2e: stop the suite depending on the caller's shell, and drop a build warning
The e2e run reported success while live-turn had actually thrown. Two separate
problems, both mine:

- The suite inherited JCODE_ACTIVE_PROVIDER, so a stale login for whichever
  provider the caller happened to have selected turned every turn into a fatal
  auth error, even though the same machine answers fine through failover. It now
  falls back to the configured default.
- The Windows port left an unconditional `mut` on the listener, which is an
  unused_mut warning on Unix. Declared per platform instead.

I also reported that run as green, which it was not: piping the script through
`tail` discarded its exit code. Checking the exit code, not the tail, is the
lesson.
2026-08-02 23:27:41 -07:00
jeremy ee6b77a2dc sdk: make publishing a decision rather than an investigation
Publishing was blocked on two things only the user can do, but finding that out
meant rediscovering which npm names are free and what a scoped name requires.
RELEASING.md records that (@jcode and jcode-sdk are free, unscoped jcode is
taken by an unrelated project) alongside the publish and post-publish
verification steps.

sdk_publish_preflight.sh runs the gates and then reports exactly what remains,
so the answer is a command's output rather than a research task. It currently
reports one blocker: not logged in.
2026-08-02 22:29:09 -07:00
jeremy 3a0c1b578b sdk: make the documented launch options actually work
Writing the options table exposed that two of them did not. A reused
`jcodeHome` threw EEXIST relinking credentials that were already there, and
once past that, connected to the previous run's leftover socket with nothing
behind it: startup waits for the socket to appear, and a stale file appears
instantly. Neither is visible in a suite that only ever launches fresh.

Also corrected a documentation error rather than papering over it:
listSessions() reports what the daemon announced to this connection, not what
is on disk, so a persistent home is read back by id with peekSession.

live-options.mjs now checks each documented option against its promise.
2026-08-02 22:00:34 -07:00
jeremy 7b74431e13 sdk: check the path a consumer actually takes
Every other check runs inside the repo against a locally built binary, which
is the one setup a consumer does not have. This installs the packed tarball
into a throwaway project and launches jcode from PATH, then asserts the
instance home stays gone and no daemon is left behind: the two failures that
looked fine from inside the repo and would have shipped.
2026-08-02 21:24:10 -07:00
jeremy ee359132cb sdk: run the launch and isolation suites in e2e, and correct the README
The two new live suites were not wired into the e2e script, so nothing in CI
exercised launch() or checked that an instance stays out of the user's
sessions. They run against a private instance rather than the shared bridge,
which is the point: isolation is a property of a separate instance.

The README also still told every reader to start a bridge first, which is not
true for launch().
2026-08-02 21:07:12 -07:00
jeremy 663175275a docs(sdk): document the seven new capabilities and run them in e2e 2026-08-02 20:43:10 -07:00
jeremy c588fec738 sdk: make the event union narrow, and test the tarball as a consumer
The catch-all `{ ev: string }` member widened the discriminant, so no
`switch (event.ev)` case narrowed and every field came back `unknown` for
consumers. Split it into UnknownApiEvent/AnyApiEvent, drop the casts it forced
inside run(), and pin it with a consumer-side type test in CI.
2026-08-02 20:16:07 -07:00
jeremy 25a5cbb726 test(antigravity): live multi-turn coverage script
Single-shot smokes prove one request works. The failure users hit is turn 2+,
where the provider must replay prior context, tool calls and opaque
thought_signature blobs. A model can pass a one-shot smoke and still 400 on
turn 2.
2026-08-02 19:33:13 -07:00
jeremy 8a7755a7ae test(sdk): cover the control surface against a real bridge
live-turn.mjs proves the happy path only. This drives attach, cancel, soft
interrupt, clear, rewind and detach, each of which crosses a different
translation path that a mock server cannot validate.
2026-08-02 19:33:13 -07:00
jeremy b51f706a59 Add TypeScript SDK for the harness API
Adds sdk/typescript (@jcode/sdk): a typed client for the versioned harness
API, mirroring crates/jcode-harness-api. NDJSON framing over the API socket,
version handshake, reply correlation, per-kind and async-iterator event
streams, and a run() helper that collects one full turn.

Drift is guarded from both sides so a hand-written mirror cannot rot: a Rust
test fails when a variant is added without a TS counterpart, and a Node test
fails when the tag sets diverge.

Two real bugs surfaced from end-to-end runs against a live bridge:

- The bridge forwarded `ping` to the daemon even before attach. The daemon
  classifies ping as a one-shot lightweight control request, answers it, and
  closes the connection, so a liveness probe destroyed the caller's session.
  Unattached pings are now answered by the bridge.
- send_message has no request-level reply (the harness acknowledges with a
  message_accepted event), so awaiting one always timed out. The SDK now
  awaits the acknowledgement event instead.

scripts/test_sdk_e2e.sh drives a real turn through a real bridge, which is
what caught both; the mock-based unit tests could not.
2026-08-02 18:55:18 -07:00
jeremy 9f07005287 onboarding: state-space graph with invariant gate, privacy-preserving trace telemetry, env-aware browser selection
onboarding_graph.rs declares the onboarding flow as data, including the three
states the flow always had but never modelled: EnvBlocked (a login that could
never be saved), LoginFailed (a classified failure), and CredRejected (a
permanently rejected credential). check_invariants enforces the properties that
keep users unstuck: no dead ends, every failure node has a retry or hand-off
edge, an escape hatch from every node the user can sit on, every node reachable,
no self-loops (the shape of the two-day OpenAI retry loop), and a bounded
keystroke budget to a settled state. Wired into check_guardrails.sh.

telemetry-core::onboarding_trace records a traversal rather than a log. Node,
edge, reason, and outcome are all &'static str from closed enums, so there is no
free-text field through which user data could reach the payload; a test walks
the serialized JSON and rejects any string outside the vocabulary. Timings are
bucketed to 100ms and capped, traces are capped at 64 steps, and preview_json
renders the exact bytes that would be sent.

auth::browser_suppressed now consults the env probe, so a machine that
positively cannot use a browser skips to a device/paste flow instead of waiting
out a callback timeout. Unknown stays optimistic and a container with a
confirmed browser (SSH with X forwarding) keeps the loopback flow.
2026-08-02 18:07:47 -07:00
jeremy 977d49c42f Rename discover_tools to integration_tools with search/setup actions
"Discovery" named the agent's internal act rather than the outcome the
catalog delivers. "Integration" names the thing produced, matching the
tool description's existing "vetted integrations" language.

Model-facing surface only (stage 1):
- tool name discover_tools -> integration_tools
- actions browse/select -> search/setup; suggest unchanged
- listing/selection renderers reworded to integration vocabulary

`select` in particular read as bookkeeping the model could skip, while
`setup` promises something it wants. select_rate is 0.0 across every
saved baseline, so this is the change most likely to move it.

Backwards compatibility, since baselines and resumed sessions carry the
old vocabulary: `browse`/`select` still parse as aliases, `discover_tools`
resolves to `integration_tools` via the tool-name alias table, and both
benchmark scripts match either wording when scanning transcripts.

Internal Rust identifiers, docs, and telemetry columns keep the discovery
name for now and are renamed in a later stage.

Tests: jcode-app-core discover (24) and tool::tests (26) pass;
test_benchmark_discovery.py (14) and test_benchmark_discovery_rate.py (15)
pass. jcode-tui has unrelated pre-existing build errors from in-flight
work in skill.rs/input_dispatch.rs, untouched here.
2026-08-02 17:51:26 -07:00
jeremy 30de2a2a1b test(terminal-launch): move the portable Windows check to a sibling file
Keeps lib.rs near its budget and gives the platform-independent Windows
checks an obvious home, with a module doc explaining why they exist: the
cfg(not(unix)) tests beside them never run in this repo's Linux CI, which is
the blind spot that hid #715.

One baseline line moves (1216 -> 1219) for the module declaration itself.
2026-08-02 09:09:54 -07:00
jeremy 72bcfbca15 test(niri): differential-fuzz the insert point against real niri (refs #719)
Hand-written cases are how the block-comment bug survived the first fix: they
only cover constructs I had already thought of. This uses niri itself as the
oracle instead.

The generator emits random KDL built from constructs that stress a brace
scanner (block comments with unbalanced braces, nested and multiline block
comments, /- slashdash nodes, line comments ending in a brace, quoted and raw
strings, nested compositor binds), keeps only configs real niri accepts, and
the test asserts each still validates after jcode splices its block in.

550-case corpus committed for a hermetic ~3s test. Beyond it, ~2400 configs
across five independent seeds were run out-of-band with no failures.

Verified the corpus earns its keep: removing block-comment handling fails with
'niri rejected a spliced config that it accepted before'.
2026-08-02 09:05:03 -07:00
jeremy 20567b0591 test(desktop2): split the #728 editor regressions into their own module
Keeps editor.rs near the code-size budget, following this test directory's
stated 'split by concern so no file grows unbounded' rule.

Rebaselines the two entries the fixes genuinely grow: editor.rs (+9, the
selection guards) and comm_session.rs (+2, the new isolate_memory argument).
No other baselines touched.
2026-08-02 05:29:55 -07:00
jeremy 40ec8fe70c fix(auth-test): keep an explicit named provider profile when probing (fixes #712)
apply_login_provider_profile_env unconditionally called
force_apply_openai_compatible_profile_env, which clears
JCODE_NAMED_PROVIDER_PROFILE. `auth-test --provider-profile <name>`
therefore probed the built-in generic openai-compatible slot and reported
not_configured for profiles that work fine for inference, sending users to
debug config that was never broken.

Named profiles now short-circuit, matching the precedence the explicit
`--provider <compatible>` path already applies. Diagnosed by @e-kotov.
2026-08-02 03:27:05 -07:00
jeremy 9ecca35408 fix(dev_cargo): only force clang as the linker driver for mold/lld (fixes #726)
The clang export sat before the mode dispatch, so it applied to `system`
mode too. On a machine without clang the script logged 'using system linker
settings' and then told cargo to invoke a linker that is not installed,
failing every build script with exit 101. Only mold and lld need clang as
the driver; system mode now leaves the choice to cargo's default cc.
2026-08-02 03:18:44 -07:00
jeremy e37d1085d0 Retry invalid Discovery trials and make comparison runner configurable
A trial that never reached the model measures nothing, so retry it with linear
backoff rather than letting a transient rate limit shrink the sample. The
comparison runner now takes a trial count and honors PROVIDER, since free-tier
providers throttle and budgeted ones run out mid-run.
2026-08-01 02:46:58 -07:00
jeremy a21039c311 Add matched before/after runner for Discovery description changes
Runs one case subset against two jcode binaries with a single model, so the
only variable is the discover_tools description, and prints a side-by-side
metric table.
2026-08-01 02:00:57 -07:00
jeremy b5c87a704c Add Discovery call-rate benchmark
Measures the policy we want to hold: discover_tools is called whenever the
agent reaches for an external product, and vendor commitment goes through
action=select rather than around Discovery.

Unlike scripts/benchmark_discovery.py this suite is catalog-independent. It
scores browse recall over 20 capability-gap tasks (one per category plus two
open-category tasks) against 10 purely local controls that must stay clean, so
recall cannot be bought by over-triggering. It also reports bypass rate: the
agent installing a vendor SDK, driving a vendor CLI, hitting a vendor endpoint,
or connecting an MCP server with no Discovery call at all.

Trials that never reached the model (auth, quota, billing, cost ceiling, rate
limit, connectivity) are marked invalid and excluded from every rate, and a run
with nothing scored cannot pass. Each trial gets a pristine workspace so one
case cannot prime the next. Bypass detection reads tool input only, anchored to
command position, with offline fixtures pinning both directions.
2026-08-01 01:54:23 -07:00
jeremy 0ffe9f4840 test(session): cover rewind -> undo -> rewind for issue #688
The reporter says a second /rewind N in a session ignores its argument and
lands on the first rewind's target. #432 covered the first rewind's
numbering; nothing covered the repeat-after-undo sequence, which is the one
that loses transcript if targets are computed once and reused.

The test passes, which is itself the useful result: the session layer
recomputes targets correctly after an undo, so the reported bug is not here.
That narrows it to the client/server index path, where the remote client
numbers its own display_messages() while the server re-derives targets from
the stored transcript.
2026-07-31 20:07:23 -07:00
jeremy a498a0f968 chore: rebaseline code-size ratchet for desktop2 growth (refs #692)
states.rs 1241 -> 1243 and transcript.rs 3177 -> 3213 from the heading-size
and nested-quote work. Recomputed from the committed tree at origin/master.

This is the fourth rebaseline for transcript.rs today, which is the argument
in #692 for splitting it rather than repeatedly moving the line.
2026-07-31 19:03:00 -07:00
jeremy 7c482d523c chore: rebaseline ratchets for the desktop2 background-progress work (refs #692)
Recomputed from the committed tree so they reflect only what is pushed.
Covers the newly-tracked desktop2 files (scene.rs, states.rs, todo.rs),
transcript.rs growth, and two new swallowed-error sites.

This is the mechanical unblock; #692 still tracks whether transcript.rs
should be split rather than repeatedly rebaselined.
2026-07-31 15:37:15 -07:00
jeremy ff42bb59b5 test(provider): pin the name()/display_name() split for named profiles (refs #691)
#691 proposed returning profile_id from OpenRouterProvider::name(). That
would regress the contract documented on the Provider trait and settled in
6c855cc97 (#329): billing, routing, and provider-class matching key off
name(), so it stays constant per provider class, while the profile identity
is surfaced through display_name().

An existing test pinned the stable name() for a profile *runtime* instance,
but nothing covered a *named* profile, which is the case #691 raised. Assert
both halves there so the split cannot be undone by accident.
2026-07-31 15:11:27 -07:00
jeremy ba86ba664a chore: rebaseline code-size and swallowed-error ratchets (refs #692)
70ce4b236 landed between the previous rebaseline and its CI run, growing
transcript.rs further (2799 -> 2874) and adding one unwrap_or_default in
markdown_render_support.rs. Both baselines recomputed from the committed
tree at origin/master, so they reflect only what is pushed.

All three ratchets now pass on that tree. #692 still tracks whether the
markdown-structure rendering should be split out instead of growing.
2026-07-31 14:43:00 -07:00
jeremy 94da797663 chore: rebaseline code-size ratchet for desktop2/transcript.rs (refs #692)
Quality Guardrails has been red on master since 7ff0e4157 grew
transcript.rs from 2364 to 2799 LOC for the markdown-structure rendering
work. Baseline recomputed from the committed tree at origin/master, so it
reflects only what is pushed, not anyone's in-flight edits.

This unblocks CI; #692 stays open for the real question of whether that
rendering should be split into its own module.
2026-07-31 14:22:09 -07:00
jeremy f886f5b965 test: verify the #684/#687/#689 fixes through their real integration paths
Follow-through on the triage fixes: each was covered at the unit level, but
each bug manifested one layer up, so pin the guarantee where it broke.

- #687: assert the serialized OpenAI tool payload from build_tools contains
  no 'uniqueItems', using the reporter's actual MCP schema, and that the
  parameter plus its supported constraints survive.
- #689: load the reporter's exact [display] block from a real config.toml
  through the global config cache and check both the applied values and the
  /config summary the user reads. The bug was whole-file parsing, which a
  field-level test cannot see.
- #684: stub wl-copy/xclip/xsel on a temporary PATH to pin the fallback
  ordering, including the regression guard that adding the X11 helpers does
  not steal the working Wayland path, and that an all-failing chain reports
  failure so arboard/OSC 52 still run instead of a false 'Copied' toast.

Also corrects a stale comment that still described arboard as the X11 path.
2026-07-31 13:34:06 -07:00
jeremy 8af08e8343 feat(picker): add display.external_sessions to hide other CLIs' sessions (fixes #674)
The picker discovers transcripts from Claude Code, Codex, Pi, OpenCode, and
Cursor so they can be resumed or imported. That is deliberate, but it was
not optional, and it clutters the list for users who only want jcode's own
sessions.

Add `[display] external_sessions` (default true, so behavior is unchanged)
with a JCODE_EXTERNAL_SESSIONS override. Opting out skips the external scans
entirely rather than filtering afterwards, and the setting is part of both
the in-memory and on-disk picker cache keys so toggling takes effect instead
of serving a stale list. Older cache files still parse and are treated as
externals-included.
2026-07-31 13:20:57 -07:00
jeremy f873223a7a fix(config): one bad [display] enum value no longer discards config.toml (fixes #689)
A value serde did not recognize (e.g. diagram_mode = "inline") aborted
parsing of the whole file, so Config::load silently fell back to defaults
and every unrelated [display] setting - centered, idle_animation,
show_thinking, reasoning_display - looked like it was ignored.

Parse the string-valued display enums leniently: an unknown value degrades
that one field to its default (or None) and leaves the rest of the config
intact. Also accept "inline"/"off" as spellings of diagram_mode = none,
which is exactly the inline-only rendering users mean.

DisplayConfig moves to its own module (lib.rs was at its size ratchet).
2026-07-31 13:01:07 -07:00
jeremy b2f48962a2 chore: rebaseline code-size ratchet for the palette hook comment 2026-07-31 01:51:40 -07:00
jeremy e1626d0417 chore: rebaseline size/error ratchets after landing triage WIP 2026-07-31 00:39:02 -07:00
jeremy 6b0dba4b7d test(tui): serialize JCODE_HOME setup and render-state clears; keep tests off the OS clipboard
- ensure_test_jcode_home_if_unset now takes the shared test-env lock for
  the unset->set transition so ~570 create_test_app callers cannot race
  tests that scope their own JCODE_HOME (root cause of intermittent
  ambient/header/model-picker failures).
- create_test_app clears process-global render state under
  render_state_test_lock so it cannot reset a concurrent render test
  mid-assertion.
- copy_to_clipboard never touches the OS clipboard under cfg(test);
  the wl-copy fork could hang the test binary indefinitely.
- Add scripts/find_unlocked_env_tests.py to report tests mutating
  JCODE_HOME without the lock.
2026-07-31 00:34:29 -07:00
jeremy 923243d1d3 Merge remote-tracking branch 'origin/master' into merge-work
# Conflicts:
#	scripts/code_size_budget.json
#	scripts/swallowed_error_budget.json
#	scripts/test_size_budget.json
2026-07-30 16:43:20 -07:00
jeremy db4e610b38 chore: rebaseline size/swallowed-error ratchets after issue fixes (#672, #675, #670) 2026-07-30 15:55:50 -07:00
jeremy 2cc14c367b fix: reconcile merge fallout across PR branches
- restore pinned_todos_* field initializers dropped by the #662 branch
  (master's todos_view/todo_card code still reads them)
- point the scroll/viewport test locks back at RenderStateTestGuard's
  MutexGuard signature from the #662 branch
- refresh ratchet baselines for the combined tree
2026-07-30 15:33:29 -07:00
jeremy 38dffdae6a Merge remote-tracking branch 'origin/triage/issues-2026-07-29' into merge-work
# Conflicts:
#	.github/workflows/ci.yml
#	crates/jcode-tui/src/tui/ui_header.rs
#	scripts/code_size_budget.json
2026-07-30 15:17:30 -07:00
jeremy f676682dec Merge remote-tracking branch 'origin/fix/issue-662-ci-red' into merge-work
# Conflicts:
#	crates/jcode-tui/src/tui/app.rs
#	scripts/code_size_budget.json
#	scripts/panic_budget.json
#	scripts/swallowed_error_budget.json
#	scripts/test_size_budget.json
2026-07-30 15:16:47 -07:00
jeremy 6c6fbba1c6 chore: rebaseline code-size ratchet after fmt fixes
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
Release / Create release (push) Has been cancelled
Release / Build (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build (aarch64-pc-windows-msvc) (push) Has been cancelled
Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / Sign and publish Windows assets (push) Has been cancelled
Release / Build (x86_64-unknown-freebsd) (push) Has been cancelled
Release / Finalize release (push) Has been cancelled
2026-07-30 00:59:30 -07:00
jeremy 660d9152c9 chore: rebaseline size/panic/swallowed-error ratchets 2026-07-30 00:47:44 -07:00
jeremy e447545bc7 chore: rebaseline size and swallowed-error ratchets after the pinned-todos tests landed
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
Release / Create release (push) Has been cancelled
Release / Build (aarch64-unknown-linux-gnu) (push) Has been cancelled
Release / Build (x86_64-unknown-linux-gnu) (push) Has been cancelled
Release / Build (aarch64-apple-darwin) (push) Has been cancelled
Release / Build (x86_64-apple-darwin) (push) Has been cancelled
Release / Build (aarch64-pc-windows-msvc) (push) Has been cancelled
Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Release / Sign and publish Windows assets (push) Has been cancelled
Release / Build (x86_64-unknown-freebsd) (push) Has been cancelled
Release / Finalize release (push) Has been cancelled
2026-07-29 20:07:50 -07:00
jeremy cd8d35ab4b chore: rebaseline size/panic/swallowed-error ratchets after intentional growth
Regenerated with scripts/check_guardrails.sh --fix against a clean checkout
so the release tag passes CI's quality guardrails.
2026-07-29 19:23:15 -07:00
jeremy 37ba22804c ci: name a missing module file directly instead of as a rustfmt error
221159294 declared `mod frame_meter;` and `mod scroll_profile;` in
jcode-desktop2/src/main.rs without committing either file. That breaks rustfmt,
not just the build:

  cargo fmt --all -- --check
  Error writing files: failed to resolve mod `frame_meter`:
    crates/jcode-desktop2/src/frame_meter.rs does not exist

rustfmt is the first step of both the Format job and Quality Guardrails, so this
single omission makes every later gate unreachable, and it reports as a
formatting problem rather than a missing file. That is the same masking dynamic
#662 was filed about.

scripts/check_module_files.py checks that every `mod x;` in a tracked .rs file
resolves, handling `#[path = "..."]` overrides, inline `mod x { ... }` blocks,
and the non-mod.rs parent directory form. It runs in ~0.6s with no compiler, and
is wired in ahead of rustfmt in both CI and check_guardrails.sh so the real cause
is named first.

Scope, stated honestly: this catches the mod-without-file variant only. The
earlier instances of the same habit (c9ccb4f01, 96a4a91ed) landed a reference
without its definition, which only the compiler can catch, and I verified this
script reports clean on both. The variant it does catch is the one worth a
dedicated gate, because it is the one that disables rustfmt and therefore hides
everything else.

Verified: fails on current master naming both missing files, and reports clean
on the v0.62.1 release commit (d9ca2a232) and on c9ccb4f01 / 96a4a91ed~1, so it
is not merely matching everything.

refs #662
2026-07-29 18:54:17 -07:00
jeremy 9cc1f424ff chore(guardrails): refresh baselines after rebase onto current master
Master's perf and tui-style work moved ui_header.rs, dispatch.rs,
ui_frame_metrics.rs and several others past their entries. None of this growth
is from this branch; refreshed so the gates reflect current master.
2026-07-29 18:54:17 -07:00