Commit Graph
919 Commits
Author SHA1 Message Date
BogdanandGitHub bd00b3b3c5 fix(bg): stream session logs with bounded memory (#1762)
* fix(bg): stream session logs with bounded memory

* fix(bg): handle log follow cleanup edge cases

* fix(bg): surface non-follow log read errors

* test(bg): isolate log streaming temp dirs
2026-06-25 09:10:01 +08:00
d32b6f0476 fix(update): stop false "development build" block on npm installs with NODE_ENV=development (#1781)
`/update` and `openclaude update` reported "Auto-update is unavailable for
a development build." even when OpenClaude was correctly installed from npm,
as long as the launching shell had `NODE_ENV=development` exported.

Root cause: `getCurrentInstallationType()` checked `NODE_ENV === 'development'`
as its first branch, before any path-based detection. A user's shell env var
then downgraded a real npm install to 'development', which routed
`resolveUpdateStrategy()` to `{ action: 'blocked', reason: 'development' }`.

Two-part fix:

1. doctorDiagnostic.ts — move the `NODE_ENV === 'development'` check to a
   fallback position after all real-install path markers (bundled mode, local
   npm, npm-global paths, /npm/, /nvm/, `npm config get prefix`). Path
   detection runs first; NODE_ENV only classifies as 'development' when no
   install path matches (i.e. an actual source-tree `bun run dev` run).

2. bin/openclaude — the heap-sizing relaunch previously used
   `fileURLToPath(import.meta.url)`, which resolves symlinks. After relaunch,
   `process.argv[1]` pointed at the real file target (repo path for
   `npm install -g .`, package path inside node_modules for real installs),
   defeating path-based detection. Preserve `process.argv[1]` (the original
   invocation path, e.g. /usr/local/bin/openclaude or nvm bin symlink) so
   npm-global path markers can match correctly.

Verified: `bun run typecheck` passes; `openclaude doctor` now reports
npm-global (not development) with NODE_ENV=development set on a real
npm global install.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 06:52:58 +08:00
BogdanandGitHub 28bbec4948 fix(memory): bound memory-directory scanning work (#1757)
* fix(memory): bound memory-directory scanning work

* fix(memory): harden bounded memory scanning follow-up
2026-06-25 06:39:05 +08:00
euxaristiaandGitHub 701b68c215 fix(query): prevent spurious Windows interruption prompt by passing 'interrupt' reason (#1733)
* fix(query): prevent spurious Windows interruption prompt by passing 'interrupt' reason

* fix(cli): pass 'interrupt' abort reason and add stop-hook regression tests

* test(query): exercise handleStopHooks abort-reason branch directly

Adds focused coverage that imports and runs handleStopHooks() while a
Stop-hook generator is being consumed, asserting abort('interrupt')
suppresses the synthetic '[Request interrupted by user]' message and a
default abort still yields it. Closes jatmn's P2 regression request.
2026-06-25 06:28:57 +08:00
euxaristiaandGitHub 669ecdfa8b fix: prevent recursive debounce infinite loop in team memory sync (#1726)
* fix: prevent recursive debounce infinite loop in team memory sync

* fix: chain cap-reschedule push after in-flight promise instead of running concurrently

* fix: preserve currentPushPromise when replaced during yield point + tests

* fix: clear pending debounce timer in _resetWatcherStateForTesting

* fix(teamMemorySync): prevent concurrent pushes, make clearing identity-safe, and avoid duplicate follow-ups

* fix(teamMemorySync): guard capped follow-up push against suppression; fix test gaps

Addresses jatmn's three P3 findings on #1726.

- watcher.ts: the capped reschedule path queues a serialized follow-up
  executePush() without consulting pushSuppressedReason, whereas schedulePush()
  short-circuits on suppression. If a permanent failure set suppression while
  the in-flight push was running, the queued follow-up fired one redundant,
  identically-failing call. Skip executePush() when pushSuppressedReason is set,
  mirroring schedulePush(). Adds a regression test (mutation-checked: removing
  the guard makes it fail).

- watcher.test.ts: the "resets to 0 when executePush completes" test was
  vacuous — rescheduleCount started at 0 and executePush() never owns that
  reset (onDebounceFire and _resetWatcherStateForTesting do). Renamed to
  "clears pushInProgress when executePush completes" and dropped the misleading
  rescheduleCount assertion; the reset path stays covered by the onDebounceFire
  cap test.

- watcher.test.ts: the top-level mock.module('./index.js') is process-global and
  mock.restore() does not undo it. Follow the spawnCtxAgent.test.ts pattern —
  (re)register the mock in beforeEach and restore the real module in afterEach —
  so it can't silently bleed into a future test that imports teamMemorySync/index.
2026-06-25 06:27:49 +08:00
euxaristiaandGitHub 02d43b6942 fix: surface swallowed error in plan file write (#1725)
* fix: surface swallowed error in plan file write

* test: add regression test for plan file write failure path

* test(ExitPlanModeV2Tool): scope fs mock to single test and pass full call signature

Two CodeRabbit concerns:

1. The global fs/promises mock in beforeAll was leaking into unrelated
   test suites (e.g. loadAgentsDir.test.ts) because mock.module() cannot
   be cleanly undone mid-suite. Move the mock into the single test that
   needs it and call mock.restore() in a finally block so it is torn
   down as soon as the assertion completes.

2. ExitPlanModeV2Tool.call() was invoked with 2 args but the Tool base
   class signature requires 4 (input, context, canUseTool,
   parentMessage). Pass the two extra args so the test typechecks
   against the real signature.

* fix(test): prevent fs/promises mock leak from ExitPlanModeV2Tool test

Addresses jatmn's P2 on #1725.

The previous test used mock.module('fs/promises', ...) in a try/finally
with mock.restore(). On Linux CI the mock was still active when
loadAgentsDir.test.ts ran, causing all five agent-fixture tests to fail
with "write failed" traces pointing back to this file.

Fix: add afterEach(mock.restore()) to guarantee the mock is torn down
after each test, regardless of pass/fail path. Verified locally by
running both test files in the same process — 6/6 pass.

* fix(test): avoid fs/promises mock entirely to prevent CI leak

The afterEach(mock.restore()) approach still leaked the fs/promises mock
into loadAgentsDir.test.ts on Linux CI (CodeRabbit comment at 12:12 EST).
Bun's mock.module on Linux replaces the module cache in a way that
persists across test files in the same process.

Fix: don't mock fs/promises at all. Instead, re-mock plans.js to point
getPlanFilePath at a nonexistent directory, so the REAL fs/promises.writeFile
rejects with ENOENT. Nothing to leak.

Verified: both test files pass in both orderings with --max-concurrency=1.

* Fix race on exiting plan mode by saving plan file before permission updates and add regression tests asserting no side effects on error

* fix(test): completely isolate mock module by using teammate context APIs instead of module mock override

* test(plan-mode): cover the React write-before-permission guard; drop global fs/promises mock

Addresses jatmn's two P2 findings on #1725.

- Extract the plan-file write guard from ExitPlanModePermissionRequest's
  handleResponse into an exported persistPlanFileBeforeExit() helper (jatmn
  suggested extraction for testability). The component path is unchanged: on
  write failure it stays in plan mode (returns early) and queues a
  'plan-save-error' notification. Adds focused tests for both success and
  write-failure (mutation-checked: a helper that swallows the failure fails the
  test). Tests use real filesystem paths — a temp file for success and a path
  with a missing parent dir for failure — so they need no fs/promises mock.

- ExitPlanModeV2Tool.test.ts: replace the process-global mock.module('fs/promises')
  in both write-failure tests with the same real-failing-path mechanism (a plan
  path whose parent dir does not exist → genuine ENOENT). This removes the
  fragile core-module mock jatmn flagged, which mock.restore() cannot undo and
  which could leak into other test files.

* test(plan-mode): assert the specific ENOENT write failure in ExitPlanModeV2Tool tests

Addresses CodeRabbit's review on #1725: the two write-failure tests asserted a
generic rejects.toThrow(), which would pass on any error. Tighten both to
rejects.toThrow(/ENOENT/) so they lock in the intended missing-parent-dir write
failure rather than masking an unrelated error. The post-write side-effect
assertions (persistFileSnapshotIfRemote / writeToMailbox / setAppState not
called) are unchanged.

* test(plan-mode): add rendered guard test for handleResponse write failure

Addresses jatmn's P3 on #1725: the only coverage for the write-before-permission
guard was at the persistPlanFileBeforeExit helper level; there was no test that
handleResponse itself returns early on write failure.

Renders ExitPlanModePermissionRequest (following the MonitorPermissionRequest
harness), points the V2 plan file at a path whose parent dir is missing (real
ENOENT), confirms the first accept option, and asserts the plan-save-error
notification is queued while toolUseConfirm.onAllow / onReject and onDone are NOT
called. Mutation-checked: removing the `if (!saved) return` guard makes it fail,
so a future refactor cannot silently drop the guard.

* fix(test): type addNotification mock so render test typechecks

CodeRabbit (correctly) flagged that `mock(() => {})` infers a zero-arg tuple, so
`call[0]` was TS2493 and `bun run typecheck` (CI) failed. Type the mock param
with the real `Notification` type, which also lets the assertion drop its cast.
2026-06-25 06:25:58 +08:00
euxaristiaandGitHub adcf5e5839 fix(permissions): bound the speculativeChecks cache with FIFO eviction (#1724)
Internal/upstream defensive maintenance: cap the speculativeChecks Map at
MAX_SPECULATIVE_CHECKS_SIZE (1000) and FIFO-evict the oldest entries after each
insert, so the bash-classifier speculative cache can't grow unbounded when the
classifier path is active (it is a stub in the open-source build; this guards
the upstream build where it is reachable).

Pure cache-bounding — no runtime or permission behavior change, and no new env
vars. Adds focused FIFO-eviction regression tests via a small `_test` surface
(mutation-checked: neutering eviction fails them). Rebased onto current main.
2026-06-25 06:25:00 +08:00
0xfandomandGitHub 3fb718f403 fix(worktree): base agent isolation worktree on parent HEAD, not origin/main (#1652)
* fix(worktree): base agent isolation worktree on parent HEAD, not origin/main

createAgentWorktree delegated to getOrCreateWorktree with no base, so the
non-PR path checked out origin/<defaultBranch> whenever that remote-tracking
ref existed locally. An isolated agent (isolation: "worktree") is expected
to see the same committed state as the parent session, but instead got an
older tree and missed files that exist only on the active branch.

Resolve the parent session's HEAD from the session cwd and thread it through
getOrCreateWorktree as a new `baseRef` option (used verbatim, then rev-parsed
to a SHA). Falls back to the prior origin-based behavior when HEAD can't be
resolved (e.g. a repo with no commits). EnterWorktree/PR worktree paths are
unchanged.

Fixes #1586

* test(worktree): make agent-base regression test hermetic; add explicit cwd seam

The integration test went through createAgentWorktree's ambient getCwd(),
which reads process-global cwd state. bun runs test files concurrently in one
process and a sibling test mutates that global cwd, so the agent-base test
raced and failed in the full suite (worktree based on origin/main, missing
the feature-only file).

Add an optional `cwd` to createAgentWorktree, used for both the canonical
git-root and the parent-HEAD lookups (defaults to getCwd()). The test pins it
explicitly so it no longer depends on the raced global cwd. Verified it passes
isolated and in the full src/utils suite, and still fails on the pre-fix code.

* test(worktree): isolate agent-base regression from leaked module mocks

Per review: the regression test imported createAgentWorktree (and thus
worktree.ts's execFileNoThrow.js dependency) at module load, so it could bind
process-global bun mock.module state left by a neighboring suite before
establishing its own isolation.

Hold the shared mutation lock for the whole test so it never runs interleaved
with suites that mock execFileNoThrow.js, and import the worktree module only
after the lock is held, via a cache-busted dynamic import, so the binding
resolves against the real module.

* test(worktree): use execa and a longer timeout in the agent-base regression

Per AGENTS.md, src/utils tests shell out via execa rather than child_process,
so swap the git() helper to execaSync. The test also performs real git work
(init, commit, branch, worktree add) that can exceed Bun's default 5s timeout
on slower/Windows runners, so give it an explicit 15s budget.

* test: stop two suites leaking partial module mocks process-wide

Bun's mock.module is process-global and is not reverted by mock.restore(), so a
partial-surface mock left active by one suite breaks any later suite that
imports the same module — it sees a module missing the un-mocked exports and
fails to link with "named export not found".

- setupGitHubActions.test.ts mocked config.js with only saveGlobalConfig.
  Spread the real surface (as it already does for execFileNoThrow/browser) and
  restore it in afterEach, so config.js keeps its full surface for later suites.
- osc.test.ts mocked execFileNoThrow.js and tempfile.js with partial stubs and
  never restored them. Spread the real surfaces and gate each overridden export
  on an active-suite flag, so when osc's tests aren't running the exports
  delegate to the real implementation instead of leaking stubs.

This unblocks worktree.agentBase.test.ts (which loads worktree.ts → config.js /
execFileNoThrow.js) when it runs after either suite.

* Revert mock-isolation changes that regressed the full test suite

The previous commit reworked the leaking config.js / execFileNoThrow.js mocks
in setupGitHubActions.test.ts and osc.test.ts (and swapped the worktree test
helper to execa). While it fixed the two-file reproductions, it regressed the
full smoke-and-tests run: createAgentWorktree began failing with "not in a git
repository" in the combined suite. Revert to the last green state while a more
robust isolation approach (process-level isolation for the real-git test) is
worked out.

* test(worktree): run the agent-base regression in an isolated process

The previous in-suite isolation attempts couldn't survive the full test run:
createAgentWorktree shells out to git via execFileNoThrow.js, which other
suites mock with `mock.module` — a process-global override Bun cannot reliably
revert, so any leaked stub made the test fail with "not in a git repository"
depending on suite ordering.

Move the actual createAgentWorktree call into a standalone child process
(worktree.agentBase.fixture.ts) that loads only the real modules, and keep the
git repo setup and assertions in the test using real git directly. Nothing the
shared test process mocks can reach the child, so the test is now order- and
leak-independent. Verified passing in isolation, alongside the two leaking
suites that previously broke it, and in the full suite.

* test(worktree): register agent-base fixture as a knip entrypoint

The agent-base regression runs createAgentWorktree in a standalone child
process (worktree.agentBase.fixture.ts) to escape leaked module mocks. knip
sees no static importer for it — it is spawned via execFileSync — and the
deadcode check fails it as an unused file. It is a genuine process
entrypoint, so add the *.fixture.ts glob to knip's entry list.
2026-06-25 06:23:48 +08:00
0xfandomandGitHub 66ddbece19 fix(bridge): match loopback hostname exactly in HTTPS credential guard (#1760)
* fix(bridge): match loopback hostname exactly in HTTPS credential guard

The bridge requires HTTPS for non-localhost base URLs to protect OAuth
credentials in transit, but the guard decided "localhost" via substring
checks (`baseUrl.includes('localhost')` / `.includes('127.0.0.1')`). Any
remote URL that merely contains that text — `http://evil.example.com/localhost`,
`http://evil.localhost.com`, `http://127.0.0.1.evil.com` — slips past the
check and is allowed to carry credentials over plain HTTP.

Extract `isLocalhostBaseUrl`, which parses the URL and matches the hostname
component exactly (mirroring the logic already in `buildSdkUrl`), and route
both guard sites plus `buildSdkUrl` through it. A malformed URL is treated as
non-local so the HTTPS requirement still applies.

Add direct coverage for the helper, including the substring-bypass cases.

* fix(bridge): reject mixed-case HTTP scheme in credential guard

The HTTPS credential guard tested `baseUrl.startsWith('http://')`, which only
matches the exact lowercase scheme. `HTTP://example.com` / `Http://example.com`
slipped past — the URL parser normalizes the scheme to `http:`, so credentials
would still be sent over plaintext.

Fold the scheme test into the parse: add `isInsecureHttpBaseUrl`, which reads
`new URL(...).protocol === 'http:'` (case-normalized) and requires a non-local
host, and route both guard sites through it. Malformed URLs return false.

Extend coverage with mixed-case HTTP/HTTPS cases.
2026-06-24 22:04:05 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
39604fe871 chore(main): release 0.20.0 (#1684)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.20.0
2026-06-24 15:07:30 +08:00
adafde30fa feat(integrations): add GLM 5.2 as an Opengateway-routed model (#1772)
* feat(integrations): add GLM 5.2 as an Opengateway-routed model

Add an `opengateway-glm-5.2` catalog entry (apiName `z-ai/glm-5.2`) to
the gitlawb-opengateway gateway, reusing the existing `glm-5.2` model
descriptor. Routes GLM 5.2 through the credit-billed Opengateway
(OpenRouter upstream) alongside the existing direct Z.AI vendor path.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(model): add z-ai/glm-5.2 to the opengateway picker fixture

The model-picker option-order assertion enumerates the gitlawb-opengateway
catalog; add the new z-ai/glm-5.2 entry (after qwen/qwen3.7-max) so it
matches the catalog.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 15:03:55 +08:00
1b1dfcfdbc Feat/ads sponsored tips (#1674)
* initial commit

* feat(ads): passive per-turn earning tips + mask earn code

- Gitlawb earning tips now appear on a per-turn cadence (default every 2nd
  tip slot, OPENCLAUDE_ADS_TIP_EVERY-tunable) for opt-in users, bypassing the
  per-startup sponsored gate that only ever showed one ad per session.
- Each shown earning tip fetches a real impression and confirms after the
  dwell, crediting opengateway credits; fails silent so ads never break the CLI.
- Mark /ads isSensitive so the earn code is redacted from history.

* feat(ads): mask earn code via paste dialog; never accept it inline

/ads on always opens a masked TextInput dialog (mask="*") — the code is
never typed inline, because the terminal echoes inline args as you type
(isSensitive only redacts history after submission, not the live echo). An
inline /ads on <code> now also opens the dialog and warns that the typed
code is exposed and should be rotated. Converts /ads to a local-jsx command.

* fix(ads): address CodeRabbit review on PR #1674

- ads.ts: hard 5s timeout (AbortController) on both fetchNextTip and confirmTip
  so a stalled connection can never hang the spinner-tip path; the abort timer
  is unref'd. (fetchWithProxyRetry forwards init.signal and treats AbortError as
  non-retryable.)
- gitlawbEarn.ts: unref the best-effort confirm timer so it can't keep a
  short-lived CLI run alive for the dwell window.
- ads.tsx: clear the stored earnCode on `/ads off` (it's a credential, no reason
  to keep it at rest after opt-out); fix the `/ads on` doc comment to match the
  always-masked-dialog behavior.
- ads.test.ts: restore ADS_BASE_URL in afterEach; add submit/cancel coverage for
  the masked dialog (enables + persists code / cancellation message) and assert
  `/ads off` clears the code.
- gitlawbEarn.test.ts: restore ADS_BASE_URL + OPENCLAUDE_ADS_TIP_EVERY in
  afterEach to stop env leaking across suites.
- tipScheduler.test.ts: cover the earning-tip branch precedence in
  getTipToShowOnSpinner (driven via the existing config mock + env, no module
  mock — avoids the bun mock.module cross-file leak).

Testing: tsc clean; tips+ads suite 36 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(ads): contextual sponsored tips — share the latest prompt for ad matching

Switches the earning tips from generic to contextually-matched ads. When the
viewer enables sponsored tips (the /ads on dialog now discloses this), the
client sends their most recent prompt to the ads service, which matches a
relevant ad via Gravity's contextual endpoint.

- types.ts: TipContext gains `latestUserMessage` (opt-in earning tip only).
- REPL.tsx: extract the latest user message at the spinner-tip pick site and
  thread the full TipContext into tip.content() (previously only {theme}).
- gitlawbEarn.ts: pass ctx.latestUserMessage to fetchNextTip.
- ads.ts: new sanitizeForAds() redacts secrets/JWTs/emails/long hex and
  truncates to 500 chars; fetchNextTip POSTs { context:{messages:[user]} } when
  a prompt is present (else GET, identity-only).
- ads.tsx: /ads on dialog + enable message disclose that the recent prompt
  (secrets redacted) is shared with the ad partner — consent folded into enabling.

Privacy: explicit disclosed consent, minimal context (last prompt only),
sanitized client-side (the ads service re-bounds size server-side too).

Testing: tsc clean; new ads.test.ts (7 sanitize cases); ads+tips suite 43 pass;
smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): show the real advertiser + ad click URL in earning tips

renderEarningTip hardcoded the Gitlawb name + gitlawb.com URL, so every served
ad rendered as "Sponsored · Gitlawb — … gitlawb.com" and discarded the actual
ad's `name` and `link`. Pass the served ad's advertiser name and click URL
(Gravity's tracker — required for click attribution/payout) into the renderer;
fall back to Gitlawb only for the static no-ad line.

Testing: tsc clean; tips suite 16 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(ads): hyperlink the advertiser name instead of printing the tracker URL

Sponsored/earning tips render the advertiser as a terminal hyperlink to the
ad's click URL (Gravity's tracker) via a shared renderSponsorLink helper, rather
than printing the long tracker URL inline. Clicks still route through the
tracker, so attribution/payout are unchanged.

Testing: tsc clean; tips/ads suite 46 pass (incl. tipLink); smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): address CodeRabbit review on the contextual ads changes

Security:
- tipLink.ts: sponsor name/url are advertiser-controlled (untrusted). Strip
  C0/C1 control chars from the name and accept only http(s) URLs (parsed via
  URL()), preventing terminal escape-sequence injection and javascript:/file:
  click targets.
- ads.tsx: soften the consent copy to "best-effort secret redaction" (both the
  dialog and the enable message) — sanitizeForAds is heuristic, not absolute.

Correctness:
- ads.ts: clamp dwell_ms to a finite, non-negative integer (malformed values no
  longer yield NaN/Infinity in the confirm-delay math).
- gitlawbEarn.ts: never point a third-party advertiser name at the Gitlawb URL —
  a real ad uses only its own click URL; the Gitlawb fallback is reserved for the
  static no-ad line.

Tests + isolation:
- ads.test.ts: cover fetchNextTip/confirmTip (POST-with-context vs GET, !ok→null,
  ad:null→null, dwell clamp, confirm normalization) via a stubbed fetch.
- tipLink: renderSponsorLink takes an injectable `hyperlinks` flag so both
  branches are deterministically tested; added control-char/unsafe-URL cases.
- gitlawbEarn.test.ts: restore global `ads` config in afterEach.
- tipScheduler.test.ts: preserve OPENCLAUDE_ADS_TIP_EVERY in cleanup.

Note: CodeRabbit's packages/memory/* findings are moot — that stray package was
removed from the branch.

Testing: tsc clean; ads+tips suite 56 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): address CodeRabbit follow-up review (round 2)

- ads.tsx: remove warmOneEarn — it fetched AND confirmed a tip during /ads on
  that the user never saw, crediting an unshown impression (and could confirm
  after a quick opt-out). Earning now happens only on the per-turn rendered-tip
  path. Also drop the unsupported "Run /ads for your balance" instruction.
- ads.ts: normalize confirm amounts (earned_micro/balance_micro) to finite
  integers via toFiniteInt, mirroring the dwell_ms clamp.
- gitlawbEarn.ts: degrade to the static fallback when tip_text is blank, so a
  malformed ad never renders an empty line and credits a blank impression.
- ads.test.ts (commands): restore global ads config in afterEach; assert the
  inline-code exposure warning (warnExposed) is shown.
- ads.test.ts (services): build the AWS-shaped fixture at runtime so it isn't
  flagged by security:pr-scan; add a success-path test for buildEarningTip()
  rendering a fetched ad + a blank-ad fallback test.

Testing: tsc clean; ads+tips suite 58 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): address CodeRabbit full review (round 3) + comments

Blocking:
- ads.ts: gate the serve response on a string `token` (the signed impression),
  not on an `ad` field. A served tip carries NO `ad` key, so CodeRabbit's
  suggested `data.ad == null` would have suppressed every real tip; the empty
  slot (`{ ad: null }`) and malformed responses both lack a token, so the token
  check covers them correctly. Documented the contract.
- gitlawbEarn.ts: drop the dead try/catch around fetchNextTip — it is
  contractually non-throwing (catches everything, returns null), so the wrapper
  was unreachable/misleading.
- ads.ts: make the base64-blob redaction boundary reliable — \b is meaningless
  around + and / (both \W), so bound the run with explicit look-around instead.

Comments / clarity (per review):
- ads.ts: note withAbortTimeout is a per-CALL deadline (shared across retries),
  not per-attempt.
- tipLink.ts: clarify the CONTROL_CHARS_RE comment — the ESC/BEL literals above
  are intentional terminal-sequence constants, not part of the matcher.
- config.ts: document that GlobalConfig.ads is managed via /ads and intentionally
  excluded from GLOBAL_CONFIG_KEYS (earnCode is a credential).

(resetEarningCadenceForTesting export is an accepted in-repo pattern — no change.)

Testing: tsc clean; ads+tips suite 58 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 14:04:03 +08:00
9c0d5c61e2 fix(deps): remove deprecated uuid install path by replacing vertex-sdk with local client (#1771)
* fix(deps): remove deprecated uuid install path

* fix(api): address PR #1232 review — type the local Vertex client surface

Resolves the blocker raised by @Vasanthdev2004, @gnanam1990, and @jatmn: the
in-repo AnthropicVertex replacement compiled under bun (no type-check) but added
4 `tsc --noEmit` errors that the upstream typed SDK did not.

- Declare `messages`/`beta` as typed class fields (BaseAnthropic doesn't, but
  the upstream @anthropic-ai/vertex-sdk client did), so typed consumers —
  client.ts `new AnthropicVertex(...)` and the SDK calling `.messages` — keep
  the resource surface. (vertexClient.ts:145/146, test:53)
- Widen the header-merge helpers to accept the base client's request header type
  (HeadersLike), and handle the NullableHeaders shape it actually passes so the
  merge stays correct, not just type-clean. (vertexClient.ts:182)

Also drops the now-stale `@anthropic-ai/vertex-sdk` entries left behind by the
dependency removal:
- scripts/externals.ts INTENTIONALLY_BUNDLED (P3)
- knip.json ignoreDependencies

Testing: `tsc --noEmit` clean; vertex/client/gemini tests 51 pass; smoke green
(INTENTIONALLY_BUNDLED back in sync, 57 entries); knip clean.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): address CodeRabbit review on PR #1232 — auth precedence + coverage

- Security (vertexClient.ts): merge resolved Google auth headers LAST so a
  caller-supplied Authorization / x-goog-user-project can't override the Vertex
  credential and send the wrong token upstream. Other request headers still
  pass through unchanged.
- Tests (vertexClient.test.ts): add focused regression coverage for the
  previously-unguarded routing/auth branches —
    * streaming → :streamRawPredict path (+ model stripped, stream preserved)
    * count_tokens → count-tokens:rawPredict path rewrite
    * auth-header precedence: caller Authorization does NOT override the Vertex
      token (guards the fix above + exercises the NullableHeaders merge branch).

Testing: tsc clean; vertexClient tests 5 pass; full src/services/api 840 pass;
smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): validate and encode Vertex model before building the URL

CodeRabbit follow-up on PR #1232: `model` was interpolated straight into the
Vertex endpoint path, so a missing/non-string model would silently route to
`.../models/undefined:rawPredict` instead of failing fast. Now throw a clear
error on a missing/empty model and encodeURIComponent the value before building
the path. Adds a focused test for the missing-model case.

Testing: tsc clean; vertexClient tests 6 pass; smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(api): address remaining review items from PR #1232

- [P2] Fix count_tokens method guard: apply method==='post' to both paths
- [P3] Remove unused accessToken option from AnthropicVertex
- [P3] Narrow batches type on messages/beta resources with Omit

* test: add count_tokens?beta=true routing regression test

---------

Co-authored-by: Kevin Codex <kevin@gitlawb.com>
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-24 13:03:29 +08:00
3eb57c6d13 fix: upgrade shell-quote 1.8.3 -> 1.8.4 (CVE-2026-9277) (#1764)
Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-24 09:44:01 +08:00
4dee44a542 feat(ux): long-turn visibility + default-on stream hang safety net (#1758)
Long-running turns looked frozen ("are you still working?") and a dropped
network stream could hang for the full 5-minute QueryGuard idle timeout.
Three focused changes:

- Spinner: show the elapsed-time counter at 5s instead of 30s (split the
  timer gate from the token-count gate). The timer is wall-clock derived,
  so it keeps ticking during long tool calls — proof of life even when no
  tokens stream.
- Spinner: surface the currently-executing tool name in the status line
  (reusing the spinnerSuffix channel; stop-hook progress still wins). A
  long subagent/typecheck now reads "(↓ Bash · 1m 20s)" instead of a bare
  spinner.
- claude.ts: enable the stream idle-timeout watchdog by default, matching
  the always-on read-timeout already used by the OpenAI/Codex shims. A
  silently dropped Anthropic-family stream now aborts and falls back to a
  non-streaming retry within STREAM_IDLE_TIMEOUT_MS (90s) instead of
  hanging to 5 minutes. Opt out with CLAUDE_DISABLE_STREAM_WATCHDOG=1.

Testing: tsc --noEmit clean; bugfixes.test.ts (31) and src/services/api
(835) pass; bun run smoke builds + runs.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 07:27:16 +08:00
bcf9421824 feat(permissions): allow npm/bun/tsc --version as read-only (#1759)
`node --version` and `python --version` are auto-approved as read-only Bash,
but `npm`, `bun`, and `tsc` version queries were missing from the allowlist, so
they fell through to a permission prompt. Add them in the same exact-anchored
form (no trailing args) so a version flag can't smuggle a script-running suffix
past the check (the `node -v --run <task>` class of bypass).

Closes the only read-only gap that the now-superseded #787 classifier covered,
without a parallel classification surface.

Testing: new readOnlyValidation.test.ts (18 cases — allows -v/--version, rejects
install/suffixed forms); tsc clean; BashTool suite 117 pass.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 07:26:57 +08:00
dc6a7781bf feat: auto-detect and persist project conventions to wiki (#1010)
* feat: auto-detect and persist project conventions to wiki

Adds a convention scanner that reads project config files (package.json,
tsconfig.json, eslint, prettier, Dockerfile, CI workflows, lockfiles) on
startup and saves extracted conventions to .openclaude/wiki/pages/conventions.md.
Includes a fingerprint cache to avoid redundant writes and a /wiki scan
command for manual re-scans.

New modules:
- src/services/wiki/conventions.ts — scanner + cache + save
- src/services/wiki/identity.ts — project identity (name, languages, monorepo)
- src/services/wiki/conventions.test.ts — 7 tests

Modified:
- paths/types/init/status — extended wiki infrastructure for conventions
- wiki.tsx/index — added /wiki scan command
- main.tsx — fires scan via startDeferredPrefetches
- init.test.ts/status.test.ts — updated for new conventions page/fields

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(wiki): address PR #1010 review — trust gate, cache, pre-init, indexing

Resolves the three blockers raised by @jatmn and @Vasanthdev2004, plus a
post-rebase integration gap.

1. Trust gate (blocker): the startup convention scan ran git (via project
   identity) before workspace trust was established. Now gated behind the same
   check as prefetchSystemContextIfSafe — runs only when non-interactive
   (implicit trust) or the trust dialog has been accepted.

2. Cache fingerprint (blocker): computeFingerprint hashed only the detected
   config sections, so identity-only changes (project name, language counts,
   monorepo, default branch) left conventions.md stale. Identity inputs are now
   folded into the fingerprint. (Hashing the rendered markdown isn't viable — it
   embeds a "Last scanned" timestamp.)

3. /wiki scan pre-init crash (blocker): forceScanConventions caught the page
   write failure but still wrote the cache, throwing ENOENT before /wiki init.
   It now only writes the cache on a successful page write and reports
   saved=false; /wiki scan surfaces "run /wiki init first".

4. Index integration: the conventions page is written to pages/, but the wiki
   index (rebuildWikiIndex, added after this PR was opened) was never refreshed,
   so the new page wasn't listed. Both save paths now reindex on success.

Testing: 3 new regression tests (identity-only re-save, pre-init no-crash/no-cache,
reindex); wiki suite 14 pass; tsc clean; smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(wiki): address CodeRabbit review on PR #1010

- main.tsx: add a terminal .catch(logError) to the deferred conventions-scan
  promise chain so a failed dynamic import or scan can't become an unhandled
  rejection on the startup path. (Major)
- conventions.ts: narrow both page-write catch blocks to ENOENT (= wiki not
  initialized → skip); rethrow other failures (EACCES, EROFS, …) instead of
  masking them as "not initialized". (Major)
- identity.ts: replace blocking execFileSync('git', …) with async execa
  (the service-layer subprocess convention); getProjectIdentity is now async,
  awaited in scanProjectConventions. Keeps the startup scan off the event loop.
- commands/wiki/index.ts: restore `ingest` in the argumentHint —
  `[init|status|scan|ingest <path>]` — it's still an implemented subcommand.

Testing: tsc clean; wiki suite 14 pass; smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 07:24:58 +08:00
JATMNandGitHub a23014b260 fix(atlas-cloud): sync static catalog with live /models metadata (#1754)
- Enrich every entry with maxOutputTokens (from max_output_length) and
  capabilities (function calling, json mode, vision, reasoning) pulled from
  the live catalog. Addresses discoveryService metadata gap.
- Add transportOverrides.openaiShim.removeBodyFields for xai/grok-build-0.1
  to drop reasoning_effort (fixes 400 on Atlas Cloud).
- Curate current model set:
  - Add: Kimi K2.7 Code, GLM 5.2, Qwen3.7 Max/Plus, Doubao Seed 2.0 variants,
    Claude Sonnet 4.6 / Haiku 4.5 (base + coding), latest GPT/Gemini/Grok.
  - Drop: K2 Thinking/Instruct 0905, older MiniMax M2.1/M2, duplicate Qwen.
- Keep source: 'static' only. Entries sorted in descending version order
  within each vendor family.
- Preserve notes: 'Free' on the owl model.

.gitignore: ignore .tmp-* directories (test artifacts such as replay-index tests).

Follows static-over-hybrid, catalog-model-ordering, and grok-build-0.1 notes.
2026-06-24 07:23:25 +08:00
JATMNandGitHub dd4c4abc81 feat(api): add OpenAI-compatible credential pool failover (#1706)
* feat(api): rotate OpenAI credential pools

* fix(api): align pooled credential discovery

* fix(cache-probe): preserve GitHub credential precedence

* fix(provider): honor pooled OpenAI fallbacks

* fix(provider): validate pooled profile credential labels

* fix(api): harden OpenAI credential pool handling

Reject placeholder values in pooled OpenAI credentials before requests, discovery, diagnostics, and profile generation can use them.

Normalize pooled credentials to a single usable key for model discovery, runtime cache partitions, cache probing, and NVIDIA NIM cache lookups.

Preserve documented profile precedence by letting live shell credentials override saved pools, carrying OpenCode fallback pools through launch, and redacting individual pool members in profile display.

Add regression coverage for pooled credential validation, profile launch/rebuild behavior, discovery/cache callers, diagnostics, provider autodetect, and shim failover semantics.

* fix(provider): cover pooled key recommendation path

Import the pooled OpenAI credential validator in provider-recommend and split invalid credentials from unset credentials in user guidance.

Add a script-level regression that runs the OpenAI recommendation path with OPENAI_API_KEYS so the ts-nocheck script cannot regress with runtime ReferenceErrors.

Scrub pooled OpenAI keys before xAI OAuth profile env construction and loosen the invalid-pool discovery test to assert auth header absence instead of exact header shape.

* fix(tests): stabilize rebased provider checks

* fix(provider): address pooled credential review findings

* test(api): cover opencode go credential failover

* fix(provider): share OpenAI credential usability checks

* fix(provider): respect pooled credential precedence

* fix(model): preserve pooled discovery credential precedence

* fix(model): fall back from unusable pooled discovery keys
2026-06-23 12:34:55 +08:00
c2467eedad feat(atlas-cloud): add GLM 5.2 to vendor catalog (#1755)
Adds zai-org/glm-5.2 with the same 202,752 context window as GLM 5.1
so the model is selectable through the Atlas Cloud provider.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-23 10:06:30 +08:00
euxaristiaandGitHub 38b0e27333 fix(opencode-go): sync model catalog with opencode.ai/go (#1745)
* fix(opencode-go): sync model catalog with opencode.ai/go

The OpenCode Go subscription page (https://opencode.ai/go) lists 13
models, but the catalog had 20. Remove the 7 models no longer offered:
glm-5, kimi-k2.5, minimax-m2.5, qwen3.5-plus, mimo-v2-pro,
mimo-v2-omni, hy3-preview.

Catalog now matches the page exactly:
- OpenAI-compatible: GLM 5.2, GLM 5.1, Kimi K2.7 Code, Kimi K2.6,
  DeepSeek V4 Pro, DeepSeek V4 Flash, MiMo V2.5 Pro, MiMo V2.5
- Anthropic messages: MiniMax M3, MiniMax M2.7, Qwen3.7 Max,
  Qwen3.7 Plus, Qwen3.6 Plus

Updates gateway catalog, model descriptors, generated artifacts, and
tests.

* fix(opencode-go): reorder model catalog to match opencode.ai/go listing

Reorder both the gateway catalog and model descriptor lists to match the
order models appear on https://opencode.ai/go, so the in-app model picker
mirrors the subscription page. No model added or removed — purely a
reorder.

GLM-5.2 → Qwen3.7 Max → Kimi K2.7 Code → MiMo V2.5 Pro → DeepSeek V4 Pro
→ Qwen3.7 Plus → MiniMax M3 → MiMo V2.5 → DeepSeek V4 Flash → GLM 5.1
→ Kimi K2.6 → Qwen3.6 Plus → MiniMax M2.7

* test(opencode-go): assert exact model set matches opencode.ai/go catalog

Address CodeRabbit review on #1745 — the count-only test wouldn't catch
catalog drift. Add a strict set assertion verifying the 13 expected IDs
are present and no removed/unexpected IDs remain.

* test(opencode-go): update Anthropic Messages route test for refreshed catalog

The direct-env-routing test listed minimax-m2.5 and qwen3.5-plus, which
were removed from the opencode-go catalog. Replace with the five
/messages-endpoint models that remain: minimax-m3, minimax-m2.7,
qwen3.7-max, qwen3.7-plus, qwen3.6-plus.

Unblocks the smoke-and-tests CI check on #1745.

* fix: update OpenCode Go model references to 13 models and add assertions
2026-06-23 09:30:08 +08:00
BogdanandGitHub 820015fbaf fix(bg): prefer exact session names over ID prefixes (#1753) 2026-06-23 07:53:29 +08:00
JATMNandGitHub 091571f643 fix(provider): Add Xiaomi MiMo token plan provider (#1751)
* Add Xiaomi MiMo token plan provider

Add a descriptor-backed Xiaomi MiMo Token Plan gateway with the token-plan SGP/CN routing hosts, raw api-key OpenAI-compatible shim settings, and MiMo catalog defaults.

Wire the new preset through generated integration artifacts, provider flag handling, active route/provider resolution, provider profile env mirroring, and ProviderManager preset ordering.

Cover the new path with gateway metadata, compatibility, provider flag, provider profile, and focused ProviderManager tests. Validated with bun run build, bun run smoke, bun run check, typechecks, provider suites, integrations:check, security:pr-scan -- --base upstream/main, and doctor:runtime.

* Address Xiaomi MiMo token plan review feedback

Reset stale known OpenAI-compatible base URLs when xiaomi-mimo-token is explicitly selected so prior provider routing cannot survive the provider flag.

Use hostname-based Xiaomi MiMo base URL detection when mirroring MIMO_API_KEY from provider profiles, avoiding substring matches in unrelated URL paths or query strings.

Add focused regression coverage for stale base URL replacement and token-plan CN profile env mirroring.
2026-06-23 07:52:33 +08:00
euxaristiaandGitHub aed42df19b fix: treat 5xx HTML overload pages as retryable provider_unavailable (#1750)
Reorders classifyOpenAIHttpFailure so the status >= 500 branch runs before
the isMalformedProviderResponse check. Gateway 502/504 overload pages have
HTML bodies (matching "<!doctype html" / "<html") that previously tripped
the malformed-provider-response path, marking the error non-retryable and
surfacing "Provider returned a malformed response" even though the failure
was transient — users had to retry manually.

Now any 5xx is classified as provider_unavailable (retryable) regardless of
body shape, matching the actual semantics. 4xx HTML responses still classify
as malformed_provider_response since those are genuine protocol failures.

Adds three regression tests covering 502 HTML, 504 HTML, and the unchanged
400 HTML path.
2026-06-23 07:51:10 +08:00
0xfandomandGitHub 8c15feaa5a test(install-surfaces): stop execFileNoThrow mock leaking into later suites (#1708)
The cleanupNpmInstallations test replaces execFileNoThrowWithCwd with a stub
that simulates a failed npm uninstall (E404). bun's mock.module is
process-wide, and re-mocking the module back to the real implementation in
afterEach does not reliably undo it, so the stub leaked into later test files
that shell out for real (e.g. git worktree add), making them fail with a bogus
"npm ERR! code E404".

Install the override once at module load and gate it on a module-level flag
that is cleared in afterEach, so the persisted mock transparently falls through
to the real implementation whenever the flag is off. The real module is
snapshotted into a plain object before the mock is installed, because bun
live-updates the imported namespace to the mock — delegating through the
namespace inside the override would otherwise recurse infinitely.
2026-06-23 07:50:32 +08:00
JATMNandGitHub 1be9b86607 feat: Add session replay timeline (#1705)
* Add session replay timeline

Implement a /replay command backed by replay sidecar indexes that capture user requests, tool execution inputs/results, modified files, durations, real retry events, and repeated tool attempts.

Add replay summaries to /resume, persist replay indexes during session cleanup, and include replay sidecars in retention cleanup.

Add focused coverage for replay index retry/repeated-attempt accounting and replay modified-file detection, including simulated Bash sed edits.

Validation run: bun install; bun run build; bun run smoke; bun run typecheck; bun run typecheck:type-tests; bun run security:pr-scan; bun run doctor:runtime; bun test src/utils/replayIndexBuilder.test.ts; bun test src/services/tools/toolExecution.test.ts. bun run check reached test:full but the full suite still reports 10 unrelated order/environment-sensitive failures in export, marketplace cache, and secure-storage tests; those files pass when rerun directly.

* Address replay review findings

Move replay tool-start tracking to the final executable input for allowed tool calls while preserving permission-denied replay records.

Harden replay sidecar persistence with session-id based paths and owner-only file mode, and compute replay summary timestamps from true min/max values.

Reuse the shared SessionSummary component in /replay and add focused regression coverage for replay storage, timestamp bounds, replay lifecycle records, and resume summary confirmation keyboard behavior.

Validation: bun run typecheck; bun run build; bun test src/utils/replayIndexBuilder.test.ts src/utils/replayIndex.test.ts src/commands/resume/resume.test.tsx src/services/tools/toolExecution.test.ts.

* Resolve replay follow-up findings

Use the shared formatDuration helper in replay summary UI, replace replay timeline non-null assertions with optional chaining, and align the resume Escape test with the single-escape input pattern plus async polling.

Validation: bun run typecheck; bun run build; bun test src/commands/resume/resume.test.tsx src/utils/replayIndexBuilder.test.ts src/utils/replayIndex.test.ts src/services/tools/toolExecution.test.ts.

* Fix replay timing and cancellation status

Preserve millisecond precision for sub-second replay displays, classify abort-shaped tool failures as cancelled in replay records, and derive replay summary duration from elapsed session bounds instead of summed tool runtime.

Validation: bun run typecheck; bun run build; bun test src/utils/replayFormat.test.ts src/utils/replayIndexBuilder.test.ts src/utils/replayIndex.test.ts src/commands/resume/resume.test.tsx src/services/tools/toolExecution.test.ts.

* Normalize denied replay tool inputs

Use the same replay input normalization for permission-denied file tools that the allowed execution path uses, preserving model-emitted relative paths and repeated-attempt signatures across denied-then-allowed retries.

Validation: bun run typecheck; bun run build; bun test src/services/tools/toolExecution.test.ts src/utils/replayFormat.test.ts src/utils/replayIndexBuilder.test.ts src/utils/replayIndex.test.ts src/commands/resume/resume.test.tsx.

* Address replay review follow-ups

* Fix replay follow-up test findings

* Stabilize replay cleanup test

* Address replay resume follow-ups

* Harden replay resume validation

* Address replay review coverage gaps

* Close remaining replay review findings

* Fix replay review follow-ups after rebase
2026-06-22 19:28:39 +08:00
JATMNandGitHub 5625f4217d fix: preserve provider route context metadata (#1741)
* fix: preserve provider route context metadata

Resolve issue #1732 by keeping active provider-profile routes attached during context limit resolution and making gateway-prefixed model IDs resolve against provider-scoped metadata instead of falling through to global aliases.

Preserve composite provider-path suffixes such as accounts/fireworks/models/... and fireworks/models/... before generic last-segment matching, so wrapped gateway IDs resolve Fireworks-specific limits instead of generic descriptors.

Refresh OpenCode Zen and OpenCode Go catalog metadata, including route-specific context/output limits, regenerate integration artifacts, add DeepSeek V4 Pro on NVIDIA NIM, add the Gemini 3.1 Pro router alias, and update user-facing OpenCode model counts.

Scope OpenCode descriptor default model names to OpenCode routes via providerModelMap so unprefixed vendor lookups are not hijacked by gateway descriptors. Add wrapper-path assertions for the user-facing max output token helper.

Add regression coverage for provider-prefixed gateway models, active-profile route preservation, account-qualified composite paths, and OpenRouter-wrapped fireworks/models/... paths. Harden Windows/full-suite validation by normalizing plugin hook display paths and resetting status-redaction HOME/USERPROFILE state.

Validation: bun install; bun run build; bun run smoke; bun run typecheck; bun run typecheck:type-tests; bun run check (4634 pass, 0 fail); bun run test:provider (857 pass, 0 fail); bun run test:provider-recommendation (91 pass, 0 fail); bun run integrations:check; bun run security:pr-scan -- --base upstream/main; git diff --check. Follow-up validation: bun test src/integrations/runtimeMetadata.test.ts --max-concurrency=1; bun test src/utils/context.test.ts src/integrations/runtimeMetadata.test.ts src/integrations/gateways/opencode.test.ts --max-concurrency=1; bun run test:provider; bun run integrations:check; bun run typecheck; git diff --check.

# Conflicts:
#	src/integrations/gateways/opencode-go.ts
#	src/integrations/models/opencode.ts

* fix: align OpenCode context metadata

Refresh OpenCode Zen and Go descriptor context/output limits against the live OpenCode model lists and Models.dev provider metadata.

Add a regression assertion for provider-specific OpenCode limits so route-scoped metadata does not fall back to generic model budgets.

* fix: remove duplicate Gemini model descriptor

Keep the canonical Gemini 3.1 Pro descriptor and rely on provider-prefixed suffix matching for google/gemini-3.1-pro runtime lookups.

Also separate the OpenCode limit regression test from the following assertion for readability.

* fix: preserve OpenCode Go messages auth metadata

* test: cover OpenCode Go review cases
2026-06-22 13:35:31 +08:00
Menno van RahdenandGitHub ae66c30df5 feat(fireworks): add GLM-5.2 model support (#1728)
* feat(fireworks): add GLM-5.2 model support

Add the Fireworks-hosted glm-5p2 model to both the vendor catalog
and the merged model descriptor with 1M context window.

* fix(fireworks): add glm-5p2 to brand modelIds list

* fix(fireworks): use published 1040k context length for GLM-5.2
2026-06-22 09:21:58 +08:00
JATMNandGitHub ab94a50a1a fix(mcp): support draft 2020-12 tool schemas (#1740)
Resolve issue #1739 by selecting the AJV dialect from MCP tool input schemas before compiling validators. MCP input schemas default to JSON Schema Draft 2020-12, including when $schema is omitted, while explicit Draft-07 schemas continue to use the legacy validator. This fixes Playwright MCP schemas such as browser_tabs, which default AJV rejected with no schema with key or ref.

Add MCPTool regression coverage for Playwright-style Draft 2020-12 schemas, omitted-$schema Draft 2020-12 tuple keywords, and explicit Draft-07 compatibility.
2026-06-22 09:16:01 +08:00
Muhammet AlanandGitHub 0e1fce45ca Fix OpenCode Go messages authentication (#1717)
* fix opencode go open model endpoints

* fix(opencode): use x-api-key for Go messages

* fix(opencode): keep Go x-api-key over stale auth

A global OPENAI_AUTH_HEADER left over from another OpenAI-compatible
route took precedence over the model-level x-api-key in
_doOpenAIRequest, so OpenCode Go /messages models still sent
Authorization: Bearer and hit the 401 this PR fixes. Ignore the global
custom-auth env when the selected model's catalog entry defines an
openaiShim.defaultAuthHeader, so the model-level x-api-key contract
cannot be bypassed. Adds a stale-custom-auth regression for all five
models via the direct-env path.
2026-06-22 09:13:21 +08:00
0xfandomandGitHub 059ec5e8b0 fix(code-indexing): guard command detection against prototype-chain names (#1710)
detectCodeIndexingFromCommand looked up the command's first word in a plain
object (CLI_COMMAND_MAPPING). A command whose first word collides with an
inherited Object.prototype member resolved to the prototype value instead of
undefined: the bare lookup returned the Object constructor for `constructor`,
and the npx/bunx branch used `in`, which walks the prototype chain. Both
falsely reported a code-indexing tool, so running e.g. `constructor ...`
emitted a bogus tengu_code_indexing_tool_used telemetry event whose `tool`
field was a function.

Switch CLI_COMMAND_MAPPING to a Map and look up via .get(), so unknown and
inherited keys both return undefined.
2026-06-22 09:03:53 +08:00
0xfandomandGitHub 6c7d147387 fix(model): preserve [1m] tag for the codex aliases (#1709)
parseUserSpecifiedModel maps the codexplan/codexspark aliases to their gpt
model ids but, unlike every Claude alias (opus/sonnet/haiku/best), dropped the
trailing [1m] tag. That suffix is an explicit client-side opt-in to the 1M
context window (has1mContext returns 1_000_000 whenever it is present,
regardless of model family), so codexplan[1m]/codexspark[1m] silently resolved
to a non-1M model and the session fell back to the model default window.

Append the tag the same way the other aliases do so the opt-in survives. The
bare aliases are unchanged.
2026-06-22 09:03:23 +08:00
4aec353f9c fix(grep): relativize content-mode paths correctly on Windows (#1704)
* fix(grep): relativize content-mode paths correctly on Windows

Grep output_mode "content" split each line at the first colon to separate path
from content, but a Windows absolute path starts with a drive-letter colon
(C:), so it split at "C" and reassembled the original absolute path — defeating
relativization (count and files_with_matches modes were already correct). Skip
a leading drive-letter colon when locating the boundary. Extracts a pure
relativizeContentLine helper with cross-platform tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(grep): relativize Windows context rows, not just match rows

ripgrep separates the path with `:` on match rows and `-` on context rows
(-A/-B/-C). The helper only looked for a boundary colon, so Windows context
rows like `C:\...\file.ts-1-before` kept their absolute path. Locate the
boundary as the first `:<n>:` (match; unambiguous since paths have no
non-drive colon) else the first `-<n>-` (context), falling back to the first
colon for line-number-less rows. This also stops date-like `-2024-` runs in
filenames from being mistaken for the context boundary. Adds tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(grep): relativize content rows by stripping the known search root

Review follow-up: the previous delimiter heuristic chose the first `-<n>-`, which
mis-split rows when the cwd or an ancestor directory contained a date-like
segment (e.g. C:\Users\proj-2024-01-15\...), and left line-number-disabled
context rows (`path-content`) with absolute paths. Strip the known absolute root
prefix instead: every ripgrep path under the root starts with `<root><sep>`, so
removing it yields the relative path + the original delimiter + content verbatim,
independent of the delimiter or whether line numbers are enabled. Paths outside
the root stay absolute, matching toRelativePath. Rewrites the tests, including
the date-cwd and no-line-number context-row regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(grep): compare the root with Windows path semantics (case/slash-insensitive)

Review follow-up: stripping the root used a literal startsWith, so when getCwd()
and ripgrep spelled the same Windows root with different casing or slash style
(e.g. C:\USERS\PROJ vs C:\Users\proj), the prefix did not match and absolute
paths leaked. Normalize the comparison for Windows roots (lowercase + treat `/`
as `\`) while slicing the original line by prefix length, mirroring
toRelativePath's case-insensitive path.win32 behavior. Adds casing/slash regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:48:53 +08:00
f5041e4d46 fix(format): roll formatFileSize over to the next unit at the 1024 boundary (#1703)
* fix(format): roll formatFileSize over to the next unit at the 1024 boundary

formatFileSize selected the unit from the unrounded magnitude (kb < 1024) but
displayed the rounded value, so sizes just under a boundary rendered as
"1024KB"/"1024MB" instead of "1MB"/"1GB" (e.g. 1048575 bytes -> "1024KB").
Compare the rounded magnitude when choosing the unit so it promotes correctly.
Adds format.test.ts covering the boundary bands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(format): assert formatFileSize(1023) renders as raw bytes

The sub-KB test labelled "raw bytes" expected formatFileSize(1023) to be
"1KB", but the implementation returns "1023 bytes" for values below the
1024-byte threshold, so the focused test (and the smoke-and-tests check) was
red. Correct the expectation to "1023 bytes".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:32:15 +08:00
ba85aa6dd0 fix(frontmatter): expand nested brace globs in paths: correctly (#1701)
* fix(frontmatter): expand nested brace globs in paths: correctly

expandBraces used the regex `^([^{]*)\{([^}]+)\}(.*)$`, whose `[^}]+` stops
at the first `}`, so nested brace groups were corrupted: `{a,{b,c}}` became
`["a}","b","c}"]` and `src/**/*.{js,{ts,tsx}}` produced stray `}` and broken
globs — silently breaking path-scoped skill / CLAUDE.md activation. Replace
the regex with a depth-aware scan that finds the matching close brace and
splits on top-level commas only, recursing as before. Unbalanced braces fall
back to the literal input. Adds frontmatterParser.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(frontmatter): keep an empty brace group `{}` literal in paths

The balance-aware scanner expanded `{}` to a single empty alternative, so
`paths: "{}"` collapsed to [""]; parseSkillPaths and the CLAUDE.md path
parser drop that empty string and treat the file as having no path
restriction (activating everywhere). The previous regex required >=1 inner
char, so `{}` stayed literal. Restore that: treat an empty brace group as
literal while still expanding any later groups in the suffix. Adds tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:24:54 +08:00
BogdanandGitHub 23bc49a01d feat(query): add lifecycle identity and terminal reasons (#1682)
* feat(query): add lifecycle identity and terminal reasons

* fix(query): isolate lifecycle tracking context

* fix(query): guard lifecycle metadata updates

* fix(query): track lifecycle during tool waits

* test(query): cover bash lifecycle metadata

* fix(query): scope lifecycle tracking to request attempts

* fix(query): disambiguate lifecycle abort log reason

* fix(query): preserve foreground subagent lifecycle tracking

* fix(query): clean up timeout and fallback lifecycle events

* fix(query): emit timeout end after cleanup
2026-06-22 08:22:57 +08:00
BogdanandGitHub b9a5030b67 fix(status): show active provider route instead of legacy provider bucket (#1673)
* fix(status): show active provider route instead of legacy bucket

The /status command collapsed many concrete providers (OpenRouter, Groq,
Ollama, Fireworks AI, etc.) into a single "OpenAI-compatible" label, making
multi-provider setups hard to verify and debug.

When apiProvider resolves to the generic "openai" bucket, /status now uses
route metadata to surface the real active route:
  Provider route: OpenRouter
  Transport: OpenAI-compatible API
  OpenAI base URL: https://openrouter.ai/api/v1
  Model: anthropic/claude-sonnet-4.5
  Credential: OPENROUTER_API_KEY configured

The legacy "OpenAI-compatible" label and fallback are preserved for unknown
custom base URLs. Dedicated provider buckets (nvidia-nim, minimax, codex,
github, xai, gemini, bedrock, vertex, foundry, firstParty, mistral) already
have accurate labels and are left untouched.

Credential display uses env-var names only (never values). Transport kind and
route label come from the existing descriptor-driven route metadata; no new
hardcoded provider maps or network calls are introduced.

* fix(status): include route status defaults

* fix(status): address route status review findings

* fix(status): cover route secret redaction review

* fix(status): avoid duplicate route resolution

* fix(status): redact base URL query credentials

* fix(status): harden status URL secret redaction

* fix(status): redact route secrets in status text

* test(status): cover fallback URL fragment redaction

* test(status): isolate route status provider imports

* fix(status): redact encoded route secrets

* fix(status): redact encoded query secrets safely

* fix(status): redact nested encoded query secrets

* fix(status): redact encoded secret substrings

* fix(status): redact strict encoded secret variants
2026-06-22 08:17:56 +08:00
SkyandGitHub 02ee7c63e9 fix: type safety, defensive defaults, and unbounded retry prevention (#1553)
* fix: type safety, defensive defaults, and unbounded retry prevention

QueryEngine.ts:
- Import PERMISSION_MODES runtime constant and validate permissionMode
  before casting in submitMessage — invalid mode strings fall back to
  'default' instead of crashing with ReferenceError: PERMISSION_MODES is
  not defined (fixes the runtime gap from the original PR)
- Use splice(0, length, ...messages) instead of length=0 + push() for
  atomic array replacement in snip replay, so concurrent readers of
  getMessages() never observe an empty state

withRetry.ts:
- Cap persistent retry loop at 100 attempts via PERSISTENT_RETRY_MAX_ATTEMPTS
  constant — prevents unbounded retry (~8 hours max with exponential backoff
  and 6-hour reset cap) when the unattended retry path is enabled

autoCompact.ts:
- Add MIN_AUTOCOMPACT_FAILURE_COOLDOWN_MS = 10_000 floor for
  OPENCLAUDE_AUTOCOMPACT_FAILURE_COOLDOWN_MS override — prevents
  misconfiguration from effectively disabling the circuit breaker

autoCompact.test.ts:
- Update test override from 5000 to 15000 to respect the new 10s minimum floor
- Add test case verifying values below the floor (5000, 9999) are rejected
  and that the floor value (10000) is accepted
- Update circuit breaker retry-time expectation from 111_000 to 121_000
  to account for the new 15s cooldown override

* test: enable UNATTENDED_RETRY feature in bun test scripts

The new persistent retry cap test in withRetry.test.ts needs the real
UNATTENDED_RETRY feature gate to fire, which requires passing
--feature=UNATTENDED_RETRY on the bun test command line. Enable it
in the standard test, test:full, test:coverage, and test:provider
scripts so the test sees the production gate behavior.

* test: cover persistent retry cap driven through real gate

Add a regression test that proves the persistent retry path stops
after PERSISTENT_MAX_ATTEMPTS=100 retryable 429s by driving the real
isPersistentRetryEnabled() gate (no test override seam). Also:

- Switch makeError to the new APIError() constructor so the test
  errors match the real wire shape and exercise the production
  canRetry/shouldRetry branches
- Add CLAUDE_CODE_UNATTENDED_RETRY to the envKeys clear list so the
  gate isn't poisoned by leaked state from a prior test
- Mock src/utils/sleep.js in importFreshWithRetryModule so the
  exponential-backoff delays don't slow the suite down

* test: defensively clear leaked env vars in client.test.ts

The 4 failing tests in CI (first-party Anthropic fetch wrapper, env-only
MiniMax routing, OPENAI_MODEL preservation, OpenAI shim options) all sit
at the top of the file and are sensitive to leaked env vars from prior
test files in the same process. Extend the beforeEach, afterEach, and
inline cleanup to clear OPENAI_AUTH_HEADER, OPENAI_AUTH_SCHEME,
OPENAI_AUTH_HEADER_VALUE, MIMO_API_KEY, VENICE_API_KEY, and
NVIDIA_API_KEY alongside the existing vars, and add ANTHROPIC_API_KEY /
ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL to the first test's inline
cleanup so it does not rely solely on the global beforeEach when run
in isolation.

* test: isolate countMcpToolTokens tests from mcp.ts env-var side effect

src/entrypoints/mcp.ts sets CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=true as
a top-level side effect on import. mcp.test.ts imports that file, so the
env var is leaked into the rest of the test process. In
analyzeContext.mcp.test.ts the kill switch forces getToolSearchMode() to
'standard', which makes isToolSearchEnabled() return false, so the
'keeps deferred MCP schemas excluded' test sees isDeferred=false and
returns mcpToolTokens=1000 instead of 0.

Clear the kill switch and ENABLE_TOOL_SEARCH in beforeEach (restoring
the original values in afterEach) so the test observes the production
default of tool search enabled regardless of test ordering.

* fix: validate against EXTERNAL_PERMISSION_MODES; confine persistent retry override to test-only module var

QueryEngine.ts:
- Switch SDK init permissionMode validation from the internal
  PERMISSION_MODES set to EXTERNAL_PERMISSION_MODES. The internal
  set can include the classifier-only 'auto' mode that is not a
  valid wire value for the SDK system/init payload, so emitting
  it would produce an unsupported configuration.

withRetry.ts:
- Rename PERSISTENT_RETRY_MAX_ATTEMPTS to PERSISTENT_MAX_ATTEMPTS
  to match the convention of the other PERSISTENT_* constants and
  re-export it as _PERSISTENT_MAX_ATTEMPTS_FOR_TEST for unit-test
  assertion of the cap value (no runtime override seam).
- Hoist isPersistentRetryEnabled() into a local
  persistentRetryEnabled const at the top of withRetry() so all
  call sites see a consistent snapshot within a single retry
  chain.
- Thread persistentRetryEnabled through shouldRetry() so its
  branch is decided once per chain rather than re-reading the
  feature flag and env var on every attempt.

* feat: emit telemetry when persistent retry cap is reached

* fix: surface changelog cache-write failures in migration

- Only swallow EEXIST (file already exists) errors in migrateChangelogFromConfig
- Rethrow all other write failures (permissions, disk full, etc.)
- Log migration errors instead of silently ignoring them
- This ensures migration failures are surfaced and will retry on next startup

* fix: split mkdir and writeFile in changelog migration

- Ensure mkdir runs before writeFile try/catch
- Only suppress EEXIST from writeFile, not mkdir
- Prevents EEXIST from mkdir incorrectly counting as successful write

* fix: stop overriding getGlobalConfig in user.test.ts mock

* Fix leftover conflict marker in QueryEngine.ts

* fix: remove stale retry guard and handle mkdir EEXIST

- Remove duplicate shouldRetry() call without persistentRetryEnabled arg in withRetry.ts
- Wrap mkdir in try-catch to handle EEXIST on Windows/Bun readonly folders in releaseNotes.ts

* test: don't restore OPENAI auth header env vars in afterEach

These vars were being restored from originalEnv which captures polluted values
from prior test files in the full suite. Removing the restoreEnv calls keeps
them cleared between tests, fixing 'Could not resolve authentication method'
failures in CI smoke-and-tests.

* fix: remove duplicate OPENAI_AUTH_* keys in originalEnv (TS1117)

* fix: restore OPENAI auth header snapshot and move MCP lock to top-level

- client.test.ts: restore OPENAI_AUTH_HEADER/SCHEME/HEADER_VALUE in afterEach
  so the file doesn't permanently clear those globals in its worker
- analyzeContext.mcp.test.ts: move acquireSharedMutationLock/release to the
  top-level beforeEach/afterEach so all env mutations in this file happen
  while the shared mutation lock is held

* fix: use splice for atomic array replacement in snip replay

Restores the atomic array replacement using splice(0, length, ...messages)
instead of length=0 + push(...) that was claimed in commit 4d54d5d but
lost during merge. This ensures concurrent readers of getMessages() never
observe an empty mutableMessages array during snip replay, matching the
compact_boundary behavior.

* ci: add UNATTENDED_RETRY feature flag to release workflow test command

The persistent retry cap test requires the UNATTENDED_RETRY feature flag
to be enabled. The release workflow was running 'bun test --max-concurrency=1'
without the feature flag, causing the test to fail on the release path.

This aligns the release workflow with the package.json test scripts which
all include --feature=UNATTENDED_RETRY.

* fix: clear auth env vars in shared setup; add telemetry at persistent retry cap

- Remove duplicate OPENAI_AUTH_HEADER/SCHEME/VALUE deletes from
  clearEnvForMiniMaxOnlyTest() (shared beforeEach already clears them)
- Clarify persistent retry cap comment: the ~8h estimate only applies to
  the exponential-backoff path; the reset-delay path (up to
  PERSISTENT_RESET_CAP_MS / 6h per attempt) can take far longer
- Telemetry event at retry cap already present from prior commit

* fix: make persistent retry cap test pass without --feature=UNATTENDED_RETRY

Updated test to account for feature flag behavior in retry logic.

* fix: normalize REPL bridge permissionMode against EXTERNAL_PERMISSION_MODES

* fix: export isPersistentRetryEnabled for test-side feature-gate assertion

* fix: use isPersistentRetryEnabled() as real feature gate in retry cap test

Refactor withRetry test to include isPersistentRetryEnabled check and update expected calls logic.

* fix: restore missing retryableRateLimit declaration in persistent retry test

Refactor runRetries function for clarity.
2026-06-22 08:16:49 +08:00
SkyandGitHub 7d130e73ba perf: eliminate response.clone() memory doubling and cache lazy tool getters (#1478)
* perf: eliminate response.clone() memory doubling and cache lazy tool getters

openaiShim.ts:
- Replace response.clone() with response.text() + JSON.parse() + new Response()
  for non-streaming usage extraction — avoids doubling memory for large responses

tools.ts:
- Cache getSendMessageTool(), getTeamCreateTool/DeleteTool(), getPowerShellTool()
  results in local IIFEs — avoids double-invocation of lazy require() getters

* test: preserve openai shim response body on parse failure

* fix(openai-shim): preserve response.url routing metadata and fix regression test

Addresses PR review feedback (two P2s).

1. Preserve response.url and response.type when recreating the Response
   after reading the body for usage extraction. new Response(bodyText)
   drops url to empty string, which broke create()'s /responses,
   /messages, and Gemini routing — descriptor routes (OpenCode
   /messages, Gemini /models/gemini-*) fell through to the generic
   OpenAI converter and returned the wrong message shape. Restore the
   original metadata via Object.defineProperty (shadowing the read-only
   prototype getter), guarded by try/catch for runtime safety.

2. Fix the flaky 'preserves response body when usage parsing fails'
   test. The original mock threw on the first global JSON.parse call
   and asserted parseCalls > 1, but Bun's native Response.json() does
   not go through JS-level JSON.parse, so parseCalls stayed at 1 and
   the assertion failed. Rewrite to scope the failure to the response
   body text and assert usageParseFailed + content correctness instead,
   which works in both Bun (native Response.json) and Node (undici).

3. Add 'preserves response.url routing metadata after body read' test
   that pins an Anthropic-shaped body behind a /messages URL — fails
   without the url fix (content becomes []), passes with it.

* fix(typecheck): use 'as unknown as FetchType' to satisfy TS2352
2026-06-22 08:14:11 +08:00
BogdanandGitHub b581bd9ece feat(zai): add GLM-5.2 support (#1689)
* feat(zai): add GLM-5.2 thinking support

* fix(provider): derive GHE Copilot URL from base URL

* fix(zai): gate GLM reasoning effort by model
2026-06-19 22:58:46 +08:00
c4aa756689 feat(commands): add /update command with package-manager auto-detection (#1687)
Adds a `/update` slash command that updates OpenClaude to the latest
published version, routing by how the running install is actually
managed so it updates the installation the user is running.

`globalPackageManager.ts` detects the owning package manager (npm,
yarn, pnpm, bun) for npm-style installs and maps it to the correct
global-install command. `installGlobalPackage()` and `getLatestVersion()`
now consume it, so the legacy `openclaude update` CLI and the background
auto-updater gain yarn/pnpm support; `getLatestVersion()` also falls
back to a direct npm-registry HTTP lookup when npm isn't on the PATH.

`updateStrategy.ts` factors the install-type routing and the
third-party-build guard out of `src/cli/update.ts` (now shared by both
entrypoints). `/update` uses it to refuse development/third-party builds,
point package-manager/native/local installs at their safe update paths,
and only do a global npm install when that's what's actually running —
instead of always installing a stray global package.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-18 09:50:12 +08:00
BogdanandGitHub cc385a6490 fix(ink): reduce high-write-ratio diagnostic noise (#1699)
* fix(ink): reduce high-write-ratio diagnostic noise

* test(ink): cover high-write diagnostic suppression

* fix(ink): preserve churn warnings at suspicious widths
2026-06-18 09:13:53 +08:00
BogdanandGitHub 8cd463383d fix(lsp): throttle diagnostic storms (#1698)
* fix(lsp): throttle diagnostic storms

* fix(lsp): account for capped storm diagnostics
2026-06-18 09:03:43 +08:00
BogdanandGitHub 916f2477f3 fix(warnings): surface perf hooks buffer guidance (#1696) 2026-06-18 08:59:36 +08:00
BogdanandGitHub df986c9275 fix(messages): make projections tool-pair safe (#1695) 2026-06-18 08:59:05 +08:00
2aad6fc93e feat(config): add OPENCLAUDE_CONFIG_DIR override (#1683)
* feat(config): add OPENCLAUDE_CONFIG_DIR env var as preferred alias for CLAUDE_CONFIG_DIR (#454)

The legacy CLAUDE_CONFIG_DIR name was the only way to point openclaude
at a non-default config home, which leaked Anthropic branding for a
fork that has otherwise rebranded to OpenClaude. Add OPENCLAUDE_CONFIG_DIR
as the preferred name. CLAUDE_CONFIG_DIR continues to work for
backward compatibility; when both are set with different values,
OPENCLAUDE_CONFIG_DIR wins and a one-time warning is logged.

- src/utils/envUtils.ts: introduce resolveConfigDirEnv() that picks
  OPENCLAUDE_CONFIG_DIR over CLAUDE_CONFIG_DIR and emits a conflict
  warning. Memoize cache key now tracks both env vars so changing
  either invalidates the cached result.
- src/utils/env.ts: getGlobalClaudeFile() previously read
  CLAUDE_CONFIG_DIR directly, missing the new alias. Route through
  resolveConfigDirEnv() so the global config file path follows the
  same precedence.
- src/utils/secureStorage/macOsKeychainHelpers.ts: the "is default
  dir" check used by keychain service-name scoping now considers
  both env vars.
- src/utils/swarm/spawnUtils.ts: forward OPENCLAUDE_CONFIG_DIR to
  teammate processes alongside the legacy var.
- src/utils/openclaudePaths.test.ts: +6 unit tests covering the new
  alias, fallthrough, conflict warning, and resolveConfigDirEnv()
  in isolation.
- .env.example: document both env vars and the precedence rule.

Verified locally on Linux: with only OPENCLAUDE_CONFIG_DIR set, with
only CLAUDE_CONFIG_DIR set (legacy still works), with both set
matching (silent), with both set conflicting (warn once + OPENCLAUDE
wins), with neither set (default ~/.openclaude). Memo cache
invalidates across 4 sequential env transitions. Built dist/cli.mjs
honors the new var and emits the conflict warning to the user.

* Fix config-dir warning and docs review findings

Only mark the config-dir conflict warning as emitted when a warning callback actually receives it, add coverage for warn-once and silent callers, and update web configuration docs for OPENCLAUDE_CONFIG_DIR precedence.

# Conflicts:
#	web/src/data/configuration.ts

* Align configuration docs with openclaude paths

Update the configuration page settings-file table to point default users at .openclaude settings and keybindings paths, matching the new config home behavior.

* Align keybindings docs with openclaude config home

Update the keybindings page, keybindings docs data, and skill index to point default users at ~/.openclaude/keybindings.json.

* Align skill and hook labels with openclaude paths

Update bundled config/keybindings skill prompts, public skills docs, hook/trust labels, and the user memory selector to use the active OpenClaude config home paths.

# Conflicts:
#	src/components/TrustDialog/utils.ts
#	src/components/hooks/SelectEventMode.tsx
#	src/skills/bundled/updateConfig.ts
#	src/utils/hooks/hooksSettings.ts

* Resolve config-home paths dynamically in skill prompts

Use runtime settings/keybindings path helpers for bundled skill prompts and the restricted-hooks banner so custom OPENCLAUDE_CONFIG_DIR values are reflected in user-facing guidance.

* Update active command prompts for openclaude paths

Point statusline, setup/onboarding prompts, plugin messages, and the external user-memory warning at the active OpenClaude settings and memory paths.

# Conflicts:
#	src/commands/auto-fix.ts
#	src/commands/onboard-github/onboard-github.tsx
#	src/commands/plugin/ManagePlugins.tsx
#	src/commands/statusline.tsx

* Fix remaining config path review findings

* Cover dynamic config paths in UI and storage tests

* Fix config path smoke failures after rebase

* Fix remaining config path review findings

---------

Co-authored-by: gnanam1990 <gnanasekaran.sekareee@gmail.com>
2026-06-18 08:57:22 +08:00
BogdanandGitHub e5cb589031 security(status): redact proxy and TLS-sensitive values in /status (#1672)
* security(status): redact proxy and TLS-sensitive values in /status

Make /status safe to share in public issues and screenshots by ensuring
proxy credentials, mTLS private key/cert paths, CA bundle paths, and
token-bearing URLs are never printed verbatim.

- Proxy URL: wrap with redactUrlForStatus (reuses redactUrlForDisplay
  for credential + sensitive query-param masking; additionally strips
  the URL fragment, which can carry tokens).
- NODE_EXTRA_CA_CERTS / CLAUDE_CODE_CLIENT_CERT: wrap with
  redactPathForStatus, which shortens a leading $HOME to ~ so paths
  stay useful without leaking usernames or home directory layout.
- CLAUDE_CODE_CLIENT_KEY: show the literal 'configured' rather than
  the path or value of a private key.

Adds two small reusable helpers in src/utils/statusRedaction.ts plus
unit tests, and extends status.test.ts with an integration test that
asserts the full buildAPIProviderProperties output is leak-free when
proxy credentials and mTLS env vars are set.

* fix(status): address status redaction review feedback

* fix(status): redact provider base URL secrets

* fix(status): unify URL status redaction
2026-06-18 08:53:31 +08:00
BogdanandGitHub 5af6f95c46 feat(config): add explicit provider env-file loading (#1668)
* feat(config): add explicit provider env-file loading

* fix(config): handle escaped quotes in provider env files

* fix(config): polish env-file parser review feedback

* fix(config): preserve provider env-file precedence

* test(config): cover provider env-file precedence

* fix(config): preserve provider env-file values

* fix(config): allow documented env-file setup vars

* fix(config): preserve provider flag precedence
2026-06-18 08:51:59 +08:00
5471e4c453 feat(agent-routing): assign a per-agent model from the /agents menu (#1632)
* feat(agent-routing): add user-settings route read/write helpers for the /agents UI

* feat(agent-routing): add AgentRouteSelector UI for picking a per-agent model route

* feat(agent-routing): open the model-route selector from the /agents detail view

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

* fix(agent-routing): scope route options to user settings, align cross-provider labels

Build route options from getSettingsForSource('userSettings') instead of
getInitialSettings(), matching the scope we persist to. Reading from the
merged view could surface an agentModels key that only exists in a
non-user scope; selecting it would write a shadow user-settings entry that
diverges from the original (losing its base_url/api_key).

Label a key as cross-provider when either base_url or api_key is present,
matching readAgentRoute, so the same route is described consistently.

Also assert the dangling case in the currentRouteValue test, which the
old test name referenced but did not cover.

* fix(agent-routing): commit custom model id on submit, match runtime cred rule

The custom-model input persisted from the per-keystroke onChange, so typing
the first character of a model id saved that single character and closed the
selector. Track the typed value and persist only on submit (the sentinel
fires the Select onChange with the full value).

Cross-provider detection now mirrors the runtime resolver (toAgentRoute):
a route is cross-provider only when both base_url and api_key are present. A
partial entry is skipped at runtime and inherits, so readAgentRoute reports it
as unconfigured (dangling) and buildRouteOptions labels it 'unconfigured,
inherits' rather than claiming a cross-provider route that will not execute.

* fix(agent-routing): resolve the picker against the runtime's effective route key

The runtime resolver normalizes routing keys (case-insensitive,
hyphen/underscore-equivalent) and falls back to default, but the picker
read and wrote agentRouting with the exact agentType key. So an existing
general_purpose route showed general-purpose agents as inheriting, and
selecting a model wrote a general-purpose sibling that first-wins lookup
ignored while the menu claimed the change took effect.

readAgentRoute now matches the normalized per-agent key the resolver
would use, and surfaces a default-fallback route with viaDefault.
computeSetRouteUpdate/computeClearRouteUpdate overwrite or clear that
existing key spelling instead of writing a sibling. The clear option is
hidden for default-inherited routes since there is no own key to remove.

* fix(agent-routing): do not save routes a higher-priority source overrides

The runtime resolver reads merged settings (userSettings ->
projectSettings -> localSettings -> flagSettings -> policySettings), but
the picker read and wrote only userSettings. A project/local/policy
agentRouting entry made /agents report the agent as inheriting, and
selecting a model wrote a userSettings route the merged chain ignored
while the menu reported success.

getAgentRoute now reads the effective merged settings, so the route line
reflects what runtime resolves. setAgentRoute/clearAgentRoute refuse with
an explanation when a higher-priority source owns the normalized route
key, and the picker surfaces that as a read-only notice instead of
offering an edit that cannot take effect.

* fix(agent-routing): source-aware shadow guidance + selector tests

flagSettings has no file to edit (it comes from the --settings flag or
SDK inline settings), so the shadow message no longer tells users to edit
a nonexistent file. Extracted shadowRemediation so the error and the
read-only selector notice share one source-aware string.

Added focused AgentRouteSelector tests covering the shadow read-only mode
(file-backed and flag sources), persisting a selected model, and surfacing
a failed save without closing the dialog.

* test(agent-routing): make selector set-route test deterministic

The persist test selected an option by ordinal without controlling the
option list and only asserted the agentType. Mock buildRouteOptions to a
known single option and assert the full [agentType, modelKey] tuple, so
the test proves the selected model key is what gets persisted.

* test(agent-routing): stop selector mock leaking into sibling test

bun's mock.module persists across files in the same process and
mock.restore() does not undo it, so mocking buildRouteOptions in the
selector test leaked into agentRouteSettings.test.ts and failed its
buildRouteOptions assertions in the full suite (passed in isolation).
Re-point the module at the real implementation in afterEach, matching the
AgentsMenu.test.tsx pattern.

* test(agent-routing): use real buildRouteOptions in selector test

Faking buildRouteOptions via mock.module leaked into agentRouteSettings.test.ts
in the full suite: bun live-updates the imported namespace, so the afterEach
re-mock to that namespace re-installed the fake. The real buildRouteOptions is
deterministic (built-in aliases list sonnet/opus/haiku first), so option 1 is
'sonnet' without any fake. Only the I/O wrappers stay mocked.

* test(agent-routing): pin mock restore to a pre-mock exports snapshot

Snapshot the real agentRouteSettings exports into a frozen const before the
first mock.module call and spread/restore from that, instead of the live
realRouteSettings namespace bun mutates when the module is mocked.

* fix(agent-routing): resolve alias routes provider-aware, guard disabled user settings

Two review findings:

- A model-only route whose model is a built-in alias (sonnet/haiku/opus/inherit)
  was sent literally as mainLoopModel, bypassing getAgentModel()'s provider-aware
  fallback. On non-Claude-native providers that 404s. resolveAgentRunModelRouting
  now runs a bare-alias model-only route through getAgentModel (parentModel +
  permissionMode threaded from the callers), so it inherits the parent model the
  same way the agent model selector does. Real model ids pass through unchanged.
- setAgentRoute/clearAgentRoute now refuse with an explanatory error when
  userSettings is not an enabled setting source (e.g. --setting-sources project),
  instead of writing a route the runtime will never load and reporting success.

* fix(agent-routing): apply model-only routes to teammate spawns

Pane/window teammates resolve their model through
resolveOutOfProcessTeammateProvider, which returns only cross-provider
overrides. A model-only agentRouting route (the common case the /agents
menu writes) was dropped, so a routed teammate type with no explicit
model inherited the parent instead of the saved route.

Add resolveOutOfProcessTeammateModelOnly, the model-only twin of the
provider resolver, mirroring runAgent's lookup order and provider-aware
alias resolution, and consult it in the teammate spawn path when there
is no cross-provider override. Enforce the model allowlist on the
resolved model when it changes the effective model.

Also add the permission-mode regression CodeRabbit requested: spy on
getAgentModel to assert the mode is forwarded into alias resolution.

* fix(agent-routing): guard agentModels key shadowing and honest clear label

The picker builds options from userSettings, but agentModels merges by
source priority, so a user-level route to a key that a higher source
(project/local/flag/policy) also defines resolves to that higher entry,
not the current-provider model the option promised. getRouteShadowSource
only checked agentRouting keys, so this collision was not surfaced.

Add findModelKeyShadowingSource / getModelKeyShadowSource and refuse to
save a model-only route whose key is shadowed by a higher source (unless
the user already owns that key in userSettings). buildRouteOptions now
flags shadowed keys so the conflict is visible before selecting.

Also fixes the clear-route label: clearing only removes the agent's own
routing key, so when a default route is configured the agent falls back
to the default, not the parent model. The label and PR description now
say so instead of promising parent inheritance.

* fix(agent-routing): reject shadowed model keys even when userSettings owns them

The previous shadow guard skipped the check when userSettings already
defined agentModels[modelKey], but that does not make the route take
effect: agentModels merges by source priority, so a higher-priority
project/policy entry for the same key still wins and the agent resolves
to that provider/model. The picker even flagged the key as shadowed
while the save path let it through, so offer and save disagreed.

Drop the ownsKey carve-out so setAgentRoute rejects whenever a higher
source defines the key. Extract collectShadowedModelKeys as the single
definition of 'defined above userSettings' that both the offer flag
(getShadowedModelKeys) and the save guard (findModelKeyShadowingSource)
derive from, and add a test locking the two paths in agreement.

* test(agent-routing): cover setAgentRoute shadow guard and model-only teammate spawn

Adds direct setAgentRoute() coverage for the higher-priority-shadow rejection
and the allowed user-only-key save, and an AgentTool teammate-spawn regression
that a model-only route reaches spawnTeammate as the resolved model with no
cross-provider override.

* test(agent-routing): cover custom model id submission persists full id

* test(agent-routing): match focus glyph cross-platform in custom-id test

The custom model regression test waited for the ❯ focus pointer, but ink
renders figures.pointer as ">" on Windows, so the wait timed out in a
Windows checkout before typing the id. Match figures.pointer directly so
the wait tracks whatever glyph the renderer emits on each platform.

* fix(agent-routing): let the route picker own Esc while open

AgentDetail kept its parent confirm:no (Esc -> onBack) handler active after
switching into routing mode. Since the Confirmation context was registered
first, a bare Esc resolved to confirm:no and exited the detail view instead
of resolving to the nested select's select:cancel, despite the picker copy
telling users Esc goes back. Gate confirm:no with isActive=!routing so the
route selector owns Esc and a single Esc only closes the picker. Add an
AgentDetail regression covering m then Esc.

* fix(agent-routing): scope Esc to the route picker and keep in-process teammate routes

Two follow-ups from review of the per-agent model routing UI.

The /agents detail view wraps AgentDetail in a Dialog whose own confirm:no
(onCancel) stayed active while the route picker was open. As the
first-registered Confirmation context it swallowed Esc and dropped back to the
agent list instead of just closing the picker. AgentDetail now reports its
picker state up through onRoutingChange, and a small AgentDetailDialog wrapper
feeds that into the Dialog's isCancelActive so the picker's Select owns Esc
while it is open.

In-process teammates run the same runAgent() as normal subagents, but the
synthetic agent definition overwrites agentType with the teammate's display
name, so the original subagent_type that agentRouting is keyed on was lost and
the configured cross-provider route never resolved (the teammate ran the routed
model on the parent provider). The original subagent_type is now carried through
the in-process handoff and used as the routing key, matching what pane and
window teammates already get by re-resolving from their CLI identity.

Adds a regression test for the picker Esc interaction through the Dialog wrapper
and resolver tests proving the route resolves from the original subagent_type
rather than the teammate name.

---------

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-06-18 08:46:51 +08:00
BogdanandGitHub 4cf981200f feat(cache): classify prompt-cache breaks by reliability (#1693)
* feat(cache): classify prompt-cache breaks by reliability

* fix(cache): stabilize prompt-cache break metadata detection

* fix(cache): honor legacy OpenAI base fallback

* fix(cache): normalize OpenAI base URL hints

* fix(cache): align cache-break provider flag truthiness

* fix(cache): ignore undefined OpenAI base hints

* fix(cache): sanitize prompt cache route labels
2026-06-18 08:42:41 +08:00