Review follow-ups to #945: a listener caveman did not start this run and
cannot prove it owns (foreign process, or a proxy binary too old to
answer) now launches direct, and the direct-launch strip removes the
agent-attributed /w/<agent> route as well as the bare gateway URL, so a
leaked outer route never reaches the child. Qwen's ACP and extension
listing guards accept yargs camelCase spellings. Proxy-aware fetch no
longer sets an IP literal as TLS servername (rejected on Node 26). The
conformance-pin test fixture follows the pinned-upstream-binary job.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A bare proxy serves whichever client connects, so a machine-level MCP
install marker can never bind to one request. Read the namespaced
caveman retrieve tool out of the request's own tool list instead, so
subscription and OAuth sessions on a standalone proxy reach the live
zone when their agent carries the tool (#908).
Cache-breakpoint planning and the provider-native cache optimizers now
default on in optimization modes; explicit off and unknown values still
fail closed, and record mode never runs them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pi extension routed models by wire protocol only. OpenCode Go
models use the OpenAI protocol, so the gateway sent them to
api.openai.com. The OpenCode Go key then caused a 401 error.
The extension now routes by provider and API. OpenCode Go models go
to the /w/pi/compat/opencode-go mount. The extension does not route
unknown providers and shows a pass-through warning.
The standalone proxy registers a built-in compat mount with the
upstream https://opencode.ai/zen/go. The config package holds the
built-in table, so the mount and its key policy come from one place.
A user compat entry with the same name wins. Keyless requests to this
mount use OPENCODE_API_KEY, not OPENAI_COMPAT_API_KEY.
Fixes#946
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Zsolt Gaspar <17578237+zsltg@users.noreply.github.com>
* fix(proxy): skip vendor trees in repo intel
BM25 was indexing .pixi/envs and other dependency
dirs, then injecting those file:line guesses every
turn. Exclude known vendor trees and drop the path
block when no local scout is configured.
Closes#899
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
* fix(proxy): detect dep trees by content, not name
Name denylist missed conda envs, undotted pixi, and
virtualenvs under any name, while dropping real
first-party build/ and vendor/ dirs.
- listFiles prefers `git ls-files --exclude-standard`,
so the project's own .gitignore defines vendored
- walk fallback identifies environments by contents
(conda-meta, pyvenv.cfg, site-packages child),
catching the interpreter stdlib they carry
- ambiguous build/dist/target/vendor need a sibling
manifest before exclusion
- symlink and junction dirs are never followed
Refs #899
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
* fix(proxy): treat the indexed repo as untrusted input
Second review found the git listing both unsafe and
incomplete.
- gitCommand pins core.fsmonitor and other exec
vectors off and strips GIT_*; fsmonitor runs during
an index read, so `ls-files` in a hostile clone ran
attacker code at session start
- git-listed paths now run the same content filter as
the walk, so an un-ignored conda prefix or committed
vendor tree no longer rides in on git's say-so
- an empty-but-successful listing means the cwd is
ignored by an enclosing repo; fall back to the walk
instead of reporting a complete map
- Direct matches the file name, not the whole path:
a term like "src" marked every file beneath it
- ListingBasis discloses which path ran, hashed in
Refs #899
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
* fix(proxy): log declined evidence, document invariants
A declined turn stays silent to the model but now
records repository_evidence_declined_no_direct_match,
so weak evidence is distinguishable from a broken or
disabled mechanism.
Adds a positive control to the suppression test and
records the gating, filtering, and git-hardening
invariants in proxy/CLAUDE.md.
Refs #899
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
* fix(installer): make prune path flavour explicit, not host-derived
pruneOrphanedManagedHooks guards against pruning a hook whose path is
absolute on the other platform. That guard is POSIX-only by construction:
win32's absolute set is a superset of posix's, so a Windows host reads
C:\... natively and prunes it. The test asserting the guard hard-coded a
Windows path and so failed on the windows CI runner.
Select the path flavour from an explicit platform arg (default
process.platform, same as rewriteLegacyManagedHookCommands) so the POSIX
side stays reachable from a Windows runner. Host behaviour unchanged.
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
* test(cli): escape stub preload path in NODE_OPTIONS
Node parses NODE_OPTIONS itself and treats a backslash inside a quoted
value as an escape, so the raw Windows path reached --require with every
separator eaten:
Cannot find module 'C:UsersRUNNER~1AppDataLocalTempcave-pi-enableXbinstub-dispatch.cjs'
Every stub-backed process died in the preload, failing all six pi enable
and wrap round trips. The quotes must stay for profile names with spaces,
so double the separators instead. POSIX is unaffected — stubEnv returns
before this on non-Windows.
Never caught before because npm test failed earlier in the same job, so
test:windows has not run since these tests joined the gate.
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
* fix(proxy): harden every untrusted git call, not just repo intelligence
`repointel` hardened its own `git ls-files` and left three other call sites
raw. `nativehook.currentRepositoryState` runs `git status` on EVERY hook
event, and `status` refreshes the index, so it triggers `core.fsmonitor`
harder than `ls-files` does — verified: the sentinel hook fires. Same for
`store/detect_outcomes` and the two `execFileSync` sites in the CLI.
- `proxy/internal/gitsafe` and `packages/cli/src/git-safe.ts` now own the
`-c` override list; all five call sites route through them.
- `verify_repo.py` fails the build on a NEW raw `git -C <dir>` call site,
which is the regression class this bug actually belongs to.
- `Bundle.DirectOnly()` strips metadata-only items BEFORE the evidence
object is written. Filtering only at render time left the BM25-proximity
guesses stored behind the `ccr://` handle, one retrieval from the model,
which is what the block refuses to render.
- `gitListBudget` caps git at half the caller's remaining warm budget. 2s
of a 3s window left the fallback walk — the MORE expensive path — unable
to finish, so the map failed closed in exactly the case the fallback
exists for.
- `NormalizeTerms` floors terms at 3 characters, matching the hook that
produces them: direct evidence is a substring test on a file's own name,
so `go`/`py`/`js` marked every file of that language a direct hit.
Regression tests verified to fail without their fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(build): stop wedge generation racing itself on Windows
mcp, shrink, and browse each run gen-wedge-installer from their own
prepack/pretest, and every run writes all four targets, not just its own.
`pnpm -r test` runs those packages in parallel, so several processes
copy onto the same file at once and Windows fails the loser:
EBUSY: resource busy or locked, copyfile
packages/shared/binary-installer/installer.mjs
-> mcp/bin/binary-installer.generated.mjs
The bytes are identical every time and the outputs are committed, so the
race was over writes that change nothing. Skip when the destination
already matches, and land a real change through a temp file + rename so
no concurrent reader sees a partial file. Verified with a 12-way
concurrent storm, and against corrupted outputs to prove the write path
still restores them.
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
* fix(proxy): keep system git config readable while hardening
GIT_CONFIG_NOSYSTEM defends against nothing here — /etc/gitconfig is
root-owned, the threat is the repository's own .git/config, and the `-c`
overrides beat every config level anyway. What it does cost is org-wide
`safe.directory` allowances: without them `git status` fails "dubious
ownership" on shared and CI machines, repository state comes back empty,
and reuse silently stops. Dropped from both wrappers, with a test so it
does not come back.
Also names the fsmonitor tradeoff at the hot call site: disabling it taxes
the people who configured it because their repo is huge, and status has a
100ms budget. That degrades safely; moving the budget is a latency
decision for every user and needs a measurement this fix does not have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(build): pin wedge generation against the concurrency it now survives
The EBUSY fix has no committed regression proof, and the failure only
appears when several packages run the script at once — which is exactly
what `pnpm -r test` does and what a single-process check never reproduces.
Eight concurrent runs must all succeed and leave the shared outputs
byte-identical; a drifted output must still be repaired, so skip-if-
identical cannot regress into skip-always.
Runs in root `npm test`, which is the gate the Windows job failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
Co-authored-by: Julius Brussee <juliusbrussee@getlockedin.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Write down the conditions under which `caveman learn` may state a currency, so
the distinction survives the next person to touch this code: spend is arithmetic
over provider-counted tokens at dated published rates, savings additionally
requires a named attribution rung, totals never blend rungs, and neither surface
ever writes `verified`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wrap CLI spawns the proxy detached with stdio ignored, and the standalone
serve path passed no logger to the gateway at all. Every warning the proxy
emitted — upstream failures, copy errors — was lost, so field reports like #897
arrived with no proxy-side evidence.
Wire the standalone logger through and tee it to ~/.caveman/proxy.log, appending
at 0600 with a single previous generation rotated at 16MB. Redaction runs on the
file handler as it does on stdout. Any failure opening the file returns nil and
serving continues unlogged: logging must never block serving.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A non-streaming response is one JSON document, but the gateway was forwarding it
chunk by chunk. When the upstream failed mid-body — HTTP/2 stream reset, GOAWAY,
connection loss under concurrent subagent load — the client had already received
a 200 header, so the failure surfaced as a truncated gzip payload and the
ZlibError reported in #897 instead of an error the client could retry.
Read the whole body before writing any header. On a read failure, retry the
upstream once; if that also fails, return a clean 502 the client can act on. The
64MB buffer cap only bounds memory — a provider JSON document never approaches
it — and streaming responses are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Render the window cost, the effective input rate after cache reuse, and the
per-sink spend figures in the static HTML report, and carry each confirmed row's
attribution method next to its number so the report never shows a saving without
saying how it was measured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A handoff digest of the local findings, built from an allowlist rather than by
redacting a full plan: identities, magnitudes and rungs only. No locators, no
evidence maps, no paths, and identity-bearing sink ids are collapsed to their
family before they leave the machine.
Allowlist-in, not denylist-out, so a field added to the plan later cannot leak
by default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compare what learn measured against what the provider actually billed, from a
usage CSV the user exports themselves. The finding is the unattributed share —
the tokens we could not see — rather than a match percentage, because the gap is
what tells the user how much of their bill this report is entitled to explain.
Header mapping is fail-closed and by name; a column is never read by position.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Some fixes cannot be proven by re-counting a file: a distilled skill costs
prefix tokens every session and pays back only on the sessions that hit its
pattern. Add an on/off holdout over the user's own sessions to measure those.
Each arm needs at least five sessions before any verdict exists; below that the
result is insufficient_data, which is never presented as a small win. The
harness compares median tokens per session and also tracks tool errors per turn,
so an arm that is cheaper but fails more is flagged rather than sold as a
saving. It reuses the outcome scan state so a report costs one corpus walk.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A number in the outcome ledger used to arrive with no account of how it was
obtained, which made a re-counted file and a before/after session median look
like the same quality of evidence. Give every confirmed row an attribution
record instead.
Each row now names its rung — deterministic_remeasure, controlled_holdout,
counterfactual_replay, interrupted_time_series — carries the standing
confounders that rung cannot rule out, and reports whether the artifact caveman
fingerprinted at apply time is still intact on disk (intact / changed_since /
target_missing).
Totals are bucketed by rung and never summed across rungs. Blending a re-counted
file with a before/after median launders the weak number into the strong one. A
regression carries no dollar figure at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five new detectors plus a widened cache-hygiene pass, all ranked and priced
through the same path as the existing sinks rather than appended afterwards:
- cache_efficiency (detect_cache_hygiene.go): what a million input tokens
actually cost after this user's cache reuse, and the multiplier against list.
It is the rate the other sinks are priced at, so it carries no token volume
and must never be summed with them. Per-turn hooks are listed as candidates,
caveman's own included.
- tool_output_portfolio: the call shapes that dominate context, ranked by
normalized signature. No paths or arguments survive normalization.
- session_outcomes: token share in sessions with no commit in their window.
Fails closed — a repo git cannot answer is dropped, never counted as
commitless. Correlational, and framed that way.
- subagent_spend: sidechain token share and per-type spawn counts. Visibility
only; it carries no practice id and never becomes "spawn fewer subagents".
- procedure_repeat:*: distillation candidates mined from tool-signature
n-grams, routed away from the net-token-negative gate because a skill's cost
and its benefit land in different places.
- config_growth (detect_config_trend.go): growth against this machine's own
snapshot history, not against a guideline. Reports the delta only so it never
double-counts the config weight sink.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`caveman learn` could rank token sinks but never say what they cost. Add a
spend block that prices the window from the usage numbers the provider itself
counted, at rates from the dated shared/provider-catalog row, stamped with that
row's catalog version.
The honesty constraints are structural, not stylistic:
- Buckets are disjoint, so nothing is double-counted.
- A model with no catalog row is never priced at a sibling's rate. Its tokens
are disclosed separately and the total is stated as a floor.
- Sinks are priced at the user's own measured effective input rate rather than
list price, so a well-cached user's findings are not inflated roughly 10x.
- The figure is window-bounded and never projected into a month or a run rate,
and it is never written as `verified` — that basis stays gateway-only.
Spend is what the window cost. It is not a savings claim; that needs an
attribution method, which lands separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The learn v3 detectors need three things the session scanners did not yet
surface: per-model provider-counted usage split into disjoint billing buckets,
per-turn hook and subagent metadata, and enough repo/commit context to join a
session to its outcome. Extend the Claude, Codex, Gemini and opencode adapters
plus the shared scan state to carry them.
Anthropic and opencode report disjoint buckets; OpenAI and Gemini report
inclusive totals and are normalized on read, so a downstream consumer never has
to know which convention an adapter used.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cacheEpochAllows re-anchors the stored prefix on every observe, so calling it
twice per request anchored the baseline to bytes the client never sent and
flipped compression on and off every other turn. Evaluate once against the
client's own body and reuse the answer.
Also: startRepositoryEvidence checks its bail-out before inserting the map
entry, so a second CWD no longer leaves an entry whose done channel never
closes; and ChatGPT records use the store's space-separated ts layout so
--since bounds and ORDER BY ts stop mis-sorting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmbqWDtNexzDfH5V3BMRz
TestKeepaliveHoldsOffIdleExit heartbeated every 10ms against a 30ms idle
threshold. Windows' default timer granularity is ~15.6ms, so the ticker really
fires every ~16ms and a loaded runner stretches it further — barely a 2x margin,
and the idle timer won that race intermittently on windows-latest ("idle exit
fired while keepalive heartbeats were arriving").
5ms against 250ms is ~50x, which absorbs a scheduling stall an order of
magnitude worse than anything observed. Ran 20x locally, green.
The assertion is unchanged: heartbeats hold idle off, and idle still fires once
they stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
Pi joins every host surface: NativeAgent unions, capability matrix (incl.
post_tool_rewrite; PostToolUseFailure now returns raw responses for pi and
opencode), native-hook output contracts, Go validAgent +
directOutputRewriteAgent, doctor/status/enable/disable/expectedRoute rows,
and the porcelain agent list (visible cap 7 → 8: '+1 more' rendered longer
than the id it hid).
Wrap door: buildPiWrapArgs prepends the profile-declared loader flag plus the
bundled extension (CAVEMAN_PI_EXTENSION → dist/caveman-pi-extension.mjs),
stamps CAVEMAN_PI_HOOK_CMD + CAVE_GATEWAY_URL, and skips the generic base-URL
env spray — pi routing is extension-side, behind its recovery gate. Missing
asset fails open to a direct launch with a warning; the bundler script fails
the build instead of silently shipping without the asset.
Enable door: journaled write of banner + baked hook invocation + bundle to
~/.pi/agent/extensions/caveman-native.js (Pi auto-discovers only .ts/.js;
.mjs would journal fine and never load). settings.json stays byte-identical.
Status/doctor: pi route health = on-disk artifact ends with the currently
shipped bundle (catches stale bundles after CLI upgrades; the gateway is
resolved at runtime by the extension through the same chain as gatewayURL(),
so enable-time route recording would only manufacture false drift).
hooks install/uninstall pi exit 0 and point at enable/disable — the rewrite
surface ships inside the native extension.
Conformance: pi joins the matrix — a real wrap through the real proxy build
compresses via /w/pi/openai/v1 with a CCR handle booked under agent_slug=pi,
and the stub refuses to run if wrap failed open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
Un-masked by the lockfile fix — Windows go tests had never run on main CI:
- readLikePath used OS-dependent filepath semantics on paths read out of
session logs, but those paths carry the convention of the machine that
wrote the log: on Windows, filepath.IsAbs("/repo") is false and Clean
flips separators, so a log's "/repo/a.go" and repo-joined "a.go" never
normalize to the same floor key. Slash-normalized path-package semantics
with a drive-letter check behave identically on every scanner platform.
- The MCP surface test concatenated a raw temp path into .claude.json; on
Windows the backslashes are invalid JSON escapes, the parse failed, and
the figma project server vanished. The project key is json.Marshal'd now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
The wrap-owned proxy treated 30 minutes of session silence as session end and
self-terminated, while the wrapped agent's ANTHROPIC_BASE_URL kept pointing at
the dead port — every later request in an open-but-quiet session failed.
The wrap CLI process lives exactly as long as the agent it spawned, so it is
the honest liveness signal: it now POSTs a loopback /caveman/keepalive beat
every 5 minutes while the agent runs, and Runtime.Keepalive refreshes the idle
clock (activity only — no session state, nothing recorded). This also covers a
proxy reused across wraps: any live wrap holds it up, and a truly orphaned
proxy is still reaped 30 minutes after the last heartbeat. Beats are unref'd,
skipped for direct/no-proxy/managed runs, and fail open like the hooks.
Fixes#860
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
Any httpClient.Do error collapsed into a flat 502 cave_upstream_unavailable,
which agent hosts treat as terminal — killing the session. Field data (issue
report): 44/12,630 requests failed this way, all with ttfb=0 and
response_bytes=0, zero below 127KB, ~0.9% above 400KB — connection resets
during the request upload, never a partially-consumed response.
Since a Do error means zero response bytes were received and nothing has been
written to the client, the buffered request is safe to replay. doUpstream
rebuilds the request per attempt and retries up to 3 times with short backoff,
never after the client context ends. Wired through the /v1/messages send, its
byte-safe original-bytes retry, and both buffered ChatGPT-route sends; the
ChatGPT streaming path has a partially-consumed body and keeps single-shot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
Upgrade `caveman learn` from a Claude/Codex setup linter into a broader local optimizer. It now scans Gemini, opencode, and opt-in aider history, uses catalog-backed context windows and measured turn-1 prefix evidence, adds new behavioral detectors, and ranks grouped portfolio moves without mixing historical totals into forward rates.
The CLI/proxy/TUI now support `learn applied`, `learn simulate`, `--repo`, per-repo reporting, and confirmed longitudinal fix verdicts (`improved`, `unchanged`, `regressed`, `insufficient_data`). Docs, the embedded `caveman-learn` skill, and tests were updated to cover the new learn v2 behavior and planning artifacts.
Track each session's peak context-window usage during learn scans and include it in the plan output. Refresh the learn HTML report with a TLDR summary, context-depth histogram, and 30-day list-price cost chart so deep sessions and major token sinks are easier to spot.
Improve Windows support across installer, CLI hooks, binary lookup, hidden child processes, JetBrains detection, and Chrome discovery. Add real Windows ACL checks for SQLite parent directories instead of skipping the security guard.
Also expand BSL-1.1 coverage/documentation for new engine-linked runtime modules (`cacheengine`, `rewriter`, `browse`) and add repo/CI checks so license boundaries and cross-platform test paths stay enforced.
Removes ADR citations (decision records live in the private repo;
links were broken here) while keeping every described invariant
inline, and fixes generator headers / doc titles that leaked the
private monorepo layout (public/cli, cloud/web). Comments and docs
only — no behavior change.
Claude-Session: https://claude.ai/code/session_01DLNTYJU8vMEdDWpV7e7k7Z
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Single-commit port of the Caveman 2 tree onto the original repo history
(candidate caveman-2@88878a4 applied over main@3098342). No unrelated-history
merge; original history untouched.
Preserved v1 surfaces:
- skills/caveman/SKILL.md byte-for-byte, plus plugin mirror and caveman.skill zip
- legacy CI (Node 18/20/22 installer + Python 3.11 suites) alongside engine-ci
- cli/install.js compatibility shim forwarding to bin/install.js
- .codex/config.toml ships both hooks and codex_hooks for older Codex (#617)
- README Trendshift + Skills.sh badges and local star-history asset
- v1.10.0-pinned curl installs; benchmarks/tmp-starcharts ignore rules
Fixed for public same-repo release:
- SECURITY.md vulnerability reporting points at this repository
- release-binaries.yml publishes to this repo via job-scoped GITHUB_TOKEN
(cross-repo PAT removed); binary-release environment still gates signing
- packages/cli/PUBLISHING.md rewritten for packages/ layout and same-repo flow
- profile-contract and provider-catalog lanes skip lane-scope gates on
initial import so the launch PR can gate on their real validation steps
- ANNOUNCEMENT.md network claim corrected: compression is local; requests
still go to the provider you configured
Caveman 2 stays opt-in: bin-v1.0.0 binaries and @caveman-ai/cli publish
separately after this lands and soaks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLvb5QniwYjmcawyssQNw9