1189 Commits
Author SHA1 Message Date
0xfandomandGitHub 059ec5e8b0 fix(code-indexing): guard command detection against prototype-chain names (#1710)
detectCodeIndexingFromCommand looked up the command's first word in a plain
object (CLI_COMMAND_MAPPING). A command whose first word collides with an
inherited Object.prototype member resolved to the prototype value instead of
undefined: the bare lookup returned the Object constructor for `constructor`,
and the npx/bunx branch used `in`, which walks the prototype chain. Both
falsely reported a code-indexing tool, so running e.g. `constructor ...`
emitted a bogus tengu_code_indexing_tool_used telemetry event whose `tool`
field was a function.

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

* fix(query): isolate lifecycle tracking context

* fix(query): guard lifecycle metadata updates

* fix(query): track lifecycle during tool waits

* test(query): cover bash lifecycle metadata

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

* fix(query): disambiguate lifecycle abort log reason

* fix(query): preserve foreground subagent lifecycle tracking

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

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

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

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

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

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

* fix(status): include route status defaults

* fix(status): address route status review findings

* fix(status): cover route secret redaction review

* fix(status): avoid duplicate route resolution

* fix(status): redact base URL query credentials

* fix(status): harden status URL secret redaction

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

* test(status): cover fallback URL fragment redaction

* test(status): isolate route status provider imports

* fix(status): redact encoded route secrets

* fix(status): redact encoded query secrets safely

* fix(status): redact nested encoded query secrets

* fix(status): redact encoded secret substrings

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

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

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

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

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

* test: enable UNATTENDED_RETRY feature in bun test scripts

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

* test: cover persistent retry cap driven through real gate

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

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

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

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

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

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

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

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

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

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

* feat: emit telemetry when persistent retry cap is reached

* fix: surface changelog cache-write failures in migration

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

* fix: split mkdir and writeFile in changelog migration

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

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

* Fix leftover conflict marker in QueryEngine.ts

* fix: remove stale retry guard and handle mkdir EEXIST

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: normalize REPL bridge permissionMode against EXTERNAL_PERMISSION_MODES

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

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

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

* fix: restore missing retryableRateLimit declaration in persistent retry test

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

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

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

* test: preserve openai shim response body on parse failure

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

Addresses PR review feedback (two P2s).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Fix config-dir warning and docs review findings

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

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

* Align configuration docs with openclaude paths

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

* Align keybindings docs with openclaude config home

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

* Align skill and hook labels with openclaude paths

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

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

* Resolve config-home paths dynamically in skill prompts

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

* Update active command prompts for openclaude paths

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

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

* Fix remaining config path review findings

* Cover dynamic config paths in UI and storage tests

* Fix config path smoke failures after rebase

* Fix remaining config path review findings

---------

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

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

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

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

* fix(status): address status redaction review feedback

* fix(status): redact provider base URL secrets

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two review findings:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

* fix(cache): honor legacy OpenAI base fallback

* fix(cache): normalize OpenAI base URL hints

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

* fix(cache): ignore undefined OpenAI base hints

* fix(cache): sanitize prompt cache route labels
2026-06-18 08:42:41 +08:00
BogdanandGitHub beab67b44a fix(session-title): harden generated title handling (#1691)
* fix: harden session title generation

* fix(session-title): preserve prompt fallbacks after generation failure

* fix(session-title): address fallback review findings
2026-06-18 08:41:31 +08:00
NikhilandGitHub 6fbbf2dffc fix(provider): honor explicit CLAUDE_CODE_USE_OPENAI=0 on fresh startup (#1690)
When no provider profile is saved, buildStartupEnvFromProfile defaults to
the Gitlawb Opengateway profile, which sets OPENAI_BASE_URL and re-enables
the OpenAI-compatible route. If the user has explicitly opted out with
CLAUDE_CODE_USE_OPENAI=0, this fallback overrode that choice and the startup
validator then reported a spurious "OPENAI_API_KEY is required" warning,
even in a clean env -i environment.

Honor the explicit opt-out in the no-saved-profile branch by returning the
process env unchanged instead of injecting the default profile. Saved
non-OpenAI profiles are unaffected and still applied.

Closes #1245
2026-06-18 08:40:33 +08:00
BogdanandGitHub 23cfc242ed fix(query): add activity-aware query guard leases (#1686)
* fix(query): add activity-aware query guard leases

* test(query): cover tool query activity lifecycle

* fix(shell): align runtime and lease timeouts

* fix(query): cap shell timeouts to query budget
2026-06-18 08:38:37 +08:00
JATMNandGitHub bd39c3a558 Add GitHub Enterprise Copilot support (#1685)
* feat(github): add GitHub Enterprise Copilot support

Add GitHub Enterprise onboarding paths for Enterprise URL and direct Copilot key auth, including URL normalization and credential storage metadata for Copilot keys.

Route GitHub Enterprise requests through the Enterprise Copilot API, update device-flow endpoints, preserve profile/startup env for GHE, and ensure direct Copilot keys win over stale OpenAI auth.

Add a GitHub Enterprise gateway descriptor and generated integration artifact entry, plus regression coverage for request routing, validation, shim auth, profile handling, onboarding env cleanup, and secure-storage hydration.

Validation run: bun run build; bun run typecheck; bun run typecheck:type-tests; bun run integrations:check; bun run test:provider; bun run test:provider-recommendation; bun run security:pr-scan; python -m pytest -q python\\tests. Known unrelated full-suite failures remain in export direct filename, marketplace cache finalization, and platformStorage tests.

* fix(github): address enterprise review feedback

Preserve explicit OPENAI_API_KEY for GitHub shim requests unless a direct GITHUB_COPILOT_KEY is set.

Keep saved GitHub Enterprise URLs when reactivating an existing onboarding login and ignore literal undefined Enterprise env values.

Persist and validate github-enterprise provider profiles without downcasting them to public GitHub.

* fix(github): honor enterprise env startup intent

Treat GITHUB_ENTERPRISE_URL and GITHUB_COPILOT_KEY as complete GitHub startup selection so saved profiles do not override env-only Enterprise setups.

Normalize path-bearing Enterprise URLs to the instance origin before constructing OAuth and Copilot token endpoints.

* test(github): cover enterprise device flow routing

Exercise requestDeviceCode with a path-bearing gheUrl and assert the actual fetch URL uses the normalized Enterprise device-code endpoint.
2026-06-18 08:36:54 +08:00
JATMNandGitHub 3135e731c9 fix: WSL stdin handling (#1679)
* Fix WSL stdin handling

Keep the default App stdin path in readable mode without resuming the stream, so WSL PTYs do not switch into flowing data mode and freeze after typed prompts or slash commands. Preserve the opt-in data-mode behavior by only calling resume() for OPENCLAUDE_USE_DATA_STDIN.

Add App stdin mode coverage for the default readable path and data-mode fallback. Harden timeout/status notice behavior and tighten test isolation for the full local check suite.

Validation: bun run build; bun run typecheck; bun run typecheck:type-tests; bun run security:pr-scan; bun run check; WSL2 PTY smoke with node dist/cli.mjs --bare --setting-sources user and /help input.

# Conflicts:
#	src/hooks/fileSuggestions.test.ts

* Address PR review feedback

Restore the file-index test mock alongside the other file suggestion dependency mocks to prevent cross-suite leakage.

Cover OPENCLAUDE_USE_READABLE_STDIN=0 as the backward-compatible data-mode env gate.

Log status notice predicate failures to the debug log before treating the notice as inactive.

Validation: bun test src\\ink\\components\\App.test.tsx src\\hooks\\fileSuggestions.test.ts --max-concurrency=1; bun run typecheck; bun run check.

# Conflicts:
#	src/hooks/fileSuggestions.test.ts
2026-06-18 08:18:44 +08:00
JATMNandGitHub 2351811b3c docs: add AI agent contribution guidance (#1676)
Add AGENTS.md with repository-specific guidance for AI coding agents, including runtime conventions, validation commands, provider-change expectations, and limits on new Python work.

Update CONTRIBUTING.md with maintainer-directed contribution expectations, CodeRabbit follow-up requirements, duplicate PR guidance, and a link to AGENTS.md.
2026-06-17 11:51:50 +08:00
0xfandomandGitHub da551e6d05 fix(model): preserve [1m] tag for the 'best' alias (#1671)
parseUserSpecifiedModel appended the [1m] (1M-context) tag for the opus,
sonnet, and haiku aliases but not for 'best'. Since 'best' resolves to the
same model as 'opus' (getDefaultOpusModel), 'best[1m]' silently dropped the
1M-context request while 'opus[1m]' kept it — so a user pinning 'best[1m]'
lost the larger context window.

Append the tag for 'best' as well, matching the other aliases. Add a
relational regression test (best[1m] tracks opus[1m], tag is case-insensitive
and not duplicated).
2026-06-17 11:49:20 +08:00
NikandGitHub de6b6bdd03 fix(context): treat Opus 4.7 as 1M-context capable in modelSupports1M (#1670)
modelSupports1M only matched claude-sonnet-4 and opus-4-6, but the firstParty
default Opus is now claude-opus-4-7 and the default session model is
claude-opus-4-7[1m] (getDefaultMainLoopModelSetting). With 4.7 unmatched,
resolveSkillModelOverride drops the [1m] suffix when a skill specifies
`model: opus` on an Opus 4.7 session, silently downgrading the effective
window from 1M to 200K and tripping autocompact / "Context limit reached" at
~23% apparent usage. The same predicate gates the beta-based 1M window path in
getContextWindowForModel.

Add opus-4-7 to the predicate (the @[MODEL LAUNCH] checklist item missed at
the 4.6 to 4.7 default bump) and cover modelSupports1M with regression tests,
including the disable-switch path.
2026-06-17 11:48:29 +08:00
0xfandomandGitHub 1b33fa62b8 fix(provider): match xAI base URL by hostname, not 'x.ai' substring (#1669)
The xAI credential-mirroring checks in providerProfiles.ts used
`baseUrl.toLowerCase().includes('x.ai')`. That substring matches unrelated
hosts — e.g. `vertex.ai`, `essex.ai`, `max.ai` all contain `x.ai` — so an
OpenAI-compatible profile pointed at such a host was wrongly treated as xAI:
its api key got mirrored into XAI_API_KEY and route detection flipped to
`xai`, breaking model routing.

Route the three sites (profileSecretsAreComplete, the openAI profile-env
builder, and the active-profile env builder) through the existing
`isXaiBaseUrl` helper, which matches `hostname === 'api.x.ai'` — consistent
with how isFireworksBaseUrl/isNearaiBaseUrl are already used in this file.

Add a regression test asserting a `vertex.ai` profile does not set
XAI_API_KEY and is not detected as the xai provider.
2026-06-17 11:47:27 +08:00
BogdanandGitHub 544b857876 fix(settings): correct stale settings path references (#1666)
* fix(settings): correct OpenClaude settings paths

* fix(settings): address review path clarity

* fix(sandbox): protect OpenClaude settings in changed cwd
2026-06-17 11:44:03 +08:00
BogdanandGitHub 29aea4969d fix(provider): centralize provider secret redaction (#1665)
* fix(provider): centralize provider secret redaction

* fix(system-check): prefer base URL route credentials

* fix(provider): avoid false credential matches

* fix(provider): redact jwt-shaped tokens

* fix(provider): redact embedded diagnostic secrets

* test(system-check): isolate provider env keys
2026-06-17 11:23:15 +08:00
Ahmar YaseenandGitHub e733908a91 fix(claude-desktop): add native Windows support for MCP server import (#1653)
* fix(claude-desktop): add native Windows support for MCP server import

SUPPORTED_PLATFORMS in platform.ts only included 'macos' and 'wsl', causing getClaudeDesktopConfigPath() to throw on native Windows. Additionally, no Windows path handler existed -- the function only handled macOS then fell through to WSL-specific /mnt/c/Users/... paths.

Changes:
- Add 'windows' to SUPPORTED_PLATFORMS in platform.ts
- Add Windows path handler in getClaudeDesktopConfigPath() using %APPDATA%
- Update error messages to reflect Windows support

Impact: unblocks Claude Desktop MCP server import on native Windows.

* docs(claude-desktop): add docstrings to satisfy coverage threshold

* fix(claude-desktop): re-throw APPDATA error and add test coverage for Windows path

The APPDATA error was being swallowed by readClaudeDesktopMcpServers() catch-all, silently returning {} instead of surfacing the misconfigured environment to the user.

Changes:
- Re-throw APPDATA error in readClaudeDesktopMcpServers() so users see the configuration issue instead of silently getting no servers
- Add claudeDesktop.test.ts with tests for the Windows APPDATA path and missing-APPDATA error

Impact: users on Windows will now see a clear error if APPDATA is unset, instead of silently getting an empty server list.

* test(claude-desktop): use try/finally for env isolation and explicit fixture values

* fix(claude-desktop): opt-in import, update help text, mock platform in tests

* test(claude-desktop): save and restore real platform module to avoid leaking mock to other tests

mock.module is process-global in bun. Previous approach registered a mock for ./platform.js that affected all other test files importing from it, breaking the full test suite on CI. Now saves the real module via dynamic import before registering the mock, and restores it in afterAll.

* fix(claude-desktop): use win32.join for Windows APPDATA path to fix cross-platform test failure

path.join() on Linux uses forward slashes, producing mixed separators when joining a backslash-based APPDATA value. Since %APPDATA% is always a Windows path, use win32.join() which always uses backslashes regardless of host OS. Also update the test expectation to use win32.join() so it matches on any platform.

* test(claude-desktop): add readClaudeDesktopMcpServers rethrow test and fix env restore

- Added test for readClaudeDesktopMcpServers verifying APPDATA error surfaces instead of being swallowed
- Added restoreAppData helper that uses delete when original was undefined to avoid string coercion to 'undefined'
- Updated existing tests to use restoreAppData

* test(claude-desktop): remove mock.module to fix CI, add pure-logic path test

mock.module is process-global in bun and cannot be safely restored, causing getPlatform() to return 'windows' for all subsequent tests on CI — which triggered findGitBashPath() -> process.exit(1) on Linux.

- Remove mock.module('./platform.js', ...) entirely
- Guard Windows-dependent tests with if (process.platform !== 'win32') return
- Add pure-logic test using win32.join that validates path construction without any module mocking, works on all platforms

* refactor(claude-desktop): extract pure helper getWindowsClaudeDesktopConfigPath

Extract APPDATA path construction and missing-APPDATA error into a pure, synchronous helper function that takes appData as a parameter. This lets the core Windows logic be tested on any platform without mocking process.env, addressing CodeRabbit's concern that the previous if(isWindows) guard left changed behavior untested on CI.

- Two new unconditional tests exercise the helper on all OS runners
- if(isWindows) integration tests remain for end-to-end coverage on Windows
- No behavioral change

* revert opt-in import, update readClaudeDesktopMcpServers jsdoc

Addresses reviewer findings: - Revert defaultValue back to {t14} in MCPServerDesktopImportDialog — the opt-in UX change is scope creep for this Windows-support PR - Update readClaudeDesktopMcpServers JSDoc to document that it can throw on Windows when APPDATA is unset
2026-06-17 11:11:10 +08:00
BogdanandGitHub a1b3346f65 feat(cli): add local background sessions (#1642)
* feat(cli): add local background sessions

Add local detached background sessions backed by an OpenClaude-owned registry under the resolved config directory.

- implement --bg spawning plus ps, logs, logs -f, kill, and an explicit attach limitation
- harden registry metadata validation, atomic writes, ID/name collision handling, and terminal-name reuse
- precreate child log files with precise ownership cleanup and register metadata only after spawn succeeds
- verify live PIDs against the session command before treating registry entries as running
- wait for process-tree termination and escalate to SIGKILL before marking sessions killed
- skip live local background sessions during --continue transcript selection
- preserve Node heap flags for detached children while avoiding stale launcher relaunch state
- handle -- separators so dash-prefixed prompts remain positional
- document storage, safety model, name reuse, and the current attach limitation

Validation:
- bun test
- bun run typecheck
- bun run smoke
- isolated built-CLI --bg/ps/logs/kill smoke
- CodeRabbit review findings addressed

* test(utils): prevent bg registry mock leakage

Restore complete bg registry and UDS module mocks after conversation recovery tests so Bun's process-global mock.module registry cannot leak partial module exports into later CLI tests.

CI exposed this under Bun 1.3.13 when conversationRecovery.test ran before the bgRegistry and bg CLI test files.

* test(utils): exercise bg registry without global mock

Replace the conversation recovery bgRegistry module mock with real registry metadata backed by a short-lived live child process. This keeps UDS as the only mocked boundary and avoids leaking a mocked registry module into later CLI registry tests under Bun 1.3.13.

* test(utils): isolate background registry state

Stop the conversation recovery test from using process-wide bgRegistry mocks or real child processes by injecting the live-session dependencies directly.

Pin and serialize the bg registry test config directory through the shared env mutation lock so path/cache state cannot leak from neighboring tests under Bun CI ordering.

* test(utils): document Bun mock restoration

Explain why conversation recovery tests re-register full module exports after mock.restore(), matching the CodeRabbit-requested Bun 1.3.13 isolation workaround.

* test(cli): isolate background registry root

Avoid relying on process-wide CLAUDE_CONFIG_DIR state in bgRegistry tests. Use a registry-local test root override so CI file ordering and mocked path modules cannot redirect background session metadata into another test's temp directory.

* test(utils): cover live session fallback paths

Add focused coverage for collectLiveBackgroundSessionIds when UDS discovery fails but registry data remains available, and when registry refresh fails but UDS data remains available.

* fix(cli): harden background session management

Validate persisted and newly-created background session PIDs before exposing them to management commands.

Reserve named live sessions with an atomic registry write, release reservations when sessions become terminal, and cover concurrent duplicate-name attempts.

Split local session management dispatch from background spawning so ps/logs/attach/kill avoid provider startup while --bg still inherits profile routing.

* fix(cli): address background session review findings

Preserve positional prompts when --bg is combined with optional-value flags such as --debug.

Recover stale name reservations whose owner metadata is missing or terminal while preserving in-flight reservations from live creators.

Cover both reviewer findings with focused parser and registry regression tests.

* fix(cli): respect delimiter for background flags

Limit background and print-mode flag detection to arguments before the -- delimiter so flag-shaped prompts remain positional.

Keep optional resume/from-pr flags out of the required-value table and add regressions for delimiter and optional-flag prompt handling.

* refactor(cli): share delimiter argument helper

Move args-before-delimiter handling into the existing dependency-free CLI args utility.

Use a dynamic import from the entrypoint so background flag routing shares the helper without adding top-level module load to version and management fast paths.

* test(cli): cover background entrypoint routing

Export the CLI entrypoint for controlled tests and add isolated importer injection so runtime routing tests do not leak global module mocks.

Replace the delimiter source-layout assertion with execution-level coverage for management commands, real background flags, and flag-shaped prompt text after --.

* fix(cli): preserve background resume selectors

Keep space-separated --resume, -r, and --from-pr values attached when building background child args.

Mark live background sessions stale when PID command identity cannot be read, avoiding termination of reused unrelated PIDs.

* fix(cli): track unknown background session identity

Represent unreadable live PID identity as a non-terminal unknown state so active sessions stay excluded from resume selection.

Refuse to terminate unknown live PIDs because the process command cannot be positively matched to the background session.

* fix(cli): honor background resume selectors

Avoid adding a generated --session-id to non-forked background resume launches so the spawned print-mode child satisfies the existing resume/session-id contract.

Pass --from-pr through headless print mode and resolve PR-linked sessions through the shared conversation recovery path.

Add regression coverage for background resume launch args and PR selector matching.

* fix(cli): treat PR resume as headless resume source

Include --from-pr in print-mode resume guards so PR-linked headless resumes can run without a prompt and share resume-only options.

Skip eager startup hooks for headless PR resumes and add explicit --session-id launch coverage.

* fix(cli): keep background PR resumes live

Resolve non-forked --from-pr background launches to the selected transcript id before writing registry metadata.

Preserve PID identity refresh for PR-resume children by matching the stored invocation when argv does not carry the transcript id.

Add regressions for launch registration and registry refresh.

* test(cli): cover PR resume lookup failures

Add regression coverage for non-forked background --from-pr launches when the selector cannot be resolved.

Verify the launch planner returns the same clear error used by handleBgFlag().
2026-06-17 11:09:23 +08:00
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