* feat(aimlapi): provider foundation — config, catalog, ambient-key gate
First layer of the AI/ML API onboarding split. Self-contained: config
endpoints + partner-header resolution, gateway catalog entry, runtime
metadata and artifact-generator wiring, and the regenerated integration
manifest.
Also lands the P1 security fix in providerProfiles: an ambient AIMLAPI
key is only forwarded when the profile targets the canonical inference
endpoint, so a proxy/staging profile can never leak the key elsewhere.
No client/checkout/UI changes here — those stack in later PRs.
* fix(aimlapi): harden credential gating and restore renamed-preset test path
* fix(aimlapi): harden credential gating and finish renamed-preset path
* fix(aimlapi): finish aimlapi.com rename in provider UI; gate generic proxy credential
* fix(aimlapi): close remaining proxy credential and attribution leaks
* fix(aimlapi): withhold ambient credentials and attribution from proxies
* fix(aimlapi): withhold ambient credentials and attribution from proxies
* fix(aimlapi): stop forcing profile credentials onto retargeted endpoints
* fix(aimlapi): withhold ambient custom headers from proxy launches
---------
Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
* feat(provider): route GPT-5.6 models to the OpenAI Responses API
GPT-5.4/5.5/5.6 (incl. gpt-5.6-sol/terra/luna) reject function tools +
reasoning_effort on /v1/chat/completions, so an agent CLI (which always
sends tools) can't use them. Add a model+base predicate
(modelRequiresResponsesApi) that auto-selects the existing /v1/responses
transport for these models on api.openai.com and Azure OpenAI hosts.
Precedence: explicit responses/responses_compat > catalog
requiredApiFormat > explicit chat_completions > predicate > default. The
gpt-5.6 catalog entries deliberately set no requiredApiFormat so the
chat_completions escape hatch works for them. Register the gpt-5.6
descriptors and openai-vendor catalog entries (with reasoning metadata so
buildResponsesBody emits nested reasoning.effort).
Also fix a latent bug: the responses branch of buildRequestUrl emitted a
bare ${base}/responses and skipped Azure handling, so a forced/auto
responses route 404'd on Azure. It now mirrors buildChatCompletionsUrl —
deployment-style bases get the deployment path + api-version, bases
already containing /deployments/ keep their path and gain api-version,
while the modern Azure v1 surface (.../openai/v1) is preserved as
${base}/responses.
* fix(provider): honor OPENAI_AZURE_STYLE in the responses gate and use the Azure v1 responses surface
CodeRabbit review on #1961: the responses auto-route gate only checked
hostnames, ignoring the OPENAI_AZURE_STYLE override the shim honors for
custom/private Azure endpoints (APIM-fronted, private link). The Azure
detection is now a single shared predicate (isAzureStyleBaseUrl) used by
both the gate and the shim: OPENAI_AZURE_STYLE truthiness first, then
hostname matching.
Per Microsoft's docs, the Responses API exists only on the Azure v1
surface ({resource}/openai/v1/responses, model in the request body, no
api-version, no deployment-scoped form), so buildResponsesUrl now
normalizes any Azure-style base to that surface instead of mirroring the
chat builder's deployment-path + api-version form, which built endpoints
that do not exist.
https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses
* fix(provider): address maintainer review on GPT-5.6 responses routing
Narrows the responses auto-route to verified variants (gpt-5.4/5.5/5.6
minus -mini/-nano, two-digit minors deliberately unmatched), corrects the
gpt-5.6 context window to 1,050,000 per the OpenAI model pages, documents
OPENAI_AZURE_STYLE's routing effect in .env.example, and hardens
buildResponsesUrl normalization (trailing-slash strip, stacked Azure
suffixes stripped until stable). Also pins --max-concurrency=1 on the
default test script and adds direct coverage for isAzureStyleBaseUrl, the
override-driven responses URL, and the gpt-5.6 catalog metadata.
* test(provider): pin responses predicate behavior for patch and suffixed ids
Pins gpt-5.4.1 (patch of a verified family, routed), gpt-5.41 (two-digit
minor read, not routed), and gpt-5.6-mini-high (mini variant, not routed)
so the predicate's edge behavior is asserted rather than implied.
* fix(provider): responses-contract test, regional OpenAI hosts, Azure deployment docs, env allowlist
Updates the providerOverride gpt-5.4 effort test to the Responses contract
the auto-route now sends (nested reasoning.effort, /responses URL); widens
the auto-route host check to OpenAI-controlled *.api.openai.com regional
endpoints; documents and regression-tests the explicit
OPENAI_API_FORMAT=responses path for arbitrary Azure deployment names; and
allows OPENAI_AZURE_STYLE through the --provider-env-file allowlist.
* fix(provider): narrow responses auto-route to verified minors and carry GPT-5.6 reasoning metadata on Azure
Narrows the model-name auto-route predicate from gpt-5.[4-9] to gpt-5.[4-6]
so unverified future minors (5.7/5.8/5.9) are not auto-routed, and syncs the
comment plus the two remaining "5.4+" phrasings in .env.example.
Fixes GPT-5.6 reasoning metadata on Azure and regional OpenAI bases: those
hosts resolve to route 'custom' (empty catalog), so resolveCatalogReasoningMetadata
returned undefined and the request dropped its default 'high' effort and the
reasoning.encrypted_content include. It now falls back to the openai vendor
catalog by model name on route 'custom', so gpt-5.6 carries its advertised
default 'high' and xhigh instead of incidental legacy controls.
* fix(provider): gate the custom-route reasoning fallback to verified OpenAI/Azure bases
The round-3 custom-route fallback also fired for arbitrary OpenAI-compatible
gateways (which resolve to route 'custom' too), injecting a default
reasoning_effort:high on a chat_completions request those gateways may reject
— a behavior change on third-party gateways the PR promised not to make.
Gate the fallback on baseUrlSupportsResponsesAutoRoute (the same verified
OpenAI/Azure surfaces the Responses auto-route uses), threading the request
base via the reasoning context (process.env fallback for the upstream path).
* test(provider): isolate OPENAI_API_BASE/OPENAI_AZURE_STYLE in the gpt-5.6 reasoning tests
The Azure/regional/gateway reasoning tests snapshot-restored only
CLAUDE_CODE_USE_OPENAI/OPENAI_BASE_URL/OPENAI_API_KEY. A leaked
OPENAI_AZURE_STYLE from another test would make isAzureStyleBaseUrl treat
the gateway base as Azure-style, firing the fallback and flipping the
'no injected default' assertion. Snapshot both keys and delete them before
each test's setup so a leaked value cannot corrupt the result.
* fix: preserve GPT-5.6 fallback and Azure routing
* fix: cover GPT-5.6 Azure edge cases
* fix: cover GPT-5 forced chat tools
* fix(provider): narrow Azure-style responses routing
* fix(provider): isolate agent overrides from Azure mode
* fix(provider): isolate override reasoning from Azure mode
* fix(provider): isolate override API format
* fix(provider): preserve responses effort routing
* fix(provider): restore safe context and clear Azure mode
* fix(provider): preserve Azure routing state
* fix(provider): preserve Azure profile routing
* fix(provider): preserve automatic Responses routing in profiles
---------
Co-authored-by: jatmn <the@jat.mn>
* test: restore compact test module stubs
* test: restore remaining compact mock modules
* test: isolate auto compact from compact mocks
* test: seal mock.module leaks in awaySummary and diff smoke tests
awaySummary.test.ts stubbed ./api/claude.js and
./SessionMemory/sessionMemoryUtils.js, and diff.test.ts stubbed
src/services/analytics/index.js, each with an incomplete module that was
never restored. bun evaluates every test file's module-level imports up front
and shares one process across the whole suite, so these stubs leaked into all
downstream importers (claude.js: ~22, analytics/index.js: ~245), producing
order-dependent failures in the smoke run.
Register the stubs in beforeAll (not at module load) and unmock() in
afterAll, and acquire sharedMutationLock so the real modules are already
cached for every other file at startup. Verified: a downstream importer now
sees the real queryModelWithStreaming / logEventAsync, and the full suite
failure count drops 73 -> 70.
Co-Authored-By: Claude <noreply@anthropic.com>
* test: restore module mocks under shared lock
* test: guard mock restoration against lock races
* test: retain lock ownership through compact cleanup
* test: retain lock ownership through auto-compact cleanup
* test: restore mocks after setup failures
* test: isolate compact task output cleanup
* test: preserve full compact mock restoration
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(statusline): show token counts in context bar (ctx 74K/200K (37%))
Instead of just percentage, show actual token usage and context window
size with integer K/M prefixes (uppercase, en-US locale).
- BuiltinStatusLine: added contextInputTokens + contextWindow fields
- buildBuiltinStatusSegments: formats "ctx {used}/{window} ({pct}%)"
- format.ts: added formatTokenCount() — integer, uppercase K/M
- All 18 tests pass
* fix(statusline): call getCurrentUsage once and reuse result
* fix(statusline): mark estimated token counts with ~ prefix
When getCurrentUsage() returns is_estimated:true (provider reported
all-zero usage), show "ctx ~74K/200K (37%)" instead of "ctx 74K/200K
(37%)" so built-in statusline preserves the estimate distinction
exposed by the custom-statusline contract.
* fix(openai-shim): send stream_options for non-Ollama local providers
Previously stream_options was disabled for all local URLs (127.0.0.1,
192.168.x.x, etc.), preventing llama-server and other self-hosted
OpenAI-compatible servers from returning usage in SSE streams. Now only
Ollama (localhost:11434) is excluded, since it rejects stream_options.
All other providers including llama-server receive stream_options and
their prompt_tokens/completion_tokens are correctly mapped via
buildAnthropicUsageFromRawUsage.
---------
Co-authored-by: Andrey Bezborodov <andrey@getdataflow.ru>
* fix(query): bound per-turn latency growth in long REPL sessions (#1949)
Addresses the progressive latency regression where consecutive prompts in a
single session grow non-linearly (2nd prompt ~10s, 3rd 10+ min) due to
unbounded message accumulation with no proactive compaction and no per-prompt
turn cap on the main thread.
- Cap the interactive REPL main thread at 50 turns per prompt (DEFAULT_REPL_MAX_TURNS).
Headless/print mode and the SDK are unchanged (--max-turns flag / SDK callers
still control it), preserving the SDK API contract.
- Default maxMessagesCompactionThreshold to '200' so message-count compaction
runs well before the context window fills, instead of 'off'.
- Lower the auto-compact threshold buffer from 13k -> 30k so compaction fires
earlier with less accumulated history. The effective-context floor buffer is
kept at 13k and getAutoCompactThreshold() falls back to it for small-context
models, so the threshold can never go negative (no #635 regression).
Test updates: isolate the hard-cap override test from the new 200-message
default, and correct an outdated constant reference in the autoCompact test.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(query): repair REPL latency guard
* fix(query): cover resume and default guard paths
* fix(query): enforce cap across interactive paths
* docs(compaction): clarify disabled message limits
* fix(query): retain explicit message thresholds
* fix(query): enforce explicit threshold recovery
* fix(query): honor legacy active-message limit
* fix(doctor): report effective message compaction limit
* fix(config): share message threshold validation
* test(doctor): cover disabled message compaction
* fix(compact): preserve latency guard coverage
* test(repl): exercise turn cap defaults
* fix(compact): honor disabled default message guard
* fix(swarm): honor disabled auto compaction
---------
Co-authored-by: Claude <noreply@anthropic.com>
readLiteMetadata scanned the raw 64KB tail for "tag":"..." without scoping to
the {"type":"tag"} entry, so any nested occurrence matched too. A tool_use input
carrying a tag parameter (Docker image tags, git tags, cloud resource tags) is
stored as literal JSON inside an assistant entry appended after the tag entry,
and last-occurrence wins — so the tool's value was reported as the session tag.
In /resume that surfaces a phantom tag tab and misfiles the session away from
its real one; an untagged session could also acquire a tag it never had.
The other two tag readers already type-scope for exactly this reason
(listSessionsImpl.ts:132, sessionStorage.ts:782, both with comments naming the
tool_use collision), and sessionBranch in this same function is scoped via
SESSION_BRANCH_ENTRY_PREFIX. This reader just missed it; mirror them.
* docs: README cleanup, green wordmark header, Trendshift badges
Header: the startup wordmark (src/constants/brand.ts half-block art)
rendered as a green two-shade SVG (docs/assets/openclaude-wordmark.svg,
textLength-pinned so rows align in any monospace font), with the three
Trendshift badges (daily/monthly/repository) centered beneath it.
Cleanup (536 -> ~430 lines, nothing lost):
- Agent routing, maxSteps limits, and GitHub Copilot sub-agent tuning
moved to docs/agent-routing.md; headless gRPC server moved to
docs/grpc-server.md; README keeps linked summaries.
- Build/test/validation commands were repeated in three sections —
consolidated into one Development section; Contributing links to it.
- New "Meet Your Buddy" section documenting the companion heroes and
their /buddy commands; added to What Works and Why OpenClaude.
- Star History moved from the header flow down beside Community.
- Setup Guides indexes the new docs pages; fixed a missing blank line
before Repository Structure and a curly quote.
All relative links, image paths, and internal anchors validated.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* docs: pure-rect wordmark for crisp rendering; drop broken Star History
The wordmark SVG previously drew the half-block art as monospace <text>,
which rendered raggedly (font-dependent glyph stretching and seams).
Regenerated as pure SVG rects computed from the brand.ts wordmark grid —
no font dependence, pixel-crisp at any size, same two-shade green split.
Star History chart removed: the badge endpoint errors and displays a
broken image.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* docs: render the wordmark at full README column width
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* feat(buddy): hero pixel-art companions with signature Enter animations
Rebuild the buddy system as heroes-only. The 18 legacy rolled species are
removed; the hatch pool is now 7 hero forms — robinhood, kaio, strawhat,
merlin, kage, ember, corsair — each hand-pickable via /buddy set.
Every hero has 22x16 truecolor half-block pixel art (idle + action poses),
a line-art fallback for low-color terminals, a narrow-mode face, and a
signature effect that fires on every message submission: arrow with impact
thunk, charging full-width energy wave, stretchy punch that extends and
snaps back, twinkling sparkle stream, spinning shuriken, gradient fire
cone, and cannonball with smoke trail.
Engine: companion animation moves from a raw 500ms setInterval to the
shared animation clock (useAnimationFrame; pauses when hidden, respects
prefersReducedMotion), with a one-shot 50ms burst driver (useShotClock,
arm-then-anchor to avoid stale-tick draw-phase skips) and a general
ActionEffect system (pure draw/travel/impact functions, frame-tested).
Effects travel right-to-left toward the prompt — matching where the
sprite actually stands.
Commands: /buddy set <form|random>, /buddy name <name>, muted-buddy
feedback (silent no-op pets now explain themselves), and a hatch-message
fix so the announced species always matches the displayed sprite (the
message previously rolled with a different seed).
BREAKING: existing rolled pets transform into a hero on upgrade (name and
personality persist; speciesOverride pins are unaffected).
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(buddy): address CodeRabbit review on PR #1972
- useShotClock: consume an in-flight shot when playback becomes
ineligible mid-flight (mute/reduced-motion/resize), so re-enabling
can't resume a stale animation.
- /buddy unmute: emit a greeting reaction — the sprite reads
companionMuted non-reactively and its clock is paused while hidden,
so a config-only unmute left it invisible until an unrelated
re-render.
- /buddy name: strip ANSI escapes and control/format characters before
saving, and cap by display width (stringWidth) instead of UTF-16
length.
- CompanionSprite: track bubble age in sync-render state instead of an
effect-updated ref, so a fresh reaction can't render pre-faded.
- companion_intro already keyed on name+species (prior commit); tests
now pin exact faces for all seven heroes, separate idle/shoot pixel
frame counts, and decode-guard the charCode species constants.
- CompanionActionFX tests: deterministic companion fixture via
complete-config module mock; raw (untrimmed) output compared against
a rendered-null baseline so a spurious blank FX row fails.
- companion.test: re-register the real config module in afterAll
(mock.restore does not undo mock.module).
- Types: SPECIES_COLORS and FORM_FLAVOR are full Records (compile
error on a colorless/flavorless future hero); dead RARITY_COLORS
removed.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* test(buddy): regression coverage for bubble age reset on reaction change
Renders CompanionSprite against a fake shared clock: ages the first
bubble past the fade threshold, swaps the reaction WITHOUT advancing
the clock, and asserts the fresh bubble renders unfaded. Fading is
detected structurally (border and text collapse to one color when
fading) so the test is independent of the active theme's exact values.
Requested by CodeRabbit on PR #1972.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* fix(memdir): enforce entrypoint cap in bytes, not UTF-16 char length
truncateEntrypointContent bounds MEMORY.md/CLAUDE.md content with
MAX_ENTRYPOINT_BYTES (the field is byteCount, the flag wasByteTruncated,
and the warning renders it via formatFileSize) but measured and cut with
String.length, which counts UTF-16 code units. For multibyte content
(CJK/emoji at 3-4 bytes each) that undercounts by up to ~4x, so a file
that is tens of KB of real bytes but under 25,000 chars slips past the cap
entirely — reported as a fraction of its true size and passed through
uncapped, defeating the budget whose purpose is to bound context bloat.
Measure with Buffer.byteLength and perform the newline-boundary cut in byte
space (Buffer.lastIndexOf(0x0a) + subarray) so the cap actually bounds
bytes. Add regression coverage for a multibyte file over the byte cap but
under the char cap, and for small multibyte content left untouched.
* fix(memdir): keep the hard byte cut on a UTF-8 boundary
The no-newline fallback in truncateEntrypointContent hard-cut the UTF-8
buffer at MAX_ENTRYPOINT_BYTES. If that offset landed inside a multibyte
character, decoding with toString('utf8') emitted a U+FFFD replacement char,
so the body was no longer bounded by the cap — e.g. a single 30KB line of
CJK produced a 25,002-byte pre-warning body against a 25,000 cap.
Back up to the character's first byte (skip continuation bytes 0b10xxxxxx)
before slicing so the decoded body stays within the cap and contains no
replacement chars. Add a regression for the no-newline CJK case.
* docs(memdir): tell memory writers about the entrypoint byte cap
The save instructions only warned that MEMORY.md is truncated after 200
lines. Now that the 25KB byte cap is effective for multibyte content, a user
can stay well under 200 short CJK entries and still lose the tail of the
index without the writer knowing the byte cap exists. Update both the
private-memory guidance (buildMemoryLines) and the team-memory guidance
(teamMemPrompts) to state both limits — 200 lines or ~24KB, whichever comes
first.
* docs(memdir): state the byte cap in the extraction writer prompts
The background auto-only and combined memory extractors load MEMORY.md through
truncateEntrypointContent, which this branch makes enforce the 25KB byte cap for
multibyte content. Their save instructions still told the model only that lines
after 200 are truncated, so an extractor could satisfy the prompt with fewer
than 200 long entries and still lose the index tail on the next load. State both
the line and byte limits, matching the interactive private/team guidance.
* test(memdir): cover the combined line + byte truncation path
Add a case where content exceeds both MAX_ENTRYPOINT_LINES and
MAX_ENTRYPOINT_BYTES, so line truncation runs first and the byte cut then
applies to the already line-clipped result. Asserts the combined reason string,
the byte-bounded body, and no split-character replacement chars.
evictLRU picks the entry with the smallest recorded access order, but that
order was set to accessOrder.size — which plateaus at ~maxSize once the cache is
full and drops on delete, so it is not monotonic. After the cache saturates,
every set/get stamps roughly the same value, and a freshly-inserted key can end
up ranked below entries a prior get already bumped. The next insert then evicts
the most-recently-touched key instead of the least-recently-used one, churning
the 50-entry session-history cache (sessionHistory.ts) into needless refetches.
Stamp recency from a monotonic counter that only ever increases.
Cursor.modifyText builds newText from prefix + insert + suffix and hands it to
Cursor.fromText, which NFC-normalizes the whole string. But the new cursor
offset was computed as startOffset + insertString.normalize('NFC').length,
normalizing the insert in isolation. When the inserted text begins with a
combining mark that composes with the last character of the prefix (e.g. "e" +
U+0301 -> "é"), the normalized newText is one UTF-16 unit shorter than that
formula assumes, so the returned offset overshoots by one and the cursor lands
past the following text — the next keystroke then edits the wrong spot.
Measure the normalized prefix-plus-insert instead, so cross-boundary
composition is accounted for. Reduces to the previous behavior whenever no
boundary composition occurs (plain ASCII, astral emoji, insert at start).
pruneByRelevance keyed recency scoring, the group tie-break, and the final
"restore chronological order" sort off message.message?.created_at. That
nested API-body field is never populated on our Message objects (nothing in
the tree assigns it), so every read was undefined and `?? 0` made all three
into no-ops: the recency bonus never fired, the tie-break never broke ties,
and the final sort left the list as [...recentMessages, ...olderGroups] —
the newest preserveRecent messages jumped ahead of older retained ones.
The chronological key actually lives on the Message envelope as `timestamp`
(an ISO-8601 string present on every variant). Add a messageTimeMs() helper
that parses it and route all three sites through it. This runs in the
auto-compaction path (autoCompact -> pruneByRelevance), so the reordered
list was being sent to the model. Add a regression using the real envelope
shape asserting retained messages stay in chronological order.
expandEnvVarsInString split the ${VAR:-default} syntax with
varContent.split(':-', 2). The code comment says the limit is there to
"preserve :- in defaults", but JavaScript's String.split(sep, limit) caps
the array length and discards the remainder — it is not a maxsplit that
glues the tail back on. So a default that itself contains ':-' is
truncated at the first occurrence: ${VAR:-a:-b} expands to "a" instead of
bash's "a:-b".
Slice at the first ':-' with indexOf so any later ':-' stays in the
default. This runs over user .mcp.json command/args/env/url/headers values.
Add coverage for the ':-'-in-default case plus set/unset/empty-default and
missing-var paths.
parseGitDiff skips per-file diffs larger than MAX_DIFF_SIZE_BYTES (1 MB) —
the constant is named _BYTES, its comment says "1 MB", and the function's
doc promises "Files >1MB: skipped entirely". But the guard measured
fileDiff.length, i.e. UTF-16 code units. Multibyte content (CJK/emoji at
3-4 bytes each) undercounts real size by up to ~4x, so a single-file diff
of several MB in bytes but under 1M code units slips past the cap and is
parsed and inserted into the result map — defeating the bound that keeps
huge single-file diffs from bloating memory/context in the diff viewer.
Measure with Buffer.byteLength. Add a regression: a diff under the cap in
code units but over it in bytes is now skipped.
getSnippetForTwoFileDiff builds each hunk's snippet from the new-file lines
(deletions are filtered out) but seeded addLineNumbers with the hunk's
oldStart. For the first hunk oldStart === newStart so labels are correct,
but any hunk following one that inserted or removed lines is mislabeled by
the net line delta of the earlier hunks — the shown numbers no longer match
the file the model is looking at.
Seed from newStart so the new-file content carries new-file line numbers.
Add a regression with a two-hunk diff whose second hunk shifts by +3.
* fix(model): default NVIDIA NIM main loop model
* fix(cost): avoid default pricing for unknown models
* fix(model): use NIM descriptor default
* test(model): pin NIM descriptor fallback
The name-based shim matcher inferred the full Z.AI GLM contract —
including enableToolStreaming — for any glm-<n> model without a catalog
entry. tool_stream is a Z.AI-proprietary streaming extension, so serving
GLM through an arbitrary OpenAI-compatible gateway (e.g. NVIDIA NIM,
integrate.api.nvidia.com) made every request fail immediately with
400 Unsupported parameter(s): tool_stream.
Only a catalog entry may opt into tool_stream (Z.AI-contract gateways set
it explicitly via transportOverrides.openaiShim). Inferred GLM routes keep
the reasoning-shaping fields, which any GLM endpoint benefits from, but no
longer send tool_stream; without it tool calls are simply not streamed.
* fix(powershell): make CMDLET_PATH_CONFIG prototype-safe
CMDLET_PATH_CONFIG is a plain object literal keyed by the lowercased cmdlet
name, unlike its sibling maps CMDLET_ALLOWLIST (readOnlyValidation.ts) and
COMMON_ALIASES (parser.ts) which are Object.create(null) specifically to defend
against prototype-chain pollution. extractPathsFromCommand does
`CMDLET_PATH_CONFIG[resolveToCanonical(cmd.name)]` guarded only by
`if (!config)`. A command named 'constructor' or '__proto__' (both survive the
lowercasing) makes the lookup return an inherited Object.prototype member —
truthy — so the guard is bypassed and `[...config.knownSwitches]` throws
(spread of undefined), crashing path-constraint validation instead of treating
the name as an unknown non-path cmdlet. Give the map a null prototype like its
siblings so inherited-key lookups return undefined.
* test(powershell): assert set-content control resolves to ask decision
The control case previously only asserted checkPathConstraints did not
throw, so it would still pass if CMDLET_PATH_CONFIG stopped resolving its
own entries and set-content fell through as an unknown cmdlet. Assert the
returned behavior is 'ask' so the test proves the null-prototype map still
holds and classifies its own keys. Also reword the collision comment to
present constructor/__proto__ as representative reachable names rather than
the only ones.
getSnippetForTwoFileDiff caps the two-file diff snippet at
DIFF_SNIPPET_MAX_BYTES and appends a "[N lines truncated]" notice. When
the cut snaps back to a line boundary (the normal case), kept ends right
before the boundary newline, so full[cutoff] is that '\n'. Counting
newlines from kept.length already includes the boundary newline plus every
later one — which is exactly the number of dropped lines — so the
unconditional +1 double-counts it and the notice reports one line too many
(two if the snippet ends with a trailing newline).
Split the boundary and mid-line cases: at a boundary the count needs no
+1; only the mid-line cut (no newline within the cap) keeps a partial tail
line that must be added. Add a regression asserting kept + reported ==
total lines.