Commit Graph
856 Commits
Author SHA1 Message Date
1aabe261db feat(bughunter): make /bughunter public + add /bughunter-security & /bughunter-perf with robust fallback prompts (#1621)
* feat(bughunter): split into /bughunter, /bughunter-security, /bughunter-perf

Replace the single /bughunter command with three siblings that share a
common prefix:

  /bughunter          — general bug hunt (existing prompt, untouched)
  /bughunter-security — OWASP-aligned, exploit-driven, confidence ≥ 8
  /bughunter-perf     — hot-path complexity, sync I/O, leaks, N+1

Both new subcommands are prompt commands built with
createMovedToPluginCommand so they migrate to the bughunter marketplace
plugin unchanged once it ships. While the marketplace is private they
inline the full audit prompt (frontmatter + !`git ...` blocks) just like
the existing /bughunter.

All three stay in the public COMMANDS list (not INTERNAL_ONLY_COMMANDS)
so non-ant users can invoke them. clearCommandMemoizationCaches() now
also flushes the zero-arg COMMANDS() and builtInCommandNames() memos so
tests can switch USER_TYPE mid-run without poisoning the cache.

Adds regression tests in src/commands.test.ts covering:
  - bughunter stays public for non-ant users
  - bughunter-security and bughunter-perf are in the public list

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

* chore(bughunter): remove orphan index.js after .js → .ts rename

The bughunter command directory was renamed from a single .js file to
index.ts in the previous commit, but git tracked them as separate paths
so the old .js was left in the tree. Drop it.

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

* feat(bughunter): enhance fallback prompts for robustness in non-git environments

- Add graceful error handling to all git commands in fallback prompts (|| echo fallbacks)
- Add explicit non-git fallback guidance in Phase 1 for all three commands
- /bughunter: search for entry points, core business logic, recently modified files
- /bughunter-security: search for auth/middleware, validation, DB, config, upload code
- /bughunter-perf: search for handlers, loops, data access, serialization, build configs
- Improve context labels to clarify git context may be empty

* fix(bughunter): address CodeRabbit feedback

- Fix test isolation: restore USER_TYPE/IS_DEMO env vars in finally blocks
- Add non-git fallback test cases for all three bughunter commands
- Fix bash pipeline issue: replace if/then/else subshells with simple git commands + static fallback text in template
- Fix output format contradiction: remove LOW confidence from scoring (Phase 3 drops LOW, so scoring only includes Critical/Medium)

* fix(test): correct case and prefix in git fallback assertions for bughunter-security and bughunter-perf tests

* fix(test): add missing opening parenthesis in bughunter test assertions

* fix(bughunter): complete non-git fallback and propagate allowedTools

- Fix git commands in all three prompts to always succeed with fallback text (using || echo)
- Modify createMovedToPluginCommand to accept allowedTools parameter
- Add allowedTools to all three bughunter commands so slash-command turn grants declared tools
- Parse allowed-tools from frontmatter at command creation time

* fix(bughunter): complete non-git fallback and allowedTools propagation

- Fix git commands in prompts to always succeed with fallback text (using || echo)
- Modify createMovedToPluginCommand to accept allowedTools parameter
- Add allowedTools to all three bughunter commands so slash-command turn grants declared tools
- Fix RECENTLY COMMITTED FILES command to avoid command substitution (permission check rejects )
- Update tests to accept shell tool's '(Bash completed with no output)' for empty results
- Use runWithCwdOverride and additionalWorkingDirectories for proper test isolation

* fix(bughunter): prevent shell injection via user-provided args

The user-provided scope was interpolated into the prompt template BEFORE
executeShellCommandsInPrompt() ran, so any !command or ```! block
syntax in the args would be interpreted and executed as shell commands.

Fix: parse frontmatter from the raw template and run shell execution first
(with {{ARGS}} still in place — inert to shell patterns), then replace
{{ARGS}} with the user scope on the processed output. This ensures args
are never fed through the shell command parser.

* refactor(bughunter): use createGetAppStateWithAllowedTools helper

Replaces duplicate inline getAppState overrides across all three bughunter
commands (bughunter, bughunter-security, bughunter-perf) with the shared
helper from src/utils/forkedAgent.ts. This:
- Eliminates ~30 lines of duplicated permission context modification
- Merges allowedTools with existing alwaysAllowRules.command (vs overwrite)

* fix(bughunter): address jatmn review - String.replace special patterns + test isolation

- Replace '{{ARGS}}' with a replacer function () => scope instead of
  the plain string 'scope'. JavaScript's String.replace treats $&, $',
  $', , 32855 specially even in string replacements, so a scope like
  'src/auth $&' would render as 'src/auth {{ARGS}}' instead of literal
  text. The replacer function bypasses all special patterns.

- Restore USER_TYPE and IS_DEMO env vars in the injection regression
  test's finally block, matching the isolation pattern used by all other
  bughunter tests.

* fix(bughunter): make fallback prompt generation work on Windows

Wrap executeShellCommandsInPrompt() in a try/catch in all three bughunter
commands. On platforms where bash is unavailable (e.g. Windows without Git
Bash), the bash-specific shell syntax (2>/dev/null, | head -N) would cause
executeShellCommandsInPrompt to throw MalformedCommandError, preventing the
prompt from being generated at all.

The catch handler replaces the !`command` inline patterns with a static
placeholder, allowing the LLM to still receive the full audit instructions
and non-git search strategies in Phase 1.

* fix(bughunter-perf): remove Low severity contradiction

The summary line included Low: L but Phase 3 drops non-measurable findings
and exclusions remove micro-optimizations. Low findings (measurable but
not user-visible) would never survive the filter, so remove Low from the
severity categories and summary line.

fix(bughunter-security): align log-forging exclusion with A9 criteria

Exclusion #11 blocked all log spoofing/forging, but A9 says to flag
log injection when it enables audit-trail forgery. Narrowed the exclusion
to allow concrete audit-trail attacks through while still excluding
generic non-exploitable logging suggestions.

* fix(bughunter-security): tighten log-forging exclusion threshold

Reword exclusion #11 to require concrete evidence of a log-entry or
structured-field forgery path, not merely unsanitized user input.

* fix(bughunter): preserve fallback text on Windows/no-bash path

Replace generic '(Shell execution unavailable)' placeholder with a regex
that extracts the || echo "..." fallback text from each shell command.
This ensures the prompt shows meaningful messages like
'(If empty: not a git repository or git unavailable)' even when bash is
unavailable (e.g. Windows without Git Bash), matching what Linux users see
from working shell execution.

Also make injection test assertion platform-agnostic — accept either bash
output or the static echo fallback text.

* refactor(test): extract duplicate mockContext into createMockToolContext helper

The three non-git fallback tests each had an identical ~42-line mockContext
object. Moved it to a shared createMockToolContext(cwd, commands) helper
and a FULL_GIT_COMMANDS constant. Also updated the injection test to use
the same helper. Net -89 lines.

* fix(createMovedToPluginCommand): only grant allowedTools when fallback prompt runs

The ant (USER_TYPE === 'ant') branch returns a plugin-install notice that
doesn't need Read/Glob/Grep/Bash tools, but allowedTools was statically
attached to the command object. This caused processSlashCommand to grant
turn-scoped permissions for tools that were never used.

Changed to a getter that returns undefined in the ant branch, so the
plugin-install notice runs without unnecessary tool permissions.

* fix(bughunter): simplify shell commands to single git commands, narrow catch to surface interruptions

* fix(bughunter): surface permission-denied/aborted shell preprocessing, fix Windows cleanup

* fix(dragDropPaths.test): resolve package.json relative to test file, not process.cwd()

* fix(commands.test): restore original cwd in rmRetry, guarantee env/cache cleanup on rm failure

* fix(bughunter): bound diff to 400 lines, swap HEAD~10 for git log -10

Address both P2 reviewer findings on feat/bughunter-command-v3-new.

(1) Fresh-repo HEAD~10 lookup stripped every snippet. In a one-commit
    repo, `git diff --name-only HEAD~10..HEAD --diff-filter=AM` exits
    128 (HEAD~10 doesn't resolve). The shell-execution catch then ran
    the outer "strip all snippets" fallback, leaving git status /
    diff --cached / diff HEAD empty even though those commands would
    have produced useful context. Switched to `git log -10 --name-only
    --diff-filter=AM`, which works at any history depth and yields the
    same file list. Applied to bughunter, bughunter-security, and
    bughunter-perf.

(2) Diff cap removed in 73d0bcb. The prompt label still advertised
    "first 400 lines" but the snippet was just `git diff HEAD -- .`,
    and a 900-line diff was injected verbatim. Added a new `lineLimits`
    option to `executeShellCommandsInPrompt` that bounds output by
    command prefix. The cap is applied to stdout *before*
    processToolResultBlock, so the persistence + empty-content guard
    flows run once on the bounded payload, and large diffs no longer
    hit the 30k Bash result cap and spill into the prompt. Each
    bughunter command passes
    `{ lineLimits: { 'git diff HEAD -- .': 400 } }`. Allowed-tools
    frontmatter is unchanged — no compound `| head -400` that the
    permission parser might reject.

Tests:
- `executeShellCommandsInPrompt applies per-prefix line limits` +
  `does not truncate below the cap` (new unit tests in
  promptShellExecution.test.ts).
- `bughunter keeps git context populated in a fresh single-commit
  repo` (regression for finding 1, uses real one-commit git repo).
- `bughunter diff block is bounded to 400 lines` (regression for
  finding 2, builds 1000-line diff and asserts ≤400 lines).
- `FULL_GIT_COMMANDS` and the injection test now include
  `git log -10 --name-only --diff-filter=AM` in place of the
  removed HEAD~10 form.

* fix(bughunter): keep recent-files path-only, cover all three siblings

Two review follow-ups on the previous P2 commit.

(P3) `git log -10 --name-only` defaulted to --pretty=fuller, so the
"RECENTLY COMMITTED FILES" block injected commit hash, author, date,
and message lines into the prompt under a files-only heading — that
extra metadata crowded out the scoped file list the command was
trying to provide. Added `--pretty=format:` to suppress the commit
header on all three commands (bughunter, bughunter-security,
bughunter-perf). Verified locally: the previous form emitted ~7
header lines per commit; the new form emits just the file paths.

(P2) The fresh-repo and 400-line cap regression tests only exercised
/bughunter, so a sibling could regress back to the old shallow-history
failure or lose the diff cap without this suite failing. Parameterized
both tests over {bughunter, bughunter-security, bughunter-perf} via a
BUGHUNTER_SIBLINGS const; each command now runs both regressions in
its own tmp dir (six new test cases total). Typecheck clean, 27
tests pass.

* fix(promptShellExecution): granular snippet fallback, restore rich error for other callers

- Add granularFallback option to executeShellCommandsInPrompt. When
  enabled, a failing shell snippet is blanked in place and the rest of
  the snippets keep their output. Permission denials and interrupted
  ShellError still rethrow as MalformedCommandError, never swallowed.
- Restore the formatted MalformedCommandError wrapping in the default
  path. Previously a no-op that rethrew the raw ShellError, which made
  processSlashCommand render only 'ShellError: Shell command failed'
  for /commit, /security-review, /commit-push-pr, loaded skills, and
  plugin commands. Now includes the failing pattern and formatted
  stdout/stderr.
- /bughunter, /bughunter-security, /bughunter-perf opt into
  granularFallback and drop the catch-and-strip-all pattern. A failing
  'git log -10' on a zero-commit repo no longer discards git status
  output.
- Tests cover per-snippet blanking, default-path rich error wrapping,
  and that permission denials still surface under granularFallback.

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

* fix(promptShellExecution): preserve trailing newline in applyLineLimit truncation

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-17 11:04:01 +08:00
beardthelionandGitHub d5588ea80d feat(context-collapse): opt-in between-turns context collapse (span summarization) (#1619)
* feat(context-collapse): implement context collapse for proactive context management

* feat(context-collapse): add turn-boundary helpers for span selection

* feat(context-collapse): deterministic turn-anchored span selection

* feat(context-collapse): code-computed span risk score

* feat(context-collapse): ctx-agent summarization instruction

* feat(context-collapse): implement ctx-agent span summarization spawn

* fix(context-collapse): make runtime activation opt-in (CLAUDE_CONTEXT_COLLAPSE)

* fix(context-collapse): address review feedback on restore state and test rigor

- restoreContextCollapseState now resets armed/lastSpawnTokens up front so a
  snapshot-less restore cannot carry stale spawn state across sessions.
- projectView reuses a stable timestamp from the replaced span instead of
  new Date(), keeping the read-side projection deterministic.
- Strengthen the disabled-state and turn-boundary assertions, drop an internal
  renderToolUseMessage assertion, and isolate the operations/persist/spawn tests
  from shared module and CLAUDE_CONTEXT_COLLAPSE env state.

* test(context-collapse): re-init enablement in persist.test hooks

resetContextCollapse() does not re-read CLAUDE_CONTEXT_COLLAPSE, so the
afterEach env delete left enabled=true in module state, leaking to the
next test file. Call initContextCollapse() in both hooks so module
enablement stays synced to the env var.

* test(context-collapse): stop spawnCtxAgent module stubs leaking across files

spawnCtxAgent.test.ts stubs shared modules (tokens, forkedAgent, messages,
analytics, log, spanSelection) via mock.module in beforeEach. bun's
mock.restore() does not undo mock.module, so the tokens stub (() => 100000)
bled into autoCompact/microCompact/runAgent tests run later in the full serial
suite, making them see every conversation as over-threshold (4 spurious
failures in test:full, all green in isolation).

Restore each stub to its real implementation in afterEach. The reals are
snapshotted into plain objects up front because 'import * as' yields a live
namespace that mock.module mutates in place, so holding the namespace would
restore the stub. autoCompact.js is deliberately not restored here since
autoCompact.test.ts re-imports it fresh via a cache-busting nonce.

Also reset+reinit the collapse module in afterEach so enabled state stays
synced to the now-unset env var.

* test(context-collapse): also restore autoCompact stub from spawnCtxAgent

The getEffectiveContextWindowSize stub on ../compact/autoCompact.js was the one
module the previous commit left unrestored, on the assumption that restoring it
would clash with autoCompact.test.ts's nonce re-import. It doesn't: the nonce
import uses a different specifier, and the snapshot restore is keyed by the
plain specifier. compressToolHistory imports getEffectiveContextWindowSize and
sizes tool-history truncation from it, so the leaked 20000-token window made it
fully omit tool results ('chars omitted') instead of mid-truncating
('[…truncated') for large-context models, failing the openaiShim compression
tests in the full serial suite. Restore all seven mocked modules.

* fix(context-collapse): re-arm after reset and gate ctx_inspect on opt-in

resetContextCollapse() left armed=false while enabled stayed true, so the
first /compact, main-thread compaction cleanup, or rewind permanently
disabled collapse for the rest of an opted-in session. Reset now mirrors
restoreContextCollapseState and sets armed=enabled.

CtxInspectTool.isEnabled() returned true unconditionally, advertising
ctx_inspect to the model in every default session even when the runtime
opt-in was off. It now returns isContextCollapseEnabled(). The opt-in is
also exposed as the contextCollapseEnabled global config key, so it is
reachable through /config instead of only the CLAUDE_CONTEXT_COLLAPSE env
var.

* refactor(context-collapse): drop no-op ternary in drainStaged persist call

The (stagedQueue.length > 0 ? 0 : 0) subtrahend always evaluated to 0, so
this is just persistCommits(processed.length).

* fix(context-collapse): persist commits before advancing the snapshot

drainStaged removed processed spans from the staged queue and then fired
persistCommits and persistSnapshot in parallel. If the snapshot write (which
no longer lists those spans as staged) landed while the commit write failed
or the process died between them, restore would find the spans neither staged
nor committed and the collapse would disappear on resume. Chain the snapshot
write after the commit write so the commit log is durable first.

* fix(context-collapse): project committed collapses on the query path, fix opt-in reach

Three issues from review:

- Committed collapses were never re-applied to the model input. The query path
  calls applyCollapsesIfNeeded but only drained staged spans; projectView (which
  replays the commit log) ran only in /context. Since messagesForQuery is rebuilt
  from full REPL history each turn and the commit log is repopulated on resume,
  the archived spans returned to the model on the next turn, undoing the collapse.
  applyCollapsesIfNeeded now runs projectView first (idempotent). Adds a
  regression that a committed collapse changes the next query input.

- Cache-safe params were saved only for exact repl_main_thread/sdk sources, but
  the REPL tags non-default output styles as repl_main_thread:outputStyle:*, so
  those sessions left the ctx-agent without params (empty spawns). Matches
  repl_main_thread:* now, via a small tested helper.

- contextCollapseEnabled had no settings control. Adds a /config toggle that
  refreshes runtime state (re-runs initContextCollapse) so it applies without a
  restart.

* fix(context-collapse): clear already-committed staged spans; harden config toggle

After projecting committed collapses before draining, a span present in both the
commit log and the staged snapshot (a restore whose snapshot predates the
matching commit write) could not be drained — projectView had already removed
its messages — so it lingered in stagedQueue and distorted spawn/overflow
checks. drainStaged now drops staged spans that are already committed and syncs
the snapshot. Adds a regression covering the committed+staged overlap restore.

Also wraps the /config context-collapse refresh in try/catch so a failed
require/init can't crash the settings UI, and lists the toggle in the
save-and-close change summary like the neighboring compaction settings.

* fix(context-collapse): re-sync runtime state on config cancel

The context-collapse toggle's onChange refreshes the module-level
enabled/armed cache via initContextCollapse(). The revert path restored
the config key on disk but left that cache untouched, so enabling the
toggle and then pressing Escape kept collapse active for the rest of the
session. Re-init context collapse after the global config snapshot is
restored so cancel fully reverts runtime state.

* fix(context-collapse): keep collapsed summaries visible to the model

projectView and drainStaged replaced an archived span with a system
informational placeholder, but normalizeMessagesForAPI filters out every
system message that is not a local command. So once a collapse committed,
the next model request lost both the archived messages and the
<collapsed> summary meant to stand in for them, defeating the feature.

Mark the placeholder with isCollapseSummary and let it take the same
model-input path as local-command system messages (converted to a user
message), so the summary survives normalization. Added a regression that
runs the projected view through normalizeMessagesForAPI and asserts the
summary is still present.

* fix(context-collapse): avoid competing snapshot write after drain

After an immediate post-spawn drain, drainStaged(messages, true) starts
its own persistCommits().then(persistSnapshot) chain to guarantee commit
durability before the snapshot stops listing the staged spans. The
unconditional await persistSnapshot() that followed could win that race
and persist a snapshot with no staged spans before the commits landed,
reopening the crash window that drops collapses on restore. Only persist
directly when nothing was drained.

* fix(context-collapse): fall back, keep summaries non-snippable, gate /context

Three review findings:

- Suppress autocompact and the blocking preempt only when collapse holds a
  real committed/staged reduction, not on mere enablement. Adds
  hasActiveReduction(); a first over-threshold turn where spawnCtxAgent cannot
  produce a span (getLastCacheSafeParams() still null) now falls back to
  autocompact/blocking instead of sending an oversized transcript.
- Preserve isMeta when converting a collapse-summary placeholder to a user
  message in normalizeMessagesForAPI, so the HISTORY_SNIP sweep cannot tag the
  only replacement for an archived span as snippable.
- Gate the two /context projectView calls on isContextCollapseEnabled(), so a
  disabled session does not under-report token usage from a lingering commit
  log while the API receives the full transcript.

Adds regressions for hasActiveReduction and for the summary surviving
normalization as a non-snippable meta message.

* fix(context-collapse): scope collapse to the main thread that owns the store

The collapse store (commitLog/stagedQueue) is module-level and shared by
in-process subagents (agent:*) and the ctx-agent (marble_origami), which
run in the same process but do not own the main transcript.
applyCollapsesIfNeeded only skipped marble_origami, so a subagent could
stage or commit a span, flip the global hasActiveReduction(), and make
the next main-thread turn suppress autocompact and the blocking
prompt-too-long preempt while projectView() no-ops against the main
messages, sending an oversized transcript to the API.

Add isMainThreadSource() and gate both application (applyCollapsesIfNeeded,
isWithheldPromptTooLong, recoverFromOverflow) and fallback suppression
(autoCompact shouldAutoCompact, query collapseOwnsIt) to the owning
thread. Subagents now autocompact and preempt their own oversized turns
normally and never mutate the shared store.

Also adds the staged-only hasActiveReduction regression CodeRabbit
requested.

* fix(context-collapse): persist archived count so resumed stats stay accurate

restoreContextCollapseState rebuilt each commit with an empty archived
list, and getStats summed that list, so after a resume /context, the
context visualization, the token warning, and ctx_inspect reported
'N spans summarized (0 messages)' even though projectView was actively
removing the archived spans. The persisted-entry docstring claimed
projectView lazily refills the archive, but it only splices by boundary
uuid and never does.

The archived messages are never read back (only their count fed
getStats), so replace the per-commit Message[] with a persisted
archivedCount. It is written with each commit and restored on resume;
pre-field sessions restore as 0. getStats now reports the same figure
live and after resume.

* fix(context-collapse): keep collapse summary non-snippable across user merge

Preserving isMeta on the system->user conversion was not enough: when the
collapsed span ends right before the next user turn, normalizeMessagesForAPI
merges the summary into that real user message. Under HISTORY_SNIP
mergeUserMessages clears isMeta whenever an operand is real user content and
keeps the real turn's uuid, so the combined block — which carries the only
<collapsed> replacement for the archived span — got a snip id and the model
could queue it for removal.

Carry an isCollapseSummary marker onto the converted user message and through
mergeUserMessages (either operand), strip any snip id already baked into the
real turn when the merge absorbs a summary, and skip such blocks in
appendMessageTagToUserMessage. The merged block stays non-snippable
regardless of merge direction or isMeta being cleared.

* fix(context-collapse): preserve collapse marker on split, drop empty snip blocks

normalizeMessages split path now forwards isCollapseSummary so an array-backed
collapse summary keeps its non-snippable marker across API normalization.
stripSnipTagsFromContent drops a text block whose only content was the snip
marker, so the merge recovery path no longer emits an empty text block.
2026-06-17 11:02:54 +08:00
NikhilandGitHub 650fae952d fix(sdk): make stub-leak detection TDZ-safe + defer to next microtask (#1287) (#1398)
* fix(sdk): make stub-leak detection TDZ-safe + defer to next microtask (#1287)

`bun run scripts/start-grpc.ts` crashed at startup with:

    ReferenceError: Cannot access 'QueryEngine' before initialization.
        at detectStubLeaks (src/entrypoints/sdk/index.ts:29:33)
        at src/entrypoints/sdk/index.ts:47:1

The detector ran at module-load time and read each critical import
directly. When the start script's circular-import chain reached the SDK
barrel before `QueryEngine.js` had finished initializing its own export
bindings, the QueryEngine reference at line 29 hit the temporal dead
zone and threw. Stub-leak detection is meant to catch `__stub: true`
markers from the esbuild plugin — TDZ is a different bug class (an
uninitialized binding can't carry `__stub`), so the detector should
treat the access failure as 'nothing to check here' rather than
crashing the entire SDK entry.

Two changes:

1. Wrap each import read in safelyAccess(() => binding) so a TDZ
   ReferenceError on one returns undefined and the loop continues.
   Real stub markers still surface as the explicit SDK init error.
2. Defer detectStubLeaks() from module-load to queueMicrotask, so
   every same-tick init in the circular chain (start-grpc.ts → SDK
   index → QueryEngine → ... → SDK index) completes before we read
   bindings. Microtask runs before any actual SDK usage, so a real
   stub leak still surfaces well before the first query() call.

Tests (3): SDK barrel imports without throwing, anti-regression on
real __stub: true bindings, TDZ-shaped access returns undefined.

* test(sdk): exercise the real stub-leak detector with stubbed fixtures (#1287)

The regression test asserted only that a local object literal had
__stub === true and re-implemented safelyAccess inline, so it never ran
the real detector: removing queueMicrotask(detectStubLeaks), dropping the
loop, or swallowing the __stub case would all still pass.

Split the detection primitives (safelyAccess + the critical-import scan)
into src/entrypoints/sdk/stubLeakDetection.ts and have the SDK entry point
import them. The test now feeds stub-shaped fixtures through the real
checkCriticalImportsForStubs / safelyAccess and asserts: a real
__stub: true binding throws the explicit SDK init error; non-stub modules
pass; a TDZ ReferenceError is tolerated (skipped) without crashing; a stub
behind a skipped TDZ access is still caught; and the SDK barrel import
never throws on its own load. Detector runtime behavior is unchanged.
2026-06-17 10:55:53 +08:00
BogdanandGitHub c74397cd2f chore(gitignore): ignore local worktree directories (#1681) 2026-06-17 10:54:37 +08:00
NikhilandGitHub b8c7c3bfac feat(memory): add memory.autoWrite alias for autoMemoryEnabled (#1326) (#1396)
* feat(memory): add memory.autoWrite alias for autoMemoryEnabled (#1326)

The attribution half of #1326 was fixed via #1335 (merged 2026-05-26).
The memory half — '[memory writes should be] explicit and configurable'
with the exact shape `memory.autoWrite` requested by the issue —
remains.

Rather than parallel-tracking a new key, alias `memory.autoWrite` to
the existing `autoMemoryEnabled` opt-out and document the relationship.
Either key opts out; when both are set, the more restrictive (false)
value wins so a parent-scope opt-out can't be silently re-enabled by a
narrower memory.autoWrite: true.

The new `memory` namespace is intentional — future opt-in fields
(approval gates, etc.) can be added under it without claiming a new
top-level key each time.

- types.ts: add `memory.autoWrite` to the settings schema; cross-link
  to autoMemoryEnabled in the description.
- paths.ts isAutoMemoryEnabled: read both keys; opt-out wins on
  conflict; default unchanged (enabled).
- paths.test.ts (new): pins default, both opt-out paths, both opt-in
  paths, opt-out-wins-on-conflict in both directions, env-var still
  overrides settings.

Tests 7/7 green. Default behavior unchanged — this is purely an
additive discoverable alias for governance / regulated / client-
sensitive repos that prefer the namespaced shape called out in the
issue.

* fix(memory): evaluate autoWrite opt-out across raw settings sources (#1326)

isAutoMemoryEnabled() read the already-merged settings object, so source
precedence had already collapsed same-key values before the "false wins"
rule applied: a lower-priority memory.autoWrite/autoMemoryEnabled: false
opt-out was silently overwritten by a higher-priority true, re-enabling
auto-memory against the stated governance guarantee.

Evaluate the opt-out across the raw per-source settings instead, via
getEnabledSettingSources() + getSettingsForSource() (per-source cached, so
the hot path stays cheap). A single false in any source now wins, so a
parent-scope opt-out cannot be re-enabled by a narrower scope flipping the
key to true.

Test now drives per-source fixtures and covers the cross-source precedence
case (lower-priority false beats higher-priority true) for both keys.

* test(memory): stop the autoWrite test leaking settings mocks across files

The previous test mock.module()'d both settings.js and constants.js. bun's
mock.restore() does not undo mock.module(), so the constants.js stub leaked
into later serial test files and broke flagSettings.test.ts (its cache-busted
settings import still resolved the mocked getEnabledSettingSources).

Drive the real getEnabledSettingSources() via setAllowedSettingSources()
instead of mocking constants, stub only getSettingsForSource, and re-register
the real settings module after each test so nothing leaks. Coverage is
unchanged (per-source fixtures + cross-source precedence cases).
2026-06-17 10:25:34 +08:00
0xfandomandGitHub 8f88608055 test(file-suggestions): stop cross-spawn mock leaking into later suites (#1667)
fileSuggestions.test.ts installs a cross-spawn mock via mock.module, which
bun does NOT undo on mock.restore() — it persists process-wide. The mock's
interception was gated on a closure captured at install time, so after this
suite the persisted mock kept returning a fake child (with no kill()) for any
git command. Test files run sequentially in one process, so a later suite's
real `git ls-files` (e.g. /lsp recommend's filesystem-scan fallback) hit the
fake child, hung to its 5s timeout, and crashed on child.kill() — an
order-dependent failure in smoke-and-tests.

Gate the interception on a module-level activeSpawnScenario that is set only
while one of this suite's spawn-scenario tests runs and cleared in afterEach,
so the persisted mock falls through to the real spawn afterward. Also give the
fake child a kill() that emits close, so any stray caller terminates cleanly
instead of throwing.

Verified the full src suite is green across repeated runs (was intermittently
red on the /lsp filesystem-scan test).
2026-06-17 06:37:58 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
5c0e6612c2 chore(main): release 0.19.0 (#1596)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.19.0
2026-06-16 23:07:21 +08:00
JATMNandGitHub 7be9dce8ef fix: vision handling for OpenAI-compatible models (#1663)
* Fix vision handling for OpenAI-compatible models

Add route-aware vision capability checks for image reads so registered non-vision models get an actionable refusal before sending image content.

Classify provider-side image/text errors as canonical vision_not_supported responses, preserve image-only tool results for OpenAI-compatible shims, and strip rejected images from retry messages.

Add focused coverage for Xiaomi MiMo/OpenGateway route collisions, canonical errors, shim image handling, and the Read prompt.

* Address vision review findings

Move the Read tool vision gate before the UNC no-I/O early return so UNC image paths cannot bypass non-vision model checks.

Add direct FileReadTool.validateInput coverage for non-vision denials, provider override/env precedence, and UNC image paths.

Add the missing OPENAI_BASE_URL exclusion assertion for the Xiaomi MiMo canonical error path.

* Isolate vision gate tests from provider env

Clear OPENAI_BASE_URL and OPENAI_API_BASE before each FileReadTool vision-gate test so full-suite provider tests cannot leak route state into these cases.

* Fix vision gate test full-suite isolation

Import FileReadTool and prompt with a cache-busted module id so compact.test's process-global mock cannot replace validateInput during test:full.

Invoke validateInput directly instead of optional chaining, matching the review finding and making missing exports fail clearly.

* Lock vision prompt env mutations

Acquire the shared mutation lock before mutating OPENAI_BASE_URL and OPENAI_API_BASE in the FileReadTool vision prompt tests, and release it after restoring the environment.
2026-06-16 15:28:24 +08:00
JATMNandGitHub bac74aafee fix: Ollama max output token override (#1659)
* Fix Ollama max output token override

Allow unknown integration models without runtime maxOutputTokens metadata to honor CLAUDE_CODE_MAX_OUTPUT_TOKENS above the Anthropic 64k fallback while still capping at the provider context window or OpenAI-compatible fallback context window.

Update the max-output error copy for third-party providers, add regression coverage for issue #1604, and ignore generated Python/pytest cache artifacts.

* Use standard pytest cache ignore pattern

Replace the non-standard pytest-cache-files pattern with pytest's default .pytest_cache directory ignore entry.
2026-06-16 15:26:47 +08:00
0c45e16f18 feat(config): add compactModel option to use a separate model for compaction (#1445) (#1629)
* feat: add compactModel config option to use a different model for compaction

When set and different from mainLoopModel, the forked-agent prompt-cache-sharing
path is bypassed (guaranteed cache miss with different models) and the streaming
fallback uses compactModel for the API call instead.

Closes #1445

* feat(config): expose compactModel in /config TUI and add compactModel test coverage

Addresses review feedback on #1629:
- Surface compactModel as a managedEnum setting in the /config screen,
  following the teammateDefaultModel pattern (submenu + ModelPicker).
- Add a compact.test.ts case covering the compactModel !== mainLoopModel
  path: cache-sharing is skipped and the streaming compaction fallback
  routes model/maxOutputTokensOverride to compactModel.

* fix(compact): use compactModel for tool-search check and add no-op guard

Addresses round-2 review feedback on #1629:
- compact.ts: pass compactModel ?? mainLoopModel to isToolSearchEnabled so
  the tool-search capability check matches the model actually used for
  streaming compaction (not always mainLoopModel when a compact model is set)
- Config.tsx: mirror the teammateDefaultModel no-op guard — return early
  when compactModel is unset and the picker confirms null (no-op selection)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(compact): normalize compactModel comparison in no-op guard

Compare globalConfig.compactModel ?? null against the picker's
selection so re-confirming the current value (including explicit
"Default"/null when a model was previously set) is treated as a
no-op instead of marking settings dirty.

* fix: resolve compactModel alias to full model ID before API calls

ModelPicker stores alias strings (e.g. 'sonnet') directly in
globalConfig.compactModel. Compact reads that value and must call
parseUserSpecifiedModel() to expand it to the canonical model ID
before comparing against mainLoopModel or sending to the API.

Two read-sites in compactConversation and streamCompactSummary are
both fixed. Test updated so the 'skips cache-sharing' case uses the
resolved model ID (legacy claude-opus-4-1 remaps to current default)
and a new test verifies alias expansion end-to-end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 15:25:46 +08:00
JATMNandGitHub c3db79832b fix: sandbox temp dir fallback (#1662)
* Fix sandbox temp dir fallback

Probe Claude temp directories before returning them and fall back through platform temp and config-home temp paths when the primary temp base is inaccessible.

Use the resolved Claude temp dir for sandboxed shell cwd tracking and TMPDIR/CLAUDE_TMPDIR propagation so the sandbox allowlist, Bash, and PowerShell providers agree on the writable temp path.

Update @anthropic-ai/sandbox-runtime to 0.0.55 and refresh bun.lock.

Validation: bun install passed after escalation; bun run build passed; python -m pytest -q python/tests passed; bun run typecheck:type-tests passed; git diff --check passed. bun run check still reports full-suite order/global-state failures; focused reruns of the reported failing files passed with a dummy ANTHROPIC_API_KEY. bun run typecheck has pre-existing unrelated repo-wide strictness failures; security:pr-scan fails before scanning on mergeBase.stderr.

* Fix PR typecheck and read-only temp fallback

Handle EROFS as an inaccessible filesystem error for sandbox temp fallback behavior.

Add narrow type annotations and inference fixes so the stricter typecheck job passes.
2026-06-16 15:23:23 +08:00
a36ef463ce docs(readme): add social links and clarify license line (#1660)
- Add Discord (discord.gg/k68zFR6AcB) and X (x.com/gitlawb) as shields.io
  badges in the top badge row and as descriptive links in the Community section.
- License section now notes contributor modifications are MIT while the derived
  Claude Code remains Anthropic's, with a "See more" link to LICENSE.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-16 09:14:52 +08:00
NikhilandGitHub 241d52af47 fix(gitDiff): keep hunk content lines beginning with -- or ++ (#1646)
The metadata-skip block in parseGitDiff matched line.startsWith('---')
and line.startsWith('+++') on every line, including lines inside a hunk.
A removed line whose content starts with '--' becomes the diff line
'---...', and an added line whose content starts with '++' becomes
'+++...'; both were treated as file-header lines and dropped, so the
rendered diff silently lost real changes.

These header markers only appear in the file preamble before the first
@@ hunk header, so the skip block is now gated on !currentHunk. Inside a
hunk, +/-/space lines are content and are kept.

Adds parseGitDiff tests covering the dropped-content case, the regression
that header lines are still skipped, and a normal hunk.
2026-06-16 08:47:57 +08:00
BogdanandGitHub bd3ad89dd7 fix(security): bundle real sandbox runtime in open CLI (#1641)
* fix(security): bundle real sandbox runtime in open CLI

* test(sandbox): cover fail-closed runtime diagnostics

* fix(sandbox): report doctor inspection failures
2026-06-16 08:42:48 +08:00
BogdanandGitHub 7c034c5a62 feat: add redacted diagnostic issue reports (#1647)
* feat: add redacted diagnostic issue reports

* fix: address diagnostic report review feedback

* fix: report Codex runtime diagnostics accurately
2026-06-16 08:41:18 +08:00
JATMNandGitHub b036e9fa7c fix: startup provider validation fallback (#1658)
* fix startup provider validation fallback

* test startup provider behavior
2026-06-16 08:26:28 +08:00
BogdanandGitHub d8dbf274b4 chore(runtime): align Node.js minimum version (#1644)
* chore(runtime): align Node.js runtime requirements

* test(runtime): cover prefixed Node versions

* fix(runtime): check node executable in doctor
2026-06-16 06:55:17 +08:00
00ff6de4ca fix(suggestions): stop slash-command dropdown freezing on a throwing getter (#1657)
Typing a multi-character slash command (e.g. /provider) left the dropdown
frozen on the bare "/" list with /simplify highlighted. Root cause: the
/sandbox command's `get description()` read
`SandboxManager.checkDependencies().errors`, but checkDependencies() returns
null at runtime, so `.errors` threw. That getter runs for every command while
building the Fuse search index (only for non-empty queries — bare "/" returns
before the index is built), so the throw rejected the whole updateSuggestions()
call and the list never narrowed.

Fixes:
- sandbox-toggle: null-guard checkDependencies() so the getter can't throw.
- commandSuggestions: make index building resilient — a single command whose
  description/isHidden/aliases getter throws degrades gracefully (safe
  fallback) instead of breaking suggestions for every command; a command whose
  name can't resolve is dropped. Broken getters now leave a one-time `warn`
  debug breadcrumb instead of failing silently.
- useTypeahead: command results re-rank every keystroke, so the highlight
  snaps to the top/best match (selectedSuggestion = 0) instead of following the
  previously selected command by id (which made it stick to /simplify).

Tests:
- New sandbox-toggle getter tests (null/present/missing deps, via spyOn).
- New commandSuggestions tests: narrowing, throwing description/isHidden/name
  getters, broken command still listed, and best-prefix-match-first ranking.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-16 06:54:07 +08:00
3kin0xandGitHub 5113e378a6 Feat/system prompt immediate tools (#1656)
* feat: instruct model to use tools immediately instead of waiting for user prompts

* test: add coverage for immediate-tool-use directive in REPL and non-REPL modes

* test(prompts): deterministically clear environment variables in prompt tests
2026-06-16 06:53:10 +08:00
c2cf603344 feat(ctx): add /ctx context window visualization and token bars to /cost (#1610)
* feat(ctx): add /ctx context window visualization and token bars to /cost

Adds a new /ctx slash command that surfaces exactly what the model sees
on the next API call, with per-category token bars, last-response
breakdown, session token usage, per-model totals, and a session summary.

The command reuses the same pipeline as /context (compact boundary,
optional context-collapse transform, microcompact, analyzeContextUsage)
so the totals match what is actually sent, not a rough estimate.

A single 'local' command with supportsNonInteractive: true is registered
in the public COMMANDS array (replacing the disabled /ctx_viz stub) and
added to REMOTE_SAFE_COMMANDS and BRIDGE_SAFE_COMMANDS, so /ctx works
identically in interactive REPL, headless -p, remote, and bridge modes.

/cost gains a Token usage section with colored bars for input, output,
cache read, and cache write tokens, appended after the existing
cost/duration/code-changes block without changing the per-model line.

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

* ctx-viz: fix bar scale to use contextWindow denominator

- Current Context block: use contextWindow as barMax so bars visually match percentage column
- Session Token Usage: use sessionTotalTokens as sessionMax for same reason
- Update test assertions for new tokens/contextWindow scale

* cost-tracker: add format test for token bar display

* test: add missing mocks for ctx_viz rendering test

* fix(ctx_viz): restore mocks after test; filter capacity rows from model-seen breakdown

* test: verify capacity rows are filtered from ctx viz output

- Add 'Free space' capacity row to mocked analyzeContextUsage categories
- Add assertion that rendered output does not contain 'Free space'
- Verifies CAPACITY_ROWS filtering logic in ctx-noninteractive.ts

* fix: isolate ctx_viz mocks and filter deferred categories

- Restore real modules in afterEach to prevent mock.module() leakage
  into downstream tests (autoCompact, compression)
- Filter deferred categories (isDeferred: true) from model-visible rows
  to avoid overstating context usage

* fix: eliminate mock.module() for leaky modules in ctx_viz test

Only analyzeContext.js is mocked (single data fixture). All other
modules (autoCompact, microCompact, context, model, state) use
their real implementations to avoid process-global mock.module()
leakage into downstream autoCompact and compression tests.

* fix: eliminate mock.module() entirely via renderCtxReport extraction

Extract renderCtxReport() from call() so the rendering test can
construct a hand-crafted RenderInput directly, requiring zero
mock.module() calls. This avoids process-global mock leakage into
downstream autoCompact/compression tests AND makes the test immune
to mock pollution from any preceding test file.

* chore: derive RenderInput from collectCtxData return type, use it in test

- RenderInput is now Awaited<ReturnType<typeof collectCtxData>> — single
  source of truth, no manual property duplication.
- Test imports RenderInput type and uses it in the renderCtxReport assertion
  instead of 'unknown', enabling compile-time fixture validation.

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-16 06:50:45 +08:00
ZhangandGitHub 8bce86f841 docs(test): clarify work secret localhost regression (#1637) 2026-06-15 14:50:27 +08:00
BogdanandGitHub 7448be1164 [codex] chore(query): add tool-pairing diagnostics (#1625)
* chore(query): add tool-pairing diagnostics

Add a pure validator for tool_use/tool_result pairing issues and feed phase, query source, model, provider, and agent context into the existing pre-API repair log. Keep ensureToolResultPairing behavior intact while making future repair logs identify missing, orphaned, duplicate tool_use, and duplicate tool_result cases.

* fix(query): complete pairing diagnostics coverage

Address CodeRabbit review by detecting server-side tool use blocks without matching in-message results and by making pairing validation lazy so it only runs after the repair path has actually mutated messages.
2026-06-15 09:38:21 +08:00
3kin0xandGitHub 124788b1f3 Feat/fuzzy-file-edit (#1561)
* feat(FileEditTool): add whitespace-agnostic fallback matching

* test(FileEditTool): add unit tests for whitespace-agnostic matcher

* fix(FileEditTool): preserve boundary whitespace in fuzzy match as requested by CodeRabbit

* fix: address PR feedback on token boundaries and indentation recovery

* fix: recover deep indentation for nested blocks

* fix: isolate trailing newline boundary from next line indentation

* fix: abort fuzzy match if requested indentation map conflicts

* fix: resolve typecheck error by checking adjustNewStringIndentation return value

* fix: preserve exact vertical newline count and horizontal boundary spacing

* fix: enforce strict inline whitespace and preserve Markdown hard breaks

* fix: add missing boolean argument to normalizeIndentation in adjustNewStringIndentation
2026-06-15 09:36:38 +08:00
BogdanandGitHub 9a72ecd25c [codex] fix(tokens): fallback when provider lacks countTokens (#1624)
* fix(tokens): fallback when provider lacks countTokens

* test(tokens): isolate shim fallback coverage

Avoid process-wide api client mocks in token estimation tests by exercising the count-token dispatch helper directly. Add non-empty tool coverage for the rough fallback path so the local overhead remains covered.
2026-06-15 09:35:43 +08:00
BogdanandGitHub 174ebd5125 [codex] fix(query): abort active work on QueryGuard timeout (#1623)
* fix(query): abort active work on QueryGuard timeout

* fix(query): contain timeout handler failures

* test(query): harden timeout handler cleanup

* test(query): centralize QueryGuard timer cleanup
2026-06-15 09:34:42 +08:00
BogdanandGitHub 9fbcd755a3 fix(mcp): demote successful stdio startup stderr (#1622) 2026-06-15 09:33:21 +08:00
BogdanandGitHub 5fd4a09d01 fix(read): improve oversized file guidance (#1626) 2026-06-15 09:32:21 +08:00
Ahmar YaseenandGitHub 661b5ad8cd docs(non-technical-setup): add Getting Help section with support link… (#1631)
* docs(non-technical-setup): add Getting Help section with support links and diagnostic check

The non-technical setup guide ended abruptly after the advanced setup
reference. New users — especially non-technical ones — had no clear path
to follow when they encountered issues beyond the common problems listed.

Add a Getting Help section at the end that includes:
- GitHub Discussions link for Q&A and setup help
- GitHub Issues link for bugs and feature requests
- A quick diagnostic check (openclaude --version) with actionable
  next steps for when the command is not found

This also cross-references the Windows Quick Start guide for npm Path
configuration, reusing existing documentation rather than duplicating
instructions.

Impact: improves first-run experience for the guide's target audience
without changing any code or behavior.

* docs(non-technical-setup): add Getting Help section and restructure around /provider flow

The guide previously directed users to set environment variables before launching the CLI. This misses the simpler onboarding path: users can install, run openclaude, and use /provider for guided setup inside the CLI without any env vars.

Changes:
- Remove required API key from 'Before You Start' prerequisites
- Rewrite 'Fastest Path' to recommend install -> run -> /provider
- Remove env variable step from the fast path
- Add note under provider descriptions that /provider handles setup
- Update 'Invalid API key' troubleshooting to use /provider instead of re-pasting env vars
- Add 'Getting Help' section with support links and diagnostic check

Impact: better aligns the non-technical guide with the current recommended onboarding flow. No code changes.

* docs(non-technical-setup): restore API key prerequisite, restructure Fastest Path around /provider

Re-add the API key to prerequisites. The key is still needed - the /provider flow is just the recommended way to enter it instead of env vars.

Changes:
- Keep API key in prerequisites
- Rewrite Fastest Path to install -> run -> /provider instead of env vars
- Add note under provider descriptions that /provider handles setup
- Update Invalid API key troubleshooting to use /provider
- Add Getting Help section with support links and diagnostic check

Impact: aligns the non-technical guide with the current recommended onboarding flow. No code changes.
2026-06-15 08:49:49 +08:00
BogdanandGitHub 4e56fd5921 [codex] perf(skills): cap skill listing budget (#1627)
* perf(skills): cap skill listing budget

* chore(skills): export listing budget options
2026-06-15 08:44:53 +08:00
JATMNandGitHub de726c43e1 Fix custom provider context discovery (#1620)
* Fix custom provider context discovery

Teach the custom OpenAI-compatible gateway to discover context windows from /v1/models metadata, including LiteLLM model_info context_length and max_input_tokens fields.

Use cached discovery metadata when resolving runtime context and output limits, with sync cache reads kept memoized and partitioned by endpoint, credential, and custom headers.

Add provider-profile maxContextLength env overrides and document LiteLLM context metadata plus the CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS fallback.

Cover startup discovery, runtime cache lookup, custom gateway parsing, profile overrides, and env custom-header cache partitioning with focused tests.

* Fix discovery smoke test isolation

Clear CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC during discovery service test setup so full-suite environment state cannot force startup discovery down the nonessential-traffic skip path.

Verified with:

- bun test ./src/integrations/discoveryService.test.ts

- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 targeted startup discovery test

- bun run smoke

- bun run typecheck

* Partition custom discovery startup test cache

Use a test-only custom header in the startup custom route discovery test so it exercises network discovery even when the full suite has pre-seeded the no-header custom discovery cache key.

Verified with:

- bun test ./src/integrations/discoveryService.test.ts

- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 targeted startup discovery test

- bun run smoke

- bun run typecheck

* Fix profile context override lifecycle

Add CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS to managed profile cleanup so switching profiles clears stale context-window overrides, including same-model OpenAI-compatible switches.

Preserve persisted context-window overrides when rebuilding OpenAI-compatible startup env after restart.

Verified with:

- bun test src/utils/providerProfile.test.ts src/utils/providerProfiles.test.ts

- bun run typecheck

- bun run smoke

* Detect profile context override drift

Include CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS in OpenAI-compatible active-profile env alignment so managed profiles with maxContextLength are re-applied when the override is missing or stale.

Verified with:

- bun test src/utils/providerProfiles.test.ts

- bun run typecheck

- bun run smoke
2026-06-14 20:37:11 +08:00
beardthelionandGitHub 716c1d47f6 feat(compact): auto-compact prompt on /resume + determinate progress bar (#1386)
* feat(compact): auto-compact prompt on /resume + determinate progress bar

On /resume, if the conversation exceeds 70% of the auto-compact threshold,
a dialog appears offering to compact before continuing. Shows token count,
context window usage, and effective window percentage. Also adds a
determinate progress bar during compaction that advances as the summary
streams in.

- Add RESUME_COMPACT_PROMPT feature flag (enabled)
- Add shouldPromptCompactOnResume() threshold gate
- Add ResumeCompactPrompt dialog component
- Emit compact_progress events during streaming in compact service
- Render ProgressBar next to spinner during compaction in REPL
- Add 5 tests for threshold gating logic

* feat(compact): determinate progress bar via streamed text deltas

Wire compact_progress events from streamed summary output (forkedAgent
onStreamEvent forwards text deltas) and render a determinate ProgressBar
in the REPL, replacing the indeterminate spinner during compaction.
Extracts the bar into a CompactProgressBar component shared by manual
/compact and the resume-triggered path. Emits coarse hooks/start ticks
on the session-memory path so the bar moves immediately.

* fix(compact): keep spinner visible and use bracketed progress bar

Render the "Compacting conversation" spinner throughout compaction with
the progress bar beneath it, instead of swapping the spinner out for the
bar. Redraw the bar as a bracketed fill ([███····]) with no background
rectangle, so it no longer reads as one solid block.

* docs: remove resume-compact-prompt plan file

Per PR review feedback — drop the planning doc from the PR.

* fix(compact): prompt to compact on CLI --continue/--resume startup

Startup resume paths (--continue, --resume <id>, ResumeConversation
screen) install initialMessages via the initial useState rather than
the resume() callback, so the threshold check never ran. Schedule the
prompt from the mount-time initialMessages effect as well.

* fix(compact): use MessageType alias for resumeCompactPending state

main aliases the message type import as `Message as MessageType`; the
rebased resumeCompactPending state still referenced the bare `Message`.

* fix(compact): always close session-memory progress and fix progress-ratio units

- Wrap the session-memory compaction attempt in try/finally so compact_end
  is always emitted, even when it returns null or throws, preventing a stuck
  progress bar/spinner.
- Clear compactProgressRatio in resetLoadingState so an aborted or errored
  compaction does not leave the progress bar rendered in the idle UI.
- Fix the progress denominator unit mismatch: estimatedOutputChars now uses a
  token-to-char converted estimate (preCompactTokenCount) instead of the
  token-scale preCompactTokenCount * 0.25, so progress no longer advances too
  fast and hits the cap prematurely.
2026-06-14 11:26:28 +08:00
a7d6580521 fix(plugins): prevent ENOENT on Windows marketplace cache finalization (#1500) (#1531)
* fix(plugins): prevent ENOENT on Windows marketplace cache finalization (#1500)

* test(plugins): add regression test for Windows marketplace cache finalization (#1500)

Add regression tests covering the case-only path comparison bug where
fs.rm would destroy source data when temp and final cache paths differ
only in case on case-insensitive filesystems (Windows NTFS).

Three test cases:
1. Generic mixed-case name (MyMarketplace/mymarketplace) — verifies the
   samePathCaseInsensitive guard skips rm+rename
2. GitHub-style naming (AgriciDaniel-claude-obsidian) — explicitly
   models the exact #1500 bug report scenario
3. Already-lowercase name (claude-obsidian) — verifies the string-equality
   fast path that GitHub sources hit post-fix

Also exports loadAndCacheMarketplace from _test for testability.

* test: add rename-failure fallback regression test for EXDEV

Add a regression test that forces the rename-failure fallback path in
loadAndCacheMarketplace. Uses a 'url' source with a mocked axios response
so the temp cache path (timestamp-based) truly differs from the final
cache path (marketplace.name.toLowerCase()), bypassing the
samePathCaseInsensitive guard. The test stubs rename to throw EXDEV and
verifies the cp+rm fallback correctly copies data to the final location,
cleans up the temporary path, and returns the final cache path.

* test: fix brittle temp-cleanup assertion in marketplace fallback test

Replace fragile rmSpy.call filter with direct readdirSync(cacheDir)
to verify the real temp directory state. Fixes CI failure at line 316.

* test: use pre-call snapshot for platform-agnostic fallback assertion

Replace brittle readdirSync length check with delta comparison
against pre-call directory state. Works on both case-sensitive (Linux)
and case-insensitive (Windows) filesystems.

* test: isolate marketplace mocks and fix cross-platform temp file assertion

- Move axios mock inside describe with beforeAll/afterAll lifecycle
- Filter temp_*.json files from directory delta comparison
- Add mock isolation to prevent leakage to other test files

* test(marketplace): assert renameSpy called and no temp files linger after EXDEV fallback

* test(marketplace): add cp spy and isolate axios mock to test lifecycle

Address outstanding review gaps for #1531:

1. The rename-failure fallback (cp + rm) is now explicitly verified via
   a cpSpy on the real fs/promises.cp. The mock.module('fs/promises',
   ...) registration in beforeAll wraps cp with a call-through spy, so
   the test exercises the actual filesystem fallback while still
   asserting the call args (temp source, finalCachePath dest,
   { recursive: true } option). Previously, the test only proved the
   final file existed, which could be reached by a different code path
   or by leftover pre-test state.

2. The axios mock is now isolated to the test lifecycle: the
   axiosGetSpy is created in beforeEach (with a fresh implementation)
   instead of at module scope, and the mock.module('axios', ...) factory
   uses a closure indirection so the spy can be swapped per test. This
   prevents call counts and implementations from leaking across tests
   in this suite or into other suites.

3. cpSpy.mockClear() runs in beforeEach to reset .mock.calls /
   .mock.results between tests while preserving the call-through
   behavior of the persistent mock.module() registration.

Skipped: the P1 about finalCachePath lowercasing the cache directory
(marketplaceManager.ts:1712). The current code already preserves
cacheDir and lowercases only marketplace.name, so the original review
concern was already addressed by an earlier commit.

Skipped: temp path detection via rmSpy filter and explicit temp_*
cleanup verification. Both are already covered by the existing
afterEntries/beforeEntries snapshot comparison (L334-377) and the
lingeringTempFiles assertion (L380-385).

* Revert "test(marketplace): add cp spy and isolate axios mock to test lifecycle"

This reverts commit c57ad55db18edbd235d8af337ac755fe1cb12eaa.

* test(marketplace): force real module + drop unused source.name

Two pre-existing issues that blocked typecheck and caused 4 marketplace
tests to fail in the full suite:

1. mock.module('./marketplaceManager.js', async (original) => ...)
   called original() to 'force the real module' but original() returns
   the previously-registered factory's output, which is a stale mock
   from a prior test file (e.g. officialMarketplaceStartupCheck.test.ts
   or lspRecommendation.test.ts). The stale mock has its own env-var
   behavior, so getMarketplacesCacheDir() returned the leaked
   /tmp/openclaude-marketplaces instead of the test's tempDir.

   The real module reads process.env.CLAUDE_CODE_PLUGIN_CACHE_DIR
   live on every call, so the beforeEach override takes effect
   immediately. Forcing the real module makes the env-var-driven
   cache path work correctly.

   The factory now returns undefined so Bun uses the real module
   regardless of any previously-registered mock.

2. The EXDEV test's url source had a 'name' field that does not
   exist in the MarketplaceSource.url variant of the schema
   (only url and optional headers are allowed). The field was
   unused — the test hardcodes finalCachePath from the marketplace
   manifest name (lowercased), not from source.name. Drop the
   field; typecheck goes green.

* test(marketplace): use mock.restore() to clear stale marketplaceManager mock

Previous attempt used mock.module(path, () => undefined) but Bun rejects
that — TypeError: 'mock(module, fn) requires a function that returns
an object'. mock.module() with no factory isn't a documented API, so
switch to mock.restore() which clears all module mocks.

After mock.restore(), the real marketplaceManager.js is used on import.
The EXDEV describe's beforeAll re-registers the axios mock (which
mock.restore() also cleared), so the suite-specific mock still works.

Skipped: spying on cp via setFsImplementation. The FsOperations
interface (src/utils/fsOperations.ts L23-72) has no cp method, so
adding cp: cpSpy to setFsImplementation would fail typecheck. The
rename-failure fallback is still verified via filesystem state
assertions: before/after readdirSync snapshot plus the explicit
lingeringTempFiles === [] check.

* Revert "test(marketplace): use mock.restore() to clear stale marketplaceManager mock"

This reverts commit bbbebfb305b5585f34e0a13530a5ea4ce7a9edd7.

* test(marketplace): fix mock.module TypeError (factory must return object)

0c917a1 used () => undefined to force the real module, but Bun rejects
factories that don't return an object ('mock(module, fn) requires a
function that returns an object'). Switch to mock.restore() at the
top of the file — clears all module mocks registered by prior test
files so the real marketplaceManager.js is used on import. The EXDEV
describe's beforeAll re-registers the axios mock (which mock.restore
also cleared), so suite-specific mocks still work.

The env-var-leak bug (getMarketplacesCacheDir returning the leaked
/tmp/openclaude-marketplaces instead of the test's tempDir) is a
deeper issue with how process.env is read in Bun's test runner.
Pre-existing on origin/main and out of scope for this PR; documented
in the PR body.

* test(marketplace): drop mock.restore() to let natural module resolution work

CodeRabbit round 8 P2: the previous implementation called
mock.restore() at file-scope on the assumption it would clear module
mocks registered by other test files (lspRecommendation.test.ts,
officialMarketplaceStartupCheck.test.ts). Bun's docs confirm
mock.restore() does NOT clear mock.module() registrations, so the
partial mock from the prior file persisted and the import of _test
broke ("Export named '_test' not found").

The reproducer is real: running lspRecommendation.test.ts first then
this file fails. But the root cause is a pre-existing bug in
lspRecommendation.test.ts (its mock for '../config.js' is missing
normalizeMaxMessagesCompactionThreshold, added to config.ts in
PR #1605 / commit 2002e4c). That lspRecommendation test fails on
its own on origin/main, and was already failing the lspRecommendation
test before the marketplaceManager test even ran.

Drop the mock.restore() call so the test's import relies on Bun's
natural module resolution. The marketplaceManager test passes in
isolation. The full-suite failure is caused by the pre-existing
lspRecommendation bug, not by this test. Fixing that is out of scope
for #1531.

* test(marketplace): import real marketplaceManager in full suite via cache-busted URL

CodeRabbit round 8 follow-up + jatmn round 8 P2: the test file's static
`import { _test } from './marketplaceManager.js'` resolves to whichever
mock was registered first for the bare path. Two other test files
register partial mocks at module top-level:

  - lspRecommendation.test.ts:32 mocks the path with 9 exports (no _test)
  - officialMarketplaceStartupCheck.test.ts:96 mocks it with 9 exports
    (no _test)

Whichever runs first wins, and downstream test files re-importing the
same bare path inherit that partial mock — yielding
"Export named '_test' not found in module './marketplaceManager.js'"
and 4 failing tests in the full suite.

The fix uses three techniques:

1. The static import at the top of marketplaceManager.test.ts is changed
   to `'./marketplaceManager.js?bust=this-test-needs-the-real-module'`.
   Bun treats the query-string suffix as a distinct module id, so the
   import bypasses the bare-path mock.module() registration in
   lspRecommendation.test.ts and officialMarketplaceStartupCheck.test.ts.
   The constant suffix is sufficient because we only need the import to
   happen once at module top-level — no per-test re-evaluation is
   required for the cache-sharing test block.

2. The EXDEV describe block's dynamic re-import (line 263) was already
   using a non-busted URL `'./marketplaceManager.ts'` to pick up the
   mocked axios. It gets the partial mock from the other test files
   instead, with the same `_test` is undefined failure. Apply the
   same `?bust=exdev-test-reimport` suffix so the re-import is treated
   as a fresh module id and still picks up the axios mock (because
   `'axios'` is a different module id than the busted marketplace URL).

3. The two upstream test files (lspRecommendation.test.ts and
   officialMarketplaceStartupCheck.test.ts) had stale manual mock
   lists that were missing recent exports (e.g.
   `normalizeMaxMessagesCompactionThreshold` added in PR #1605,
   `logAntError`, `logMCPError`, `isSourceInBlocklist`, etc.). These
   missing entries broke transitive imports of marketplaceManager →
   config / debug / log / marketplaceHelpers. Switch their mocks to the
   `...await import('...real=...')` spread pattern already used for
   growthbook (line 41). This preserves all real exports while
   overriding the test-specific ones — robust to future additions.

The same comment in the static-import block documents the technique
so future readers understand why a query string is part of the URL.

Verified locally: `bun test --max-concurrency=1 ./src/utils/plugins/`
runs all 37 tests across 5 files (lspRecommendation,
marketplaceManager, officialMarketplaceStartupCheck, pluginLoader,
gitAvailability) with 0 failures, regardless of file order. Each
test file in isolation also passes.

* test(marketplace): make typecheckable via @ts-expect-error + template literal

jatmn round 13 P2: the cache-busted imports from cc6c184 (commit)
were valid runtime under Bun but TypeScript couldn't resolve them,
breaking `tsc --noEmit`. Fix by combining two patterns already in the
codebase:

1. **Static import with `// @ts-expect-error`**: the top-of-file import
   uses the `?bust=this-test-needs-the-real-module` query string. The
   `// @ts-expect-error` directive above the import statement suppresses
   the TS2307 ("Cannot find module"). Pattern from
   `src/hooks/useApiKeyVerification.test.tsx:104`.

2. **Dynamic import with template literal + `as typeof` cast**: the
   EXDEV describe block's re-import (which must happen after
   `mock.module('axios', ...)` is set up) uses a template literal
   `` `./marketplaceManager.ts?bust=exdev-test-reimport-${Date.now()}` ``
   so TypeScript treats the specifier as a dynamic `string` (not a
   literal that it tries to resolve at compile time). The
   `as typeof import('./marketplaceManager.js')` cast then re-types
   the result. Pattern from `src/utils/hookChains.integration.test.ts:43-50`.

Also added `!` non-null assertions on the cached module-level
`loadAndCacheMarketplace` reference to silence the "possibly
undefined" warnings that arose when the variable was declared
without an initializer.

Verified:
  - `bun x tsc --noEmit --ignoreDeprecations 6.0` against
    src/utils/plugins/marketplaceManager.test.ts: no errors except the
    pre-existing `bun:test` ambient module declaration (which is the
    same error all *.test.ts files in the repo show).
  - `bun test --max-concurrency=1 src/utils/plugins/marketplaceManager.test.ts`:
    4/4 pass.
  - `bun test --max-concurrency=1` of the three test files that share
    marketplaceManager.js (lspRecommendation,
    officialMarketplaceStartupCheck, marketplaceManager): 11/11 pass.
  - The 16 failures in the broader smoke run are pre-existing on
    origin/main (GlobalConfig — showCacheStats, provider-specific cap
    tests for deepseek/gpt-4o/MiniMax-M2.7/etc., all added by the
    recent Fireworks AI provider upstream commit). Not introduced by
    this PR.

* debug(marketplace): log EXDEV rm/renames

* test(marketplace): use rmSync instead of mocked fs/promises rm for EXDEV fallback

openclaudeInstallSurfaces.test.ts mocks fs/promises with a no-op rm. The
EXDEV test's rm wrapper delegated to NodeFsOperations.rm which used the
mocked rmPromise, leaving temp_*.json artifacts. Use the synchronous
rmSync from 'fs' (not mocked) instead.

* fix(plugins): gate marketplace cache path comparisons on FS case-sensitivity

The cache finalization and cleanup paths compared full paths with an
unconditional .toLowerCase(), so on case-sensitive filesystems (Linux)
two distinct cache entries differing only in case were treated as the
same directory. This skipped the rename in loadAndCacheMarketplace,
skipped stale-cache cleanup in addMarketplaceSource, and made
removeMarketplaceSource delete a recomputed lowercase path that misses a
mixed-case cache dir.

Add isCaseInsensitiveFs()/pathsEqualForFs() helpers and gate both
comparisons on them, and delete the recorded installLocation in
removeMarketplaceSource instead of a recomputed path. Pins the existing
#1500 tests to win32 (the case-insensitive scenario they model) so they
stay deterministic on the case-sensitive Linux CI runner.

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

* test(plugins): verify EXDEV cp+rm fallback via an injectable fs.cp spy

The marketplace cache finalization fallback called cp() via the direct
fs/promises import, so it could not be observed through the swappable
fs implementation that rm/rename already use. The EXDEV regression test
could only assert end state, not that the cp+rm fallback branch ran.

Add cp to FsOperations/NodeFsOperations and route the finalization
fallback through fs.cp, then spy on it in the EXDEV test to assert the
copy ran exactly once with temp -> final and { recursive: true }.

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

* fix(plugins): probe cache volume case-sensitivity instead of assuming by platform

isCaseInsensitiveFs() assumed every darwin process runs on a
case-insensitive volume, but macOS can mount case-sensitive APFS/HFS+.
On those volumes two cache paths differing only in case are distinct, so
pathsEqualForFs() wrongly reported them equal and addMarketplaceSource()
skipped cleanup of the old mixed-case cache dir, orphaning it.

Replace the platform heuristic with isCaseInsensitiveFsAt(dir): Windows
is always case-insensitive (and has unreliable inode numbers) so trust
the platform there; elsewhere stat the directory under a case-flipped
name and treat the volume as case-insensitive only when both names
resolve to the same inode/dev. Memoized per directory, fails safe to
case-sensitive. pathsEqualForFs() now takes the cache dir to probe.

Tests inject statSync to simulate both volume kinds, so they're portable
(no real case-sensitive mount required).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:19:22 +08:00
822eff39d1 fix(copilot): limit sub-agent concurrency to reduce Premium Request usage (#678) (#1534)
* fix(copilot): limit sub-agent concurrency to reduce Premium Request usage (#678)

* fix(copilot): enforce sub-agent concurrency cap at Agent invocation level

AgentTool.isConcurrencySafe() now returns false when
getCopilotMaxConcurrentSubagents() > 0, preventing the tool scheduler
from batching multiple Agent calls together. This ensures at most one
sub-agent runs at a time when the cap is active.

Previously, AgentTool was always concurrency-safe, allowing the
scheduler's runToolsConcurrently to batch multiple Agent calls from
a single assistant message — bypassing the documented MAX_SUBAGENTS cap.

Add comprehensive copilotOptimization unit tests.

* fix(copilot): enforce cap for any positive value and honor OPTIMIZATION_DISABLED

- shouldForceSyncSubagentsInCopilotMode: gate on > 0 instead of === 1
  so any configured cap (2, 3, ..., 10) forces serial execution
- isConcurrencySafe: early-return true when OPTIMIZATION_DISABLED is set
- Update log message to reflect any-cap behavior

* fix(copilot): align scheduler with launch path, fix mock leak

- isConcurrencySafe now uses shouldForceSyncSubagentsInCopilotMode()
  instead of raw cap check, matching the launch path at line 447
- Add afterAll(mock.restore) to copilotOptimization.test.ts to
  prevent providers.js mock leaking to AgentTool routing tests

* fix(copilot): clarify MAX_SUBAGENTS semantics and fix remediation hint log

- Document that only MAX_SUBAGENTS=0 and =1 are enforced; values 2-10
  have no runtime effect.
- Fix the log remediation hint to depend on the actual cause: MAX=0
  suppresses sub-agents entirely (not just forces sync), FORCE_SYNC=1
  requires unsetting the flag, and MAX>=1 requires ALLOW_SUBAGENTS=1
  to restore parallel execution.

* docs(env): document GITHUB_COPILOT_* tuning vars in .env.example

The Copilot Premium Request optimization introduces four env vars
(GITHUB_COPILOT_MAX_SUBAGENTS, GITHUB_COPILOT_ALLOW_SUBAGENTS,
GITHUB_COPILOT_FORCE_SYNC_SUBAGENTS, GITHUB_COPILOT_OPTIMIZATION_DISABLED)
that change how sub-agents run for CLAUDE_CODE_USE_GITHUB=1 sessions.
Previously these were documented only in source comments, which made
them undiscoverable for users affected by the new default.

Add them to the GitHub Models section (Option 4) of .env.example with
descriptions of each var's effect and default value, addressing the
reviewer ask to put the new default behavior in user-facing docs.

* fix(copilot): telemetry reflects final async mode; docs in README

Address outstanding review gaps for #1534:

1. Telemetry is_async/isAsync now uses the final shouldRunAsync value
   computed once at the top of the function (was duplicating the partial
   expression, omitting isCoordinator/forceAsync/assistantForceAsync/
   proactiveModule signals that contribute to the launch decision).
2. The shouldSuppressSubagentsInCopilotMode() throw now happens before
   the event log (so a suppressed-agent error isn't followed by a
   misleading 'is_async: true' event).
3. isCoordinator, forceAsync, assistantForceAsync are now computed once
   alongside forceSyncCopilot instead of being declared inline later.
4. README: add GitHub Copilot sub-agent optimization subsection under
   Provider Notes, with the env var table mirroring the .env.example
   entry (default behavior, cap semantics, all-opt-out).

The doc comment in copilotOptimization.ts L16-29 already explains
MAX_SUBAGENTS=0/1 enforcement; the test at L186-191 is consistent with
the current implementation (positive cap = synchronous).

Skipped: getEffectiveConcurrencyCap() in toolOrchestration.ts (the
function no longer exists in the current code; the bot's review was
based on an earlier version).

* fix(copilot): skip <BackgroundHint /> when forced sync

When forceSyncCopilot is true the task can no longer be backgrounded
(registerAgentForeground is skipped at L918), but the background hint
UI was still rendered once the progress threshold elapsed. That
advertises a non-existent affordance on every long-running Copilot
sub-agent, which is confusing for users.

Gate the hint on the same !forceSyncCopilot condition as the
foreground registration. Address the CodeRabbit P2 on round 6.

* test(copilot): use spyOn instead of mock.module to avoid partial-mock leak

CodeRabbit P2 review on round 7 found the copilotOptimization test
registered mock.module('./model/providers.js', () => ({ only 4 exports }))
which removed all other exports of providers.ts. Downstream tests in the
same CI process (e.g. withRetry, domainCheck, apiPreconnect, agent) that
import symbols like isFirstPartyAnthropicBaseUrl would then fail with
'Export named ... not found in module' errors.

Switch to spyOn() on the real providers module's getAPIProvider. The
real module's other exports remain available, and the spy is torn
down via mockRestore() in afterEach. Also drop the cache-busting
dynamic-import pattern: the spy persists across the static import, so
the test no longer needs a fresh module per test.

Also fix README P3: the earlier PowerShell heredoc introduced a TAB
(0x09) and Form Feed (0x0C) in place of 't' and 'f' in the new Copilot
section, rendering 'tengu_agent_tool_selected' as 'engu_...' and 'false'
as 'alse'. Rewrite the line with proper 't' and 'f' characters and add
backticks for code formatting (was unformatted plain text).

Skipped: P2 scheduler-boundary coverage (CodeRabbit round 6 item).
That requires driving multiple Agent tool-use blocks through the
scheduler in AgentTool/StreamingToolExecutor, which is a larger change
than the current PR's scope.

* test(copilot): add FORCE_SYNC overrides ALLOW_SUBAGENTS precedence test

CodeRabbit round 9: add a test that pins the precedence between
GITHUB_COPILOT_FORCE_SYNC_SUBAGENTS=1 and GITHUB_COPILOT_ALLOW_SUBAGENTS=1.
The user explicitly asking for synchronous execution must win over the
softer "I'm fine with the cap" opt-out. A future reordering of the
checks in shouldForceSyncSubagentsInCopilotMode() would silently allow
parallel Copilot sub-agent launches when the user asked for sync; this
test locks the precedence.

Verified locally: 23/23 pass (was 22/22 before adding this test).

* fix(copilot): address jatmn round 11 P2/P3 and add scheduler-boundary coverage

This commit addresses the latest human + bot review feedback on #1534
across three findings:

1. **P3: Update GitHub Copilot comment in github.ts to use billing-cycle
   wording.** The previous comment hard-coded "per month (300 for
   Copilot Free)" — a calendar quota the runtime doesn't own. Mirror
   the wording from src/utils/copilotOptimization.ts: "per billing
   cycle, with the exact quota set by the user's Copilot plan." Same
   docstring shape across both files now.

2. **P2: Add afterEach cleanup to copilotOptimization.test.ts.**
   Captured the GITHUB_COPILOT_* env vars at module top-level and
   restore them in afterEach. Previously only beforeEach deleted them,
   so the precedence test (which sets FORCE_SYNC=1 + ALLOW_SUBAGENTS=1)
   left those values in process.env after the file completed. Verified
   by `bun test src/utils/copilotOptimization.test.ts ../copilot-env-probe.test.ts`:
   before the fix, the probe test sees FORCE_SYNC=1 leaked. After the
   fix, the probe sees the original env. This is the round 11 P2
   review item from jatmn.

3. **P2: Add scheduler-boundary regression test.** New file
   src/tools/AgentTool/AgentTool.copilotScheduling.test.ts pins the
   launch↔scheduler alignment by calling `AgentTool.isConcurrencySafe()`
   directly under each Copilot flag combination. Seven matrix rows:
   OPTIMIZATION_DISABLED=1, default cap=1, cap=2, ALLOW_SUBAGENTS=1,
   FORCE_SYNC=1 alone, FORCE_SYNC=1 + ALLOW_SUBAGENTS=1 (precedence),
   cap=0 (suppressed). A future reorder of the helpers in
   copilotOptimization.ts that breaks the precedence would fail
   FORCE_SYNC + ALLOW_SUBAGENTS, locking the launch/scheduling
   alignment. This is the round 9 / round 11 P2 review item from
   CodeRabbit + jatmn that has been deferred across multiple rounds.

   The test uses spyOn on providers.getAPIProvider to control the
   provider state, then imports AgentTool via cache-busting
   (?copilotScheduling=... query string) — the same pattern as
   AgentTool.routing.test.ts. Per-test timeout of 30s absorbs the
   ~16s one-time AgentTool module load (subsequent tests are sub-1ms
   because the module is cached after the first beforeAll import).

All three changes are verified locally:
  - `bun test src/utils/copilotOptimization.test.ts` — 23/23 pass
  - `bun test src/tools/AgentTool/AgentTool.copilotScheduling.test.ts` — 7/7 pass
  - `bun test --max-concurrency=1` of both files together — 30/30 pass

* test(copilot): move per-test timeout to 3rd arg (bun:test API)

* fix(copilot): let FORCE_SYNC override MAX_SUBAGENTS=0 + add scheduler-boundary test

Two review findings:

1. FORCE_SYNC vs suppression: shouldSuppressSubagentsInCopilotMode()
   returned true for MAX_SUBAGENTS=0 before FORCE_SYNC was consulted, so
   GITHUB_COPILOT_MAX_SUBAGENTS=0 + GITHUB_COPILOT_FORCE_SYNC_SUBAGENTS=1
   threw "Sub-agents are disabled" instead of running them synchronously,
   contradicting the documented behavior. FORCE_SYNC (like ALLOW_SUBAGENTS)
   now bypasses the =0 suppression; docs clarified accordingly.

2. Scheduler-boundary coverage: the existing tests only called
   isConcurrencySafe() directly. Added a regression that drives multiple
   Agent tool-use blocks through the real batching path
   (partitionToolCalls, now exposed via _test): forced-sync splits them
   into serial single-block batches, ALLOW_SUBAGENTS coalesces them into
   one concurrent batch. Catches a future divergence between launch and
   scheduling policy for multiple Agent blocks in one assistant message.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:15:34 +08:00
f4c3be850e chore: remove dead code and add knip gate to CI check (#1612)
Delete 32 unreferenced source files (~4,000 lines) verified dead by
import-specifier grep and knip: test-only token utilities, orphaned hooks
(useTaskListWatcher, useSkillImprovementSurvey + its component), the
removed DevBar and ConfigTool UIs, unregistered bundled skills (stuck,
verifyContent), unused analytics sinks, the benchmark command, and
stale migrations/helpers.

Remove unused dependencies code-excerpt, stack-utils, and tsx from
package.json plus their entries in build stub/external lists.

Add knip with a tuned knip.json (entrypoints, build-time stub targets,
subprocess-launched fixtures, and runtime-string-imported SDKs ignored;
providerAutoDetect kept intentionally as provider pre-wiring) and wire
`bun run deadcode` into the `check` script so dead code stays dead.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-14 10:13:37 +08:00
db2d093af3 fix(session): filter Anthropic-only params from 3P provider requests (#248) (#1533)
* fix(session): filter Anthropic-only params from 3P provider requests (#248)

* test: add regression tests for provider gates (PR #1533)

- betas.test.ts: add isAnthropicProvider tests for mistral, xai, minimax
- compact.test.ts: verify compactConversation skips forked-agent
  cache-sharing for non-Anthropic providers and uses it for Anthropic

These tests pin the provider gates added in PR #1533:
- getMergedBetas() returns [] for non-Anthropic (betas.test.ts)
- compactConversation()/streamCompactSummary() skip cache-sharing
  when isAnthropicProvider() returns false (compact.test.ts)

* test: fix leaky mock harness and add redacted_thinking coverage

- Complete all missing module exports in compact.test.ts mock harness to
  prevent global mock leakage breaking other test files
- Add afterAll cleanup hook for safety
- Add regression test for redacted_thinking blocks stripped before
  OpenAI-compatible replay in openaiShim

* test: eliminate mock.module leaks causing 10 CI failures

Replace broad mock.module() stubs for betas/providers/envUtils with
env-var-based provider control. Add mock.restore() safety nets to
betas.test.ts and autoCompact.test.ts. Tests now use real modules
without cross-test-file mock contamination.

* test: clear provider profile env vars and remove aggressive mock.restore

- Snapshot and clear CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED flags
  so isAnthropicProvider() returns correct values regardless of
  provider profile state from previous tests
- Remove mock.restore() from betas.test.ts and autoCompact.test.ts
  that was breaking legitimate mocks in other test files

* test(betas): ensure provider env vars are cleared in afterEach, not just beforeEach

* test(3p): also clear VENICE_API_KEY and MIMO_API_KEY per test

resolveEnvOnlyProviderRouteId (src/integrations/routeMetadata.ts:428) returns
'venice' or 'xiaomi-mimo' when those env vars are set, even when the test
sets CLAUDE_CODE_USE_OPENAI=1 — its 'env-only' check runs BEFORE the
USE_OPENAI branch (L658). Without clearing these, leaked values from
earlier tests in the same process cause isAnthropicProvider() to return
the wrong value, so the new provider-gate assertions fail:

  getMergedBetas returns [] for the openai provider
    expected: []
    received: [claude-code-20250219, interleaved-thinking-2025-05-14,
               context-management-2025-06-27, prompt-caching-scope-2026-01-05]

  isAnthropicProvider is false for the openai/gemini/mistral/xai/minimax
    expected: false  received: true

  compactConversation provider gate > uses forked-agent cache-sharing
    for Anthropic providers
    expected: runForkedAgent called  received: 0 calls

The previous 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED[_ID]' fix was
insufficient because those vars don't gate the env-only routes directly;
VENICE_API_KEY / MIMO_API_KEY do. Add them to the per-test cleanup in
both betas.test.ts (PROVIDER_ENV_KEYS) and compact.test.ts (SAVED_ENV).

Skipped: pre-existing CI failures unrelated to this PR's provider gate
(autoCompactIfNeeded circuit breaker × 7, getEffectiveContextWindowSize
on MiniMax M2, microCompact MCP, /export direct filename, getProjectMemoryPathForSelector).
These fail on origin/main with the same pre-PR failures and are out of
scope for #1533.

* test(3p): use crypto.randomUUID() for message uuids

The Message type's uuid field is now a branded UUID type from node:crypto
(strings must match the 5-segment UUID format). After rebase onto the
latest origin/main, the typecheck failed at L26 and L38 where
\\	est-\\\ no longer satisfies the type.

crypto.randomUUID() produces a valid UUID per call, so each message
gets a unique, type-correct uuid. No behavioral change for the tests
themselves — they only rely on the messages being distinct.

* test(3p): cast assistantMessage.message through 'as never'

The Message type's assistant message field is AssistantMessageContent
<BetaContentBlock>, which requires id, model, usage at the type level.
The compact code paths under test don't read these fields, so the
helper focused on the text content needs to bypass the type check.

Pre-rebase this likely passed via a wider union; after the rebase the
branded type tightened. 'as never' keeps the helper minimal without
having to fabricate realistic id/model/usage values.

* test(betas): add non-Claude GitHub regression test for provider gate

CodeRabbit P2 review on the most recent round: the provider gate tests
cover the GitHub Native Anthropic exception (Claude model) but no
sibling assertion for CLAUDE_CODE_USE_GITHUB=1 with a non-Claude model.
That untested branch is the risky half of the gate — a future broadening
of isGithubNativeAnthropicMode() (e.g. matching on the wrong substring,
or matching on 'claude' too permissively) would silently re-introduce
Anthropic-only beta headers for OpenAI-style models served via GitHub.

Add the inverse case: GitHub provider with OPENAI_MODEL='gpt-4o-mini' must
return [] (the gate strips the headers). Verified locally: betas.test.ts
runs 19/19 (with the new test) and the betas+compact pair runs 21/21
cleanly, confirming no test-isolation regression between the two files.

* docs(compact): update stale "3P default: true" comments

CodeRabbit round 10 nitpick: the comment at compact.ts L434-439 still
claims "3P default: true" and frames the GB flag as a "kill-switch",
but the code is now gated by isAnthropicProvider() so non-Anthropic
3P providers are never on this path in the first place. Update both
the compactConversation() block and the streamCompactSummary() block
(now points at the first block instead of a separate stale sentence)
to describe the actual current behavior: cache-sharing is enabled
only for Anthropic-capable providers when tengu_compact_cache_prefix
is on; non-Anthropic providers remain incompatible and would send
Anthropic-only params they reject.

Verified: 2/2 compact.test.ts still passes; no behavioral change, only
the comment text.

Skipped: rebase to current origin/main (#1533 was rebased earlier in
4c1b4c2; no new conflicts since).

* test(3p): also clear NEARAI_API_KEY per test

Reproduce locally with `bun test --max-concurrency=1` and find that
`providerValidation.test.ts` (which runs alongside the other 3800+ test
files in the same CI process) sets NEARAI_API_KEY for one of its
neearai-detect tests, and the previous cleanup list missed that env
var. So when betas.test.ts and compact.test.ts later run their
`isAnthropicProvider is true for firstParty` (and the other
`isAnthropicProvider is true for *`) tests, hasNearaiEnvOnlyProviderIntent()
returns true on the leaked value, the function classifies the provider
as 'nearai' (not Anthropic), and the gate strips all betas. The test
that sets no env var then sees `getMergedBetas()` return `[]` instead
of the Anthropic list, which is the failure pattern on CI.

resolveEnvOnlyProviderRouteId (src/integrations/routeMetadata.ts:428)
returns 'nearai' (or 'xai', 'minimax', 'venice', 'xiaomi-mimo') for
those env var leak paths, all of which were missed by the prior
profile-env-var fix. The previous CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
fix was insufficient because that var doesn't gate the env-only routes
directly — only the API keys do. NEARAI_API_KEY is the latest in the
family (xai/minimax/venice/mimo all already in the list).

Add NEARAI_API_KEY to PROVIDER_ENV_KEYS in betas.test.ts and
SAVED_ENV in compact.test.ts, matching the xai/minimax/venice/mimo
additions from 768ab65.

Verified: betas.test.ts (19/19) and compact.test.ts (2/2) still
pass in isolation and as a pair. The full-suite failure should now
resolve since the leak path is covered.

Skipped: tracing the exact leak site. The 3800+ test files can't
all be enumerated; the fix is comprehensive (all known env-only
intent vars are now in the clear list).

* test(3p): pre-warm importFreshBetas in beforeAll to avoid 5s timeout

The first call to importFreshBetas() in this file triggered a 4.3s module
load (growthbook feature-flag init), which exceeded Bun's default 5s test
timeout. On the agent's local Windows machine, that race is close to
deterministic (the openai test, being first in source order, paid the
full cost and reported `(fail) this test timed out after 5000ms`). On CI
(Ubuntu) the import was usually fast enough to pass — but flaky, and
completely unrelated to actual test correctness.

Move the slow import to beforeAll so it happens once before any test
runs and is not charged to the first test's budget. After pre-warming,
every per-test import is sub-second (the 579ms / 8ms timings in the
last run confirm this). All 21 tests in the betas+compact pair now pass
deterministically.

This is the only remaining source-order flake in this file. The actual
test logic (provider-gate assertions, env-var isolation, cache-busting
imports) was already correct.

* test(3p): add FIREWORKS_API_KEY to provider env cleanup

CI smoke run on #1533 still shows the openai/gemini/bedrock tests
failing in `getAPIProvider` even after the pre-warm + clearProviderEnv
fix from 5095f76. Tracing through the call chain reveals the missing
env var:

1. `getAPIProvider()` first checks `CLAUDE_CODE_USE_FOUNDRY` (not set,
   so passes through). It then calls
   `resolveActiveRouteIdFromEnv(process.env)`.
2. `resolveActiveRouteIdFromEnv` checks the standard
   `CLAUDE_CODE_USE_*` env vars (all cleared by `clearProviderEnv()`).
   It then calls `resolveEnvOnlyProviderRouteId(process.env)`.
3. `resolveEnvOnlyProviderRouteId` calls
   `hasFireworksEnvOnlyProviderIntent`, which returns true when
   `FIREWORKS_API_KEY` is set AND none of the conflicting API keys
   are set.
4. When the intent is true, `resolveEnvOnlyProviderRouteId` returns
   the string `'fireworks'`. The caller (`getAPIProvider`) feeds this
   into its switch statement, which has no `'fireworks'` case, so it
   falls into the `default:` branch and returns `'firstParty'`.
5. `isAnthropicProvider()` sees `'firstParty'`, returns true.
6. `getMergedBetas()` takes the anthropic-list branch and returns the
   betas list instead of `[]`.

So a leaked `FIREWORKS_API_KEY` from a prior test (PR #1590 added
fireworks to upstream `routeMetadata.ts` as the 6th `*_API_KEY` env
var) silently defeats every test in `betas.test.ts` that expects a
non-anthropic provider. The previous fix (`65ff2f76`) added
`NEARAI_API_KEY`; the Fireworks equivalent was missed because the
FIREWORKS route is "env-only" (no `CLAUDE_CODE_USE_FIREWORKS` toggle)
and was added more recently in PR #1590.

This commit adds `FIREWORKS_API_KEY` to both `PROVIDER_ENV_KEYS` in
`betas.test.ts` and `SAVED_ENV` in `compact.test.ts`. Verified:

  - `bun test --max-concurrency=1 src/utils/betas.test.ts
    src/services/compact/compact.test.ts` — 21/21 pass deterministically.
  - A synthetic leak test (sets `FIREWORKS_API_KEY='leaked'` then runs
    betas.test.ts) confirms the openai/gemini/bedrock/etc. tests now
    return `[]` and `isAnthropicProvider()` returns `false` even when
    the key is present in the parent process's env. Without the fix,
    the test would return the anthropic beta list.

After this commit, the CI smoke-and-tests job on #1533 should turn
green (the env-var-leak root cause is finally fully covered — the only
remaining smoke failures should be the same pre-existing ones observed
on origin/main: model cap tests for deepseek/gpt-4o/MiniMax-M2.7, the
`showCacheStats` registration test, etc.).

* test(3p): scrub provider env vars instead of restoring leaked snapshot

The smoke suite runs many test files in one process. Snapshotting
process.env at module load time captures values leaked by earlier test
files, so restoring from that snapshot re-introduces the leaks between
our tests. Delete every provider/profile env var before and after each
test instead; each test now starts from a known-clean state and sets only
the vars it needs.

* debug(3p): add env/provider diagnostics to openai beta test

* test(3p): restore real providers.js in betas/compact tests

* test(3p): fix providers, diskOutput, and messages mock leaks

* test(compact): restore projectInstructions/path/config mocks in afterAll

The importCompact() harness replaces these modules wholesale via
mock.module(), but afterAll only restored a subset. The
projectInstructions stub exported only getProjectInstructionFilePaths,
so every other export was undefined process-wide after this file ran.
In the full test:full order, runAgent.routing.test.ts then crashed in
processMemoryFile() with "path must be of type string, got undefined",
turning smoke-and-tests red.

Restore the real path, config, and projectInstructions modules in
afterAll alongside the others (all three were already pre-imported for
this purpose). Verified with:
  bun test src/services/compact/compact.test.ts \
    src/tools/AgentTool/runAgent.routing.test.ts --max-concurrency=1

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

* fix(compact): keep cache-sharing for GitHub Native Anthropic compaction

The compaction cache-sharing gate used isAnthropicProvider() alone, which
is false for getAPIProvider() === 'github'. But GitHub Native Anthropic
mode (CLAUDE_CODE_USE_GITHUB=1 with a Claude model) routes through the
native Anthropic client where cache_control / prompt caching works, and
the beta-header gate already preserves Anthropic betas for it. As a
result compaction always took the cold-cache path for those sessions,
reintroducing the cost the forked-agent flow avoids.

Add isCompactionCacheSharingCompatible(model) = isAnthropicProvider() ||
isGithubNativeAnthropicMode(model), mirroring the betas.ts gate, and use
it at both compactConversation() and streamCompactSummary() sites. Adds a
compact provider-gate test for GitHub Native Anthropic mode.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:12:18 +08:00
beardthelionandGitHub 9e902db866 feat(agent-routing): model-only agent routes (set the verifier or any agent's model on the current provider) (#1617)
* feat(agent-routing): allow model-only agentModels entries in settings schema

* feat(agent-routing): resolve model-only agent routes that reuse the current provider

* feat(agent-routing): enforce org model allowlist for model-only agent routes

* docs(agent-routing): document model-only routes and built-in agent keys

* test(agent-routing): stub and assert partial-entry routing warnings

Silence the intentional console.error noise from partial agentModels
entries in CI logs by stubbing console.error per describe block and
asserting the expected warning message fires, so future routing
warnings or failures are not masked.

* test(agent-routing): consolidate shouldEnforceModelAllowlist import

Move the import to the top import block instead of mid-file.
2026-06-14 10:10:37 +08:00
JATMNandGitHub 5f48ada523 Add dedicated continue command (#1618)
Add a first-class /continue command for issue #1613 so interrupted work can resume from the transcript instead of opening the /resume picker or merely acknowledging the request.

Keep /resume focused on previous-conversation selection while sharing goal/todo continuation helpers with /continue. Continue active goals without resetting turn counters or last evaluation state, resume paused goals through the goal resume path, and fall back to a hidden continuation instruction when no goal or todos are tracked.

Make goal persistence best-effort during paused-goal continuation so an on-disk save failure does not block the in-memory resume and continuation query.

Thread optional /continue hints through goal/todo continuation paths as well as transcript-only continuation.

Register and localize /continue separately from /resume, and add tests for active, paused, completed, todo-backed, picker fallback, transcript-only continuation, optional hints, persistence failures, and argument-search behavior.

Validation run: bun run build; bun run typecheck; focused resume tests; command registry tests. Prior full-check runs still have an unrelated full-suite-only LSP timeout, and raw env also needs auth for existing export tests.
2026-06-14 10:09:08 +08:00
a3a3c3659d perf(cli): restore --version fast path with dynamic provider imports (#1611)
The static imports of providerProfile.js and providerValidation.js at the
top of cli.tsx side-effect-loaded the entire integrations graph (~11.7k
lines of vendor/gateway/model descriptors) at module evaluation, defeating
the zero-import --version fast path. Convert them to dynamic imports at
their use sites, matching the file's existing convention.

--version: ~0.47s median (0.35-0.60s) -> steady 0.26s.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-13 16:11:39 +08:00
b9c65deabc feat(gemini-vertex): native Gemini Vertex client and auth helpers (1/3) (#1607)
* feat(gemini-vertex): add native Gemini Vertex client and auth helpers

Standalone foundation for the native Gemini-on-Vertex provider:

- geminiVertexClient.ts: Anthropic-shaped client for the Vertex AI
  generateContent API — message/tool conversion (functionCall /
  functionResponse, thought signatures), system instruction mapping,
  streaming and non-streaming, ToolSearch tool_reference rendering,
  temperature clamping for thinking models.
- geminiAuth.ts: Vertex helpers (project/location/model resolution from
  env, GEMINI_VERTEX_* with Google Cloud fallbacks), access-token / ADC
  credential resolution with injectable GoogleAuth for tests, and a
  default Vertex model constant.

No call sites yet — provider routing, selection and profiles land in the
follow-up PRs. 22 targeted tests, typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(gemini-vertex): pin GEMINI_VERTEX_* override precedence and blank-value handling

Addresses CodeRabbit review on the env-resolution helpers: explicit
overrides win over defaults, project id precedence is
GEMINI_VERTEX_PROJECT > GOOGLE_CLOUD_PROJECT > GCLOUD_PROJECT >
GOOGLE_PROJECT_ID, and blank values are treated as unset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(gemini-vertex): cover whitespace trimming of explicit overrides

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 07:54:42 +08:00
614a8d9c54 fix(tool-search): enable MCP tool deferral on converted-wire providers (#1608)
* feat(tool-search): enable MCP tool deferral on converted-wire providers

The CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS kill switch (defaulted on since
#281) disabled tool search everywhere, so OpenAI-compatible providers
received every MCP tool inline. Large palettes (e.g. claude-flow exposes
293 tools, ~306KB of schemas) crash the Codex backend mid-stream with an
opaque server_error surfaced as 'API Error: 500'.

The kill switch exists to keep Anthropic beta shapes (defer_loading,
tool_reference, beta headers) off the wire for accounts that lack the
betas. Converted wires never carry those shapes, so the switch is now
scoped to Anthropic-wire providers only, and the converters render
ToolSearch tool_reference results as plain text:

- toolSearch.ts: pure resolveToolSearchMode(env, provider); kill switch
  applies only to firstParty/bedrock/vertex/foundry/minimax
- codexShim.ts: render tool_reference blocks in tool results; drop a dead
  filter that targeted a nonexistent tool name
- openaiShim.ts: same rendering on the chat_completions path

Verified end-to-end on a real session: 343 tools that failed
deterministically with 500 on the Codex backend now complete (the model
defers, searches, loads and calls MCP tools across multiple turns).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(tool-search): cover tool_reference rendering on the chat/completions path

Addresses CodeRabbit review: the OpenAI shim conversion now has a focused
regression test mirroring the Codex shim coverage, via a test-only
__test.convertMessages export (same pattern as WebSearchTool).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(tool-search): assert plain-text tool message content directly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 07:53:41 +08:00
0b24b60ce9 feat(provider): add Fireworks AI as official OpenAI-compatible provider (#1590)
* feat(provider): add Fireworks AI as official OpenAI-compatible provider

Includes vendor descriptor, brand descriptor (276 models), model
descriptors (full + merged), routing metadata, env auto-detection,
profile support, client defaults, and docs.

* test: add focused regression tests for Fireworks AI auth and routing

- Add 7 env-only routing tests in client.test.ts (shim routing,
  stale model replacement, base URL override, shim option cleanup,
  non-Fireworks override ignored, priority with MiniMax, Bedrock yield)
- Add FIREWORKS_API_KEY auto-detection test in providerAutoDetect.test.ts
- Add profile apply/persistence/env-drift tests in providerProfiles.test.ts
- Fix FIREWORKS_API_KEY propagation in strictEnv early return path

* fix: address reviewer comments on Fireworks integration

- Remove OPENAI_API_KEY exclusion so Fireworks cred wins over stale OpenAI key
- Fix TS type error in test by using String() wrapper
- Add Fireworks to detection priority comment in providerAutoDetect.ts
- Add useFireworksEnvOnlyProvider to shim condition for pattern consistency
- Replace loose .includes('fireworks.ai') with isFireworksBaseUrl() exact hostname check

* fix: add explicit case 'fireworks' in applyProviderFlag for credential precedence

- Add 'fireworks' to PREFERRED_PROVIDER_ORDER
- Add case 'fireworks' with dedicated key winning pattern (mirrors atlas-cloud)
- Add FIREWORKS_API_KEY to copiedOpenAIKeyProvider detection so stale
  keys are cleaned up when switching away from Fireworks

* fix: guard fireworks defaultModel assignment against 'undefined' string coercion

* fix: remove leftover conflict marker in providerProfiles.ts

* docs(fireworks): add JSDoc to Fireworks functions for coderabbit docstring coverage

Adds JSDoc annotations to isFireworksBaseUrl, getFireworksBaseUrlOverride,
hasFireworksEnvOnlyProviderIntent, isFireworksModelName, and
applyFireworksEnvOnlyDefaults.

* fix(fireworks): cross-check NEARAI_API_KEY in env-only intent functions

hasNearaiEnvOnlyProviderIntent and hasFireworksEnvOnlyProviderIntent were
missing mutual cross-checks. When both NEARAI_API_KEY and FIREWORKS_API_KEY
are set, neither excludes the other, and nearai silently wins by ordering.
Adding !hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) to the nearai intent
and !hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) to the fireworks intent
ensures both return false, forcing explicit provider selection.

* fix(fireworks): fix typo in JSDoc — OPENAI_API_API_BASE -> OPENAI_API_BASE

* fix(fireworks): remove merge artifact and preserve no-key auth headers

- src/utils/providerAutoDetect.ts: remove leftover ======= conflict
  marker and stale duplicate priority lines
- src/utils/providerProfiles.ts: preserve apiFormat, authHeader,
  authScheme, authHeaderValue in the no-key OpenAI-compatible
  fallback path so saved Responses mode / custom auth config
  survives restart

* fix: Fireworks env-only startup preservation and MIMO priority comment

- Add FIREWORKS_API_KEY check to hasConcreteProviderSelection() so env-only
  Fireworks setup is not overwritten by Gitlawb Opengateway default
- Add regression test verifying FIREWORKS_API_KEY survives no-profile startup
- Fix providerAutoDetect.ts priority comment to include MIMO_API_KEY (position 8)
  and renumber subsequent entries to match actual detection order

* fix: also preserve env-only NEAR AI startup in hasConcreteProviderSelection()

* fix: remove duplicate Fireworks model descriptor, add FIREWORKS_API_KEY to test env cleanup

* fix: move duplicate model check to generation-time, add OPENAI_AUTH_* env cleanup to test harness

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-13 01:10:19 +08:00
d08593de92 feat(web): rebuild landing as Astro static site with gitlawb theme and full docs (#1606)
* feat(web): rebuild landing as Astro static site with gitlawb theme and full docs

Replaces the Vite+React SPA in web/ with a fully static Astro 6 site:

- gitlawb-aligned design system: dark-default monochrome surface tokens,
  Geist Mono, hairline grids, [light]/[dark] toggle — keeping the orange
  #ff7a1a openclaude accent (#e85d00 in light mode for AA contrast)
- 9-page docs section: installation, quickstart, providers, slash
  commands (all 69 user-facing commands with argument hints), CLI
  reference (every non-hidden flag), configuration, keybindings, skills —
  rendered from typed data files seeded from the CLI source
- SEO: per-page canonicals/OG/Twitter, JSON-LD (SoftwareApplication,
  TechArticle, BreadcrumbList), @astrojs/sitemap, robots.txt, and
  generated 1200x630 OG cards, all on https://openclaude.gitlawb.com
- zero framework JS: theme toggle, copy buttons, mobile nav, and TOC
  highlighting are small vanilla scripts
- CI-compatible: same typecheck/build script names, bun.lock regenerated

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

* feat(web): redesign logo as gitlawb-aligned circuit mark

White terminal face (hollow node eyes, >_ prompt mouth) on a black
square with an orange git-fork trace descending to two commit nodes —
same stroke language as the gitlawb mark. Adds the SVG source as the
favicon and regenerates all three OG cards with the new logo.

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

* fix(web): address review feedback on copy button a11y and css lint

- CopyCommand: generic aria-label (component is reusable, not
  install-specific) and a visually-hidden role="status" live region so
  screen readers announce the copied state
- global.css: lowercase text-rendering keyword, blank line before
  color-scheme, kebab-case fade-up keyframe name

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

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-12 23:54:24 +08:00
TortesandGitHub 8f92346cf4 fix: avoid file suggestion OOM on large repos (#1074)
* fix: avoid file suggestion OOM on large repos

* fix: handle ignore scope and abort semantics in file suggestions

* test: stabilize rebased branch CI verification

* test: make proxy env cleanup windows-safe

* test: preload file suggestions module in setup

* fix: keep file suggestions lazy on startup

* chore: address final review nits
2026-06-12 23:04:46 +08:00
89d05317b6 feat: add Vietnamese i18n for slash command descriptions (#1431)
* feat: add Vietnamese i18n support for slash command descriptions

Add a simple i18n helper that reads the `language` setting from config
to display localized skill descriptions. Currently supports English
(default) and Vietnamese.

To switch to Vietnamese, set in ~/.claude/settings.json:
  { "language": "vietnamese" }

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* feat(i18n): add Vietnamese translations for all 85 command descriptions

- Fix detectLocale() to read ~/.claude/settings.json directly via
  readFileSync instead of broken require('../../utils/config.js')
- Add commandDescVi translation map with 85 Vietnamese descriptions
- Export translateCommandDescription() for use in command rendering
- Modify formatDescriptionWithSource() to translate descriptions
  when language is set to "vietnamese"
- Bump version to 0.15.1

* fix: add prepare script for git-based installs

When installing via `npm install -g git+https://...`, npm runs the
`prepare` script automatically. This ensures the CLI is built from
source during installation.

Requires Bun to be installed globally.

Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>

* fix(i18n): read locale from merged settings

* feat(i18n): translate all prompt-type commands + add env validation + node version files

## Changes

### 1. Fix prompt-type command translations (src/commands.ts)
- `formatDescriptionWithSource()` now calls `translateCommandDescription()` for ALL command types
- Previously only translated `builtin`/`mcp` source commands
- Now translates: workflow, plugin, bundled, and default cases
- Fixes: /review, /insights, and other prompt-type commands now display Vietnamese

### 2. Add missing Vietnamese translations (src/skills/bundled/i18n.ts)
Added 17 new command translations:
- /btw: "Đặt câu hỏi nhanh bên lề mà không làm gián đoạn cuộc hội thoại chính"
- /compact: "Xóa lịch sử hội thoại nhưng giữ tóm tắt trong ngữ cảnh"
- /auto-fix: "Cấu hình tự động sửa: chạy lint/test sau khi AI chỉnh sửa"
- /bridge-kick: "Chèn trạng thái lỗi bridge để kiểm thử khôi phục thủ công"
- /review: "Hoàn thành đánh giá bảo mật cho các thay đổi đang chờ trên nhánh hiện tại"
- +12 more commands

### 3. Add Zod env validation at startup (src/utils/envValidation.ts)
- New file: validates critical env vars using Zod at startup
- Crashes immediately if invalid (instead of wasting time)
- Validated vars: ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CONFIG_DIR, HTTP_PROXY, HTTPS_PROXY, NODE_EXTRA_CA_CERTS
- Integrated into src/entrypoints/init.ts

### 4. Add node version files
- .nvmrc: Node 22
- .node-version: Node 22
- Matches Dockerfile (node:22-slim) and package.json engines (>=22.0.0)

## Test Results
- 3007 pass, 11 fail (all in changeDetector.test.ts - pre-existing, unrelated to i18n)

Co-Authored-By: OpenClaude <noreply@openclaude.ai>

* fix: restore validateBoundedIntEnvVar in envValidation.ts

* Localize bundled skills descriptions at read time

* fix(i18n): localize slash command suggestions

Search rendered localized command descriptions and rebuild the Fuse index when language-sensitive text changes.

Preserve Unicode letters and numbers for Vietnamese slash queries, localize the remaining requested command descriptions, and keep exact slash command submission from following a stale highlighted suggestion.

Tests: bun test src/commands.test.ts; bun test src/utils/suggestions/commandSuggestions.test.ts; bun test src/utils/envValidation.test.ts

Thanks to @jatmn for the patient review and guidance.

* fix(i18n): tighten slash command localization scope

* fix(i18n): centralize localization and preserve external metadata

* fix(commands): scope localized descriptions to OpenClaude-owned commands

* fix(i18n): read session language before initial settings

* fix(i18n): prefer whenToUse localization keys

---------

Co-authored-by: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Co-authored-by: OpenClaude <noreply@openclaude.ai>
Co-authored-by: lht3003-rgb <lht3003-rgb@users.noreply.github.com>
2026-06-12 22:24:50 +08:00
a3f144bbf2 fix(ollama): parse text-based tool calls as fallback (#1053) (#1076)
* fix(ollama): parse text-based tool calls as fallback for #1053

Ollama's OpenAI-compat streaming endpoint does not emit structured
`delta.tool_calls` fields for many models (qwen2.5-coder, llama3.x,
phi-4, gemma). Instead these models write the tool call intent as a
JSON block inside the response text, so the agent loop never receives
a tool_use event and tools are silently skipped.

Fix:
- Accumulate streamed text in `accumulatedText` during the delta loop
- At `finish_reason=stop` with no API-level tool calls, and only when
  `isOllamaProvider()` is true, parse the accumulated text with a
  regex that matches both common formats:
    {"name":"X","arguments":{...}}
    {"type":"function","function":{"name":"X","arguments":{...}}}
  (also handles ```json``` fenced blocks)
- Emit proper content_block_start/delta/stop tool_use events for each
  parsed call, then override finish_reason to 'tool_calls' so
  stopReason becomes 'tool_use' and the agentic loop continues
- The fallback is gated on isOllamaProvider() — no-op for all other
  providers

Also adds unit tests for parseTextToolCalls covering: bare JSON object,
fenced block, type:function shape, string-encoded arguments, dedup by
name:args, multiple distinct calls, plain text, malformed JSON.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama): replace non-greedy regex with brace-depth scanner for bare JSON tool calls

The regex arm `(\{"(?:name|type)"\s*:[\s\S]*?\})` stopped at the first
`}`, truncating any tool call whose arguments is a nested object (e.g.
{"file_path":"/tmp/foo.ts"}). JSON.parse then failed on the truncated
string, so parseTextToolCalls returned no calls.

Fix: split into two passes.
- Pass 1: fenced ``` blocks — regex is safe here because ``` bounds the
  non-greedy match, forcing the engine to extend past inner `}` chars.
- Pass 2: bare JSON — extractBalancedJson walks character-by-character
  tracking string/escape/brace depth and returns the full balanced object.
  processedRanges prevents double-counting inner objects nested inside an
  already-extracted outer tool call.

All 10 focused tests now pass, including the 5 that were failing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama): context guard (P1) + text buffer (P2) for text-based tool calls

P1: add trailing-context guard in parseTextToolCalls Pass 2 — bare JSON
immediately followed by non-whitespace, non-`{` text is skipped, preventing
JSON examples in explanatory prose from becoming real tool calls.

P2: buffer Ollama text deltas instead of yielding text_delta immediately.
At finish_reason=stop the buffer is flushed with tool-call JSON ranges
stripped, so raw JSON never leaks as visible text when Ollama emits
text-based tool calls.

Also changes parseTextToolCalls to return { calls, toolCallRanges } so
callers can reconstruct stripped text; adds stripRanges() helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ollama): add P1/P2 integration tests + fix TS fetch cast

- Add integration tests for think-tag filtering (P1) and visible text
  preservation before structured tool_calls (P2)
- Fix globalThis.fetch assignment cast: use `as unknown as FetchType`
  to satisfy TS overlap requirement (matches pattern in openaiShim.test.ts)
- All 15 tests pass (10 existing parseTextToolCalls + 5 new streaming)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama-shim): use stripThinkTags() instead of raw regex for think-content removal

The fallback path that strips tool-call JSON ranges from accumulatedText
was using a hand-rolled /<think>[\s\S]*?<\/think>/gi regex to remove hidden
reasoning content. stripThinkTags() (already imported from thinkTagSanitizer.ts)
handles closed pairs, unterminated opens, and orphan tags — making the cleanup
more robust without duplicating logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama): flush ollamaTextBuffer when real delta.tool_calls arrive before finish_reason=stop

* fix(ollama): open text block on flush when hasEmittedContentStart is false + regression tests

Adds a defensive guard in the no-tool-call flush path: if ollamaTextBuffer
has content but hasEmittedContentStart is false, emit content_block_start
before the delta so the text is never silently dropped.

Also adds 3 regression tests covering plain Ollama text responses (two-chunk,
single-chunk, and multi-chunk) to prevent regressions in the normal text path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama): preserve visible prose before text-based tool-call fallback

When an Ollama model emits normal text followed by tool-call JSON at
finish_reason=stop, the hasEmittedContentStart flag is false (text is
buffered, never emitted). The existing fallback only handled the case
where a text block was already open; prose was silently dropped.

Fix: lift stripped/strippedVisible computation before the
hasEmittedContentStart guard and add an else-if branch that opens a
text block, emits the visible prose, and closes it before starting the
synthetic tool_use blocks.

Also adds a regression test: Ollama stream with prose chunk then bare
JSON tool-call chunk, asserts the prose appears in a text_delta and
the Read tool_use block follows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama): apply explanatory-text context guard to fenced JSON blocks

The context guard (skip JSON followed by non-{ trailing text) was only
applied in Pass 2 (bare JSON). Pass 1 (fenced blocks) parsed all
```json``` blocks unconditionally, so explanatory fenced examples were
treated as live tool calls.

Fix: apply the same trailing-context guard to Pass 1 — if non-whitespace,
non-{ text immediately follows the closing fence, skip the block.

Also adds two regression tests:
- fenced block followed by explanatory prose is skipped
- fenced block at end of string (no trailing content) is still parsed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama): only add fenced/bare range to toolCallRanges after parseAndAdd accepts it

parseAndAdd now returns bool. Ranges are pushed to processedRanges (for
Pass-2 dedup) unconditionally, but only added to acceptedRanges (returned
as toolCallRanges) when parseAndAdd returns true. Previously, fenced or
bare JSON blocks with bad JSON, missing name, or duplicate content were
added to toolCallRanges regardless, causing their text to be silently
stripped from output even though no tool call was emitted.

Adds four regression tests: bad-JSON fence, no-name fence,
duplicate fenced blocks, and duplicate bare JSON.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ollama): address CodeRabbit review — fetch restore, isOllama threading, terminal flush, pretty JSON

- Restore globalThis.fetch in afterEach across all streaming describe blocks
  to prevent SSE stub leakage between tests
- Thread isOllama boolean from resolveProviderRequest call site into
  openaiStreamToAnthropic generator instead of re-reading env vars inside
- Flush ollamaTextBuffer for all terminal Ollama finish reasons
  ('stop','length','content_filter','safety'); only remap finish_reason to
  'tool_calls' for the original 'stop' case
- Allow optional whitespace/newlines in BARE_TOOL_CALL_START_RE so
  pretty-printed bare JSON like '{\n  "name":' is detected
- Add regression tests for pretty-printed JSON detection and 'length' flush

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ollama): add content_filter and safety terminal finish reason cases

Regression coverage requested by CodeRabbit — the OLLAMA_TERMINAL_REASONS
set includes 'content_filter' and 'safety' in addition to 'length', so add
dedicated flush-buffer assertions for those two reasons and confirm
stop_reason is not remapped to tool_use semantics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: retrigger — flaky test re-run (sessionStorage ENOENT + autoCompactCooldown race)

* fix(openaiShim): derive Ollama flag from resolved request baseUrl

Replace global isOllamaProvider() (which reads OLLAMA_BASE_URL /
OPENAI_BASE_URL env vars) with isLikelyOllamaEndpoint(request.baseUrl).
The request has already been routed through resolveProviderRequest(), so
its baseUrl is the authoritative source for provider identity; using
global env state could misroute streams when a provider profile or
per-agent providerOverride selects a different endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 22:18:54 +08:00
2002e4c116 UI/upgraded UI (#1605)
* feat(ui): OpenClaude brand identity — orange accent, wordmark, dot-pulse spinner

Give OpenClaude a real visual identity instead of reskinned upstream visuals:

- theme.ts: add brand/brandShimmer keys and repoint the claude family
  (claude, claudeShimmer, clawd_body, briefLabelClaude) to the gitlawb
  orange #ff7a1a across all 6 themes — darkened variants for light
  backgrounds, luminance-separated variants for daltonized themes,
  redBright floor for 16-color ANSI themes. Values stay rgb() strings
  (parseRGB in Spinner/utils.ts silently fails on hex).
- brand.ts (new): BRAND_NAME, BRAND_TAGLINE, accent constant, and a
  2-row Unicode half-block OPENCLAUDE wordmark split for two-tone render.
- LogoV2: full logo shows the wordmark + centralized tagline;
  CondensedLogo drops the duplicated OPEN CLAUDE header and renders a
  brand-colored name+version line; Clawd mascot body now clawd_body.
- Startup splash: new 'ember' gradient palette (brand orange) as the
  default (/logo keeps sunset et al.); tagline centralized from brand.ts.
- Spinner: dot-pulse glyph sweep (· ∘ ○ ◎ ◉ ●) replacing the asterisk
  set (drops Ghostty/darwin special-cases); defaults switched to
  brand/brandShimmer. Reduced-motion and stall interpolation unchanged.
- figures.ts: TEARDROP_ASTERISK marker → ◎ to match the new glyph family.

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

* feat(ui): built-in default statusline

Ship a status bar that renders when no custom /statusline command is
configured: `model · ctx % · session cost · rate limit`. Previously this
slot was empty unless the user wired up their own shell command.

- BuiltinStatusLine.tsx (new): pure segment builders
  (buildBuiltinStatusSegments / fitSegments — drops lowest-priority
  segments right-to-left as the terminal narrows) plus a memoized
  component reusing the exact data pipeline the custom StatusLine feeds
  to user commands (getRuntimeMainLoopModel, getCurrentUsage,
  calculateContextPercentages, getTotalCost, getRawUtilization). Pure
  in-process computation — no subprocess, no debounce.
- Context % colors warning ≥70 / error ≥90 (aligned with auto-compact
  warnings); rate limit shows the worst of the 5h/7d windows and is
  absent (not 0%) for API-key users; cost hidden at $0.
- PromptInputFooter: custom statusline always wins; the built-in bar
  also suppresses the "? for shortcuts" hint like the custom one does.
- defaultStatusLineEnabled global-config toggle (default on) surfaced in
  /config settings.
- Unit tests for segment building, narrow-width fitting, threshold
  colors, and custom-statusline precedence.

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

* feat(ui): polish pass — fuzzy match highlighting, completion flash, diff gutter, dialog hints

- Fuzzy pickers: new highlightFuzzyMatch (contiguous-run or greedy-
  subsequence, bold so the focused row's color survives) applied to
  Quick Open and prompt-history results. FuzzyPicker now passes the live
  query as a third renderItem argument so memoized render callbacks
  can't capture a stale query.
- Select: inline option descriptions now truncate with an ellipsis on
  narrow terminals instead of clipping at the edge (row made shrinkable,
  description wrapped in truncate-end).
- CompletionFlash (new): static `✓ Done · 12s` row for ~1.5s after a
  response completes. Keys off the isLoading transition (the spinner
  also hides mid-turn for streaming text); same row footprint as the
  spinner; suppressed for sub-second turns, brief mode, open
  permission/prompt queues, running teammates, and reduced motion.
- Diff gutter: line numbers on +/- lines are now dimmed (previously
  full decoration intensity competed with content, worst on light
  themes); +/- sigils keep full intensity. Mirrored in the
  non-highlighted Fallback renderer.
- Dialog: optional showNavigationHint prepends "↑/↓ navigate" to the
  default input guide; opted in from Select-hosting dialogs
  (IdleReturn, DevChannels, WorktreeExit). Dialog rewritten as plain
  React (was react-compiler output) to take the new prop safely.
- Spinner: the ↑/↓ request-direction glyph now leads the status parens
  whenever any status shows — previously buried in the tokens part,
  which only appears after 30s; width gating reserves its space.
- Replaced the stale `/`-search TODO in ScrollKeybindingHandler with a
  pointer to the shipped REPL transcript search.

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

* fix(ui): address review — explicit statusline default, narrow-terminal thinking gate

- createDefaultGlobalConfig now sets defaultStatusLineEnabled: true
  explicitly, matching the factory's other default-true booleans (read
  sites already coalesced to true; no behavior change).
- SpinnerAnimationRow: the width gate reserves mode-glyph space that a
  thinking-only spin never uses; on narrow terminals where nothing else
  renders, re-try the thinking gate with that space returned so
  '(thinking)' shows instead of nothing.

CompletionFlash NaN guard was reviewed and not applied: both refs are
useRef(0) — initialized numbers by type and construction — and the
active→inactive transition guard guarantees the start time was set
before the elapsed math runs.

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

* fix(ui): address review round 2 — hint accuracy, flash suppression, coverage

- Dialog: the cancel shortcut hint now renders only when isCancelActive,
  matching the keybinding it advertises.
- PromptInputFooter: new exported resolveFooterStatusLine(settings,
  guards, config?) is the single source of truth for which status line
  renders — custom wins over builtin, and all render guards (prompt
  mode, short fullscreen, exit message, pasting) force null. suppressHint
  and the render now share it, so '? for shortcuts' is suppressed only
  when a status line actually renders.
- CompletionFlash: suppression now clears an already-active flash (the
  effect resets state and the render guard skips the commit-gap frame)
  instead of only preventing new ones.
- REPL: flash suppression reuses the existing hasActivePrompt aggregate,
  which covers the sandbox, worker-sandbox, and elicitation queues the
  hand-rolled expression missed.
- builtinStatusLineShouldDisplay takes an injectable config (defaults to
  getGlobalConfig()) so the config-off path is testable without module
  mocks; tests added for config-off, custom-wins-regardless, and the
  resolver's full variant x guard matrix.

Validation: typecheck exit 0; full suite 3733 green; smoke + bundle
guard green.

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

* fix(ui): drop fit-budget floor, cover reduced-motion render gap

- BuiltinStatusLine: remove the artificial 10-column floor on the fit
  budget — fitSegments returning [] is the signal that nothing fits,
  and the empty branch already handles it (row-reserve in fullscreen,
  null otherwise). The floor forced a truncated segment onto panes
  narrower than the model name.
- CompletionFlash: the render guard now also skips the one frame
  between reducedMotion flipping on and the effect clearing the flash,
  same as the suppressed case.

Component-level mount tests for PromptInputFooter were considered and
deliberately not added: the footer needs 10+ mocked contexts to mount,
mock.module harnesses leak across bun test files in this repo, and the
branching under review is exactly the pure resolveFooterStatusLine
contract already pinned by tests.

Validation: typecheck exit 0; full suite 3733 green; smoke + bundle
guard green.

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

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-12 21:59:32 +08:00
BogdanandGitHub 3752dfe6f7 fix(typecheck): recreate missing CLI Transport interface (#1581)
* fix(typecheck): recreate missing CLI Transport interface

* fix(transports): implement async close in CLI transports

* fix(transports): harden async close cleanup

* fix: address async transport close review feedback

* test: isolate environment-sensitive suites

* fix(transports): drain uploader after close failure

* fix(transports): type guard CCR stream events

* test: remove unused auto-compact fixture helper
2026-06-12 15:17:56 +08:00
94d2a6a503 ci: split typecheck into its own PR-checks job (#1599)
The Typecheck step lived inside the smoke-and-tests job and
typecheck:type-tests ran inside `bun run check`, so type errors were
buried mid-job and serialized behind the build. They now run as a
dedicated parallel `typecheck` job (tsc --noEmit + the focused type
tests) with its own status check, and `check` slims to
smoke + test:full so nothing runs twice in CI. Local scripts
(typecheck, typecheck:type-tests, hardening:strict) are unchanged.

Review feedback: the new job's checkout sets
persist-credentials: false (no credentials needed), and
CONTRIBUTING.md now documents typecheck as a CI-enforced check
instead of a recommended-local-only one.

Validation: workflow YAML parses (jobs: smoke-and-tests, typecheck,
web); typecheck exit 0; type-tests green; `bun run check` green.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-12 10:55:11 +08:00
JATMNandGitHub 9c742319fb Fix snip metadata leaks and TUI corruption (#1600)
* Fix snip metadata leaks and TUI corruption

Replace visible snip ID tags with internal snip_id metadata and update SnipTool guidance/tests so the model can request pruning without echoing user-visible IDs.

Add compatibility parsing for legacy [id:...] markers, clarify synthetic OpenAI shim tool-result messages, and tighten test cleanup around env/session state.

Mitigate Konsole+tmux rendering corruption by disabling the scroll fast path in that environment while preserving bottom-follow behavior and clearing culled cached output.

Validation: bun run build; bun run smoke; ANTHROPIC_API_KEY=test-key bun run check; ANTHROPIC_API_KEY=test-key bun run test:full; python -m pytest -q python/tests; bun run security:pr-scan -- --base upstream/main.

* Stabilize cooldown smoke test

Use usage-bearing high-context fixtures in autoCompactCooldown tests so the cooldown assertions do not depend on process-global threshold overrides surviving full-suite order. Add CodeRabbit-requested SAST suppression comments for snip regex literals.

* Use rule-id semgrep suppressions

Update the snip regex suppressions to CodeRabbit's requested nosemgrep rule-id form, with the explanatory text kept in a separate comment.

* Pin cooldown test context window

Set and restore CLAUDE_CODE_AUTO_COMPACT_WINDOW in autoCompactCooldown tests so the high-context fixture lands above the auto-compact threshold but below the hard prompt limit on both local Windows and Linux CI.

* Honor autocompact breaker metadata

Block oversized requests when autocompact reports an active or tripped breaker, even if a later auto-compact config read is stale. This keeps cooldown protection active and stabilizes the CI smoke path.
2026-06-11 13:32:43 +08:00
eacc7d8fac feat: add NEAR AI provider integration (#1594)
* feat: add NEAR AI provider integration

- Create vendor, brand, and model descriptors for NEAR AI (22 models)
- Add NEAR AI to route metadata, client, provider auto-detect, and profiles
- Update compatibility tests and ProviderManager test PRESET_ORDER
- Add README and docs entries for NEAR AI provider
- Update .env.example with NEAR AI configuration

* fix: address CodeRabbit review comments

- Fix .env.example: change 'Option N' to 'Option 11' in quick reference
- Narrow isNearaiModelName to use explicit NearAI model prefixes instead of broad includes('/')
- Add NEARAI_API_KEY propagation in strictEnv startup path

* fix: align NEAR AI validation host matching with wildcard subdomain routing

- Add *.completions.near.ai to matchBaseUrlHosts in vendor descriptor
- Add matchHostnameAgainstRouteHosts helper with wildcard (*.) prefix support
- Use helper in both resolveRouteIdFromBaseUrl and getRuntimeValidationTarget
- Add regression test for qwen35-122b.completions.near.ai TEE endpoint
- Add NEARAI_API_KEY to test env cleanup list

* fix: align Near AI integration with env-only provider best practices

- Replace loose .includes('near.ai') with isNearaiBaseUrl() in providerProfiles.ts
  for exact hostname validation (all 4 instances)
- Add NEARAI_API_KEY to copiedOpenAIKeyProvider detection in providerFlag.ts
- Add case 'nearai' to applyProviderFlag switch with dedicated key precedence
- Add 'nearai' to PREFERRED_PROVIDER_ORDER
- Add useNearaiEnvOnlyProvider to OpenAI shim condition in client.ts
- Remove OPENAI_API_KEY exclusion from hasNearaiEnvOnlyProviderIntent (dedicated
  key wins over stale generic key, consistent with xAI pattern)
- Update detection priority comment in providerAutoDetect.ts to include
  MIMO_API_KEY, XAI_API_KEY, and NEARAI_API_KEY

* fix: add exact completions.near.ai host to isNearaiBaseUrl

* fix: add higher-precedence provider key exclusions to hasNearaiEnvOnlyProviderIntent

* fix: add OPENAI_API_KEY and MINIMAX_API_KEY exclusions to hasNearaiEnvOnlyProviderIntent

* fix(near-ai): don't let stale OPENAI_API_KEY suppress Near AI routing

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-11 12:09:25 +08:00