Compare commits

..
234 Commits
Author SHA1 Message Date
Andrew QuandGitHub 3098342331 Update README.md with Skills.sh badge (#338) 2026-08-10 19:59:13 +02:00
Julius Brussee 11ddc0c981 docs: simplify install section
Make the README install flow easier to scan by surfacing one-agent install paths directly and pointing users to `INSTALL.md` for the full matrix. Also ignore the local `tmp-starcharts/` scratch directory so generated chart work stays out of git.
2026-08-08 13:04:58 +02:00
Julius BrusseeandClaude Opus 5 14d4f2e21a docs: add Trendshift badge to README header
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012SSMrNKRooMGmJ1yNFpLMy
2026-08-08 00:48:02 +02:00
Julius BrusseeandGitHub ec83e5bace Update README for clarity on coding runs 2026-08-04 15:44:52 +02:00
Julius Brussee 7066cc8154 chore: use local star history image in README 2026-08-03 15:34:49 +02:00
github-actions[bot] fcf7663366 chore: sync SKILL.md copies [skip ci] 2026-08-03 10:57:48 +00:00
Julius BrusseeandClaude Opus 5 e07431f127 release: pin install ref to v1.10.0
The sweep adds src/hooks/caveman-parse.js and regenerates
checksums.sha256 to include it. The detached install path fetches
hooks from RAW_BASE = raw.githubusercontent.com/<repo>/<PINNED_REF>,
and the integrity gate refuses any hook that doesn't match the pinned
release. Left at v1.9.1 that ref has no caveman-parse.js, so
curl | bash 404s and fails integrity for new users.

Bump must land inside the tagged commit so v1.10.0 contains both the
new hooks and a PINNED_REF pointing at itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012SSMrNKRooMGmJ1yNFpLMy
2026-08-03 12:57:36 +02:00
Julius Brussee 52bbbcdd86 docs: clarify token savings by workload
Update README to separate the 65% chat-style prose benchmark from full agentic coding runs, adding JetBrains’ independent 86-task result (8.5% output-token savings). Adds context on why both numbers are valid, explains workload-dependent ceilings, and aligns Caveman 2 positioning around measuring real savings on each team’s own traffic.
2026-08-03 12:51:36 +02:00
Julius BrusseeandClaude Fable 5 6566ec23d3 ci: run the full test suite on every push and PR
Tests existed but nothing ran them in CI. Node 18/20/22 matrix runs the
installer suite plus every standalone tests/test_*.js runner; a python job
runs unittest discover (compress tests mock the Claude call, no network).
Node is set up in the python job too — several python tests shell out to
node for hook checks. Inspired by PR #695, minus the scanner extras.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
2026-07-21 02:01:27 +02:00
Julius BrusseeandClaude Fable 5 ed1fbb7f4c fix(install): rename bin/ to cli/, harden Windows quoting, clean uninstall
Marketplace fix (#712, #705): Claude Desktop rejects plugins containing a
top-level bin/ directory, and .claude-plugin/marketplace.json packages the
repo root, so the installer directory is now cli/. Every reference updated
(package.json bin entry + files, shims, docs, tests, caveman-init require
path). Supersedes PR #726.

Security (PR #717 verified): quoteWinArg only quoted on whitespace/quotes,
leaving cmd.exe metacharacters (& | ^ < > % parens) unescaped on the
shell:true Windows spawn path. Attacker-influenced arguments (--with-init
cwd, --with-mcp-shrink value) could chain commands. Trigger regex now
covers the metacharacter set; quoting logic split into a platform-
independent, unit-tested helper.

Also:
- uninstall removes .caveman-active.prev, .caveman-mode-log.jsonl,
  .caveman-statusline-suffix, .caveman-nudge-shown; keeps
  .caveman-history.jsonl with a printed note; dry-run now says
  'would remove' instead of lying (#635, supersedes PRs #693 #636)
- Array.isArray guard in rewriteLegacyManagedHookCommands — malformed
  hook event no longer crashes the installer mid-run (supersedes PR #646)
- gemini extensions install --consent: the security prompt hung every
  piped/non-interactive install forever (#676, part of PR #664)
- OpenClaw skill stamps the real PINNED_REF version instead of hardcoded
  1.0.0; new --no-always flag for load-on-demand installs (supersedes
  PR #720)
- shims scope NPM_CONFIG_ALLOW_GIT=all to the npx call — npm >=12
  defaults allow-git to none and EALLOWGITs github: installs (#698)
- .codex/config.toml ships hooks + codex_hooks keys so auto-activation
  works on both sides of the codex-cli rename (#617)
- caveman-help card shows the Windows config path (%APPDATA%) (#723)
- caveman-parse.js added to HOOK_FILES, opencode payload (.cjs), and the
  regenerated checksums.sha256; manifest now matches shipped hook
  contents — release must bump PINNED_REF to a tag containing these files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
2026-07-21 02:01:27 +02:00
Julius BrusseeandClaude Fable 5 dcd51f16fe fix(compress): stop Windows data loss — UTF-8 + atomic writes (#652, #655, #686)
Every read/write resolved to the locale codec (cp1252/cp949 on Windows):
non-ASCII files were silently mojibake'd, and because Path.write_text
truncates before encoding, a UnicodeEncodeError left the target at 0 bytes.
The backup readback check couldn't catch it — it read back with the same
wrong codec.

- encoding=utf-8 pinned on every I/O call site (compress, validate,
  detect, benchmark); validate now decodes strict — it's the fidelity gate
- write_text_atomic: encode first, temp file in same dir, fsync, preserve
  permissions, os.replace; temp unlinked on any failure
- fix-retry pass gains the same empty-output guard as the first pass
- fix-retry preamble leak (#588): output must start at the original's
  structural anchor (frontmatter/heading) or the attempt is rejected
- primary-write failure now prints the backup path — users hitting the
  crash had no idea a backup existed
- extract_inline_codes: strip fences via the CommonMark-aware extractor;
  the old column-0 regex leaked indented fences into inline-code pairing,
  causing false validation failures (extracted from PR #619's diagnosis)
- SKILL.md/README/SECURITY corrected: backups live in the out-of-tree data
  dir (#420), not beside the source file

Supersedes PRs #683 #678 #626 #534 and the fence fix from #619 with a
local implementation. 58 python tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
2026-07-21 02:00:59 +02:00
Julius BrusseeandClaude Fable 5 d833f4adab feat(stats): surface net savings, not just gross (#145, #677)
/caveman-stats only reported gross output savings, hiding the regime where
the ~1,250-token/turn rule injection costs more than compression saves —
the exact case docs/HONEST-NUMBERS.md warns about.

- Est. rule overhead: per-turn injected-rule input cost x turns
  (CAVEMAN_RULE_OVERHEAD_TOKENS overrides the default 1250)
- Est. net: saved minus overhead; when negative, says plainly that caveman
  cost more than it saved for this workload
- shown only for attributed uniform spans (follows #601 attribution);
  history rows without turn counts are excluded from net, never guessed
- statusline suffix stays gross savings, unchanged semantics

Reimplements the idea from PR #718 locally. 46 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
2026-07-21 02:00:59 +02:00
Julius BrusseeandClaude Fable 5 7693a046ee fix(hooks): shared parser, envelope unwrap, resume-safe SessionStart
- extract mode parsing into src/hooks/caveman-parse.js, consumed by both
  the Claude tracker and the opencode plugin — fixes the three #602 drifts
  (brevity triggers missing, bogus level overwrote flag with default,
  independent modes unreachable via expanded templates)
- unwrap Claude Code's <command-name>/<command-args> slash envelope: real
  slash-UI /caveman <level> and /caveman off were silent no-ops (#537);
  foreign envelopes skip natural-language detection entirely
- SessionStart branches on payload source: startup resets to configured
  default, resume/clear/compact preserve a valid existing flag (#691)
- scheduled-task prompts (<scheduled-task marker) skip flag mutation and
  reinforcement so unattended runs aren't hijacked
- per-turn reinforcement honors repo-local defaultMode off via
  getDefaultMode(cwd) gate — read-only, never deletes the shared flag
  (#634; rejects #532's cross-session flag deletion)
- reinforcement anchor shrunk ~57%, opencode line kept identical (#660)
- statusline setup nudge shown once, gated by .caveman-nudge-shown (#661)
- /caveman-stats delivered via hookSpecificOutput.additionalContext so the
  macOS desktop app renders it (#618)
- safeWriteFlag: retry rename on Windows sharing violations, always unlink
  temp in finally — no more .caveman-active.<pid>.<ts> litter (#511 #578)
- statusline.sh exits 0 on empty suffix file — non-zero exit was hiding
  the whole status bar (#711)
- cavecrew-model-overrides resolves plugin root across layouts; env model
  overrides were a silent no-op (#645)
- opencode dev-tree loader: base require on the loaded file so
  caveman-parse's relative require resolves in both layouts

Supersedes PRs #623 #674 #700 #691 #634 #660 #661 #692 #632 #622 #657
#578 #511 #645 #590 #498 #501 with local implementations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
2026-07-21 02:00:43 +02:00
Julius BrusseeandClaude Fable 5 710173f965 fix(skill): negation safety, tool-call silence, language-drift hardening
Verified against filed reports and applied minimal rewordings:
- never-drop-negations rule — dropping not/never inverts instructions (#670)
- tool-call silence defined: no preamble/progress notes around calls (#713, #714)
- language rule de-named: removes Portuguese/Spanish attractor tokens that
  drove drift under compaction; every emitted line in session language
  (#665, #654, #701, #539)
- particles/postpositions exempt from article-dropping — Korean/Japanese
  case markers are grammar, not filler (#680)
- classical chars scoped to wenyan modes only; Auto-Clarity example marked
  format-only so warnings render in session language (#679, #680)
- switch line covers all six levels + off (#549, #670)
- persisted-text boundary: docs/issues/PR text/memory files write normal
  (#483, #562, #582, #670)
- wenyan-full row: character reduction labeled chars-not-tokens

Supersedes PRs #715 #702 #684 #685 #667 #654 #658 #549 #670 #483 #562 #582
with local implementations. Eval snapshots need regeneration (API-gated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
2026-07-21 02:00:27 +02:00
github-actions[bot] 0d95a81d35 chore: sync SKILL.md copies [skip ci] 2026-07-03 11:08:51 +00:00
Julius BrusseeandClaude Opus 4.8 e2c09c9a7e docs: flatten savings claim to a flat 65% across all surfaces; bump PINNED_REF to v1.9.1
Replace the ~50-65% range and the ~50%-vs-terse figure with a single 65%
(the measured average output reduction vs verbose baseline) on every product
surface: README hero + ASCII card + benchmark blurb, plugin.json,
marketplace.json, gemini-extension.json, caveman SKILL.md + skill README,
caveman-init rule frontmatter, and the docs site (index.html telemetry widget
+ marquee still showed the old 75%). Trim HONEST-NUMBERS.md to match. Raw
benchmark/eval snapshots and the eval-harness methodology docs are untouched.

Bump PINNED_REF v1.9.0 -> v1.9.1 so the standalone hook-download fallback
fetches cavecrew-model-overrides.js from the release tag instead of 404ing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0163QczkBHKpYgxx3sBc7X3b
2026-07-03 13:07:25 +02:00
Julius Brussee dc95e915c5 Caveman Updated Skill + Readme 2026-07-03 12:29:48 +02:00
Julius Brussee 686c0cce64 Teaser; Caveman 2
(It'll be good)
2026-07-03 00:36:08 +02:00
Julius BrusseeandClaude Fable 5 e9cb8435d6 fix(#601): attribute session tokens to the mode active when each message happened
/caveman-stats credited ALL output tokens since session start to whatever
mode the flag held at stats time: activating caveman late inflated the
estimate (verbose tokens back-computed as compressed), deactivating it
zeroed honest savings — and the wrong number compounded into the lifetime
history and statusline.

The flag files carried no timestamps, so past sessions cannot be exactly
reconstructed. Fix going forward + honest fallback:

- caveman-config: recordModeChange() appends {ts, mode, prev} to
  .caveman-mode-log.jsonl on every actual flag transition (deduped,
  symlink-safe via appendFlag, best-effort)
- mode tracker + SessionStart activate hook log every flag mutation
  (set, off, NL deactivation, one-shot restore, session-start reset)
- caveman-stats joins the log timestamps against the session JSONL
  message timestamps and computes savings per mode span; the first
  row's prev covers the pre-inception span
- with no log coverage: a flag written mid-session means the earlier
  tokens have UNKNOWN mode — they are excluded and labeled, never
  guessed (no-fake-savings); with no evidence of a mid-session change,
  whole-session attribution stays (correct when the mode never changed)
- history rows, --share, and the statusline suffix all use the
  attributed figure; mixed sessions render a per-mode breakdown with
  the estimate basis stated

Old inflated history rows cannot be retroactively corrected (the data to
re-attribute them was never recorded).

Tests: mid-session activation (inflation case), mid-session deactivation
(zeroed case), tracker transition-log dedup, unattributable-prefix
exclusion. Checksums manifest refreshed for the four changed hook files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 16:09:27 +02:00
Julius BrusseeandClaude Fable 5 a8846964b5 fix(#597): ship spawn-options.js in caveman-shrink npm package
The files array omitted spawn-options.js, which index.js requires at
startup — the next npm publish would have shipped a package that crashes
with MODULE_NOT_FOUND on every launch (verified via npm pack --dry-run).

- add spawn-options.js to package.json files
- bump 0.1.1 (registry 0.1.0 is stale; the fix is unpublishable without
  a version bump anyway — NOT published here)
- add a static packaging test that walks every relative require reachable
  from the package entry points (bin + main) and fails if any resolved
  module is missing from files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 16:03:04 +02:00
Julius BrusseeandClaude Fable 5 6919dc2c4f fix(stats): budget-saved % was output reduction mislabeled as usage share
The limit-headroom meter (8a5ab60) printed saved/(saved+output) as
'Session budget saved: ~X% of your usage this session' and 'Est. budget
saved: ~X% of tracked usage'. That ratio is algebraically the output
reduction (always ~65% in full mode) with an output-only denominator —
input + cache tokens, which dominate agentic sessions and count against
Pro/Max limits, were excluded. docs/HONEST-NUMBERS.md on this same
branch says real session-level totals land ~14-21% and below zero on
terse workloads, so the label overstated limit relief.

Fix: say only what the math computes.
- Session view: drop the budget line; the saved line now reads
  '(~X% of output)' and the footer states input/cache usage is
  unchanged.
- Lifetime view: relabel to 'Est. output reduction: ~X% (output tokens
  only, est.)'.
- budgetSavedPct -> outputReductionPct with a comment forbidding
  usage/budget relabeling; tests assert no usage/budget claim appears.
- Hook checksum refreshed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:47:02 +02:00
Julius BrusseeandClaude Fable 5 e6cce23ed6 feat: bridge to Caveman Cloud — verified savings link in README + install summary
README gets a two-line Caveman Cloud section near the bottom; the
installer summary now points at /caveman-stats (labeled estimates)
and getcaveman.dev. One link, one honest sentence each — local
numbers are estimates, Cloud measures and verifies them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:33:28 +02:00
Julius BrusseeandClaude Fable 5 ec4f664910 docs(security): no-telemetry statement, privacy facts, scanner-warning notes (#347, #504, #383, #28)
Extends the #614 SECURITY.md scaffolding: verified zero-network-after-
install statement (skill is a prompt; hooks/stats/statusline/shrink
have no http/https/fetch), exact install-time fetch list incl. the
detached curl fallback (release-tag-pinned, SHA-256-verified), what
stays local, air-gapped/enterprise note, and honest explanations for
the Defender (#383) and Snyk caveman-compress (#28) flags. Adds a
top-level README Privacy section (#504) and corrects INSTALL.md's
'no network calls' claim to name the curl-fallback exception.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:33:08 +02:00
Julius BrusseeandClaude Fable 5 d28be46e50 fix(#234): honest savings claim — ~50-65% measured output reduction, not ~75%
README headline now states the measured range with method (65% avg vs
verbose default from benchmarks/, ~50% median vs 'Answer concisely.'
control from evals/) plus explicit caveat: input tokens untouched,
session-level savings smaller. Stats box shows output vs input split,
drops the unmeasured ~3x speed row. Same ~75% claim corrected in
SKILL.md description (+ synced plugin copy and dist/caveman.skill),
plugin.json, marketplace.json, gemini-extension.json, caveman-init
rule frontmatter, skill README, and CLAUDE.md.

Note for maintainer: the GitHub repo description still says 'cuts 65%
of tokens' — should read 'cuts ~50-65% of output tokens (measured)'.
Not changeable from a local commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:32:47 +02:00
Julius BrusseeandClaude Fable 5 335ab56dea docs: HONEST-NUMBERS.md — when caveman saves, when caveman costs (#145, #550, #506)
Plain-truth page: measured output reduction (65% avg vs verbose,
~50% median vs terse control), the ~1-1.5k/turn input cost of the
injected rules, net-negative cases (terse Q&A per #145, per-request
billing per #506, adverse tool-side counters per #550), and how to
A/B it yourself. /caveman-stats savings labeled as estimates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:31:53 +02:00
Julius BrusseeandClaude Fable 5 8a5ab60ef0 feat(stats): limit-headroom meter — session budget saved % for subscription users
Claude Code Pro/Max users don't spend dollars, they spend a 5-hour /
weekly usage limit. /caveman-stats now also reports the savings in
their currency: 'Session budget saved: ~X% of your usage this session
(est.)', and the lifetime view gets 'Est. budget saved: ~X% of tracked
usage (est.)'.

Honesty rules baked in:
- % = saved / (saved + used) from tokens we actually count — nothing
  else. budgetSavedPct returns null (line omitted) when no savings are
  measured: honest zero, no claim.
- no plan-limit sizes are assumed or hardcoded (Anthropic doesn't
  publish token quotas); the footer says so explicitly.
- clearly labeled (est.); USD lines stay for API users.

Checksums refreshed for the caveman-stats.js change. 5 new tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:17:35 +02:00
Julius BrusseeandClaude Fable 5 bceaa0cf6d fix(#528): benchmarks/run.py reads only ANTHROPIC_API_KEY from .env.local
The old loader setdefault'ed EVERY key found in repo-root .env.local
into os.environ. Security scanners (Hermes skill scan, issue #528) flag
that as a high-severity exfiltration surface: install caveman into a
profile with secrets in .env.local and the benchmark quietly pulls all
of them into its process environment.

The benchmark only ever needs ANTHROPIC_API_KEY (anthropic.Anthropic()
reads it implicitly), so read that one key and nothing else — skip the
file entirely when the var is already set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:14:12 +02:00
Julius BrusseeandClaude Fable 5 19f7b5a0c0 fix(#565): install.ps1 works when piped to iex
irm .../install.ps1 | iex crashed with "Cannot bind argument to
parameter 'Path' because it is null": under iex there is no script
file, so $MyInvocation.MyCommand.Path is $null and Split-Path threw.
The top-level param() block also can't receive arguments through a
pipe.

- wrap all logic in Install-Caveman, invoked at the bottom with the
  automatic $args (populated for file runs, empty under iex)
- replace $MyInvocation.MyCommand.Path with $PSCommandPath, guarded —
  pipe installs skip the local-clone branch and go straight to npx
- static regression tests in tests/installer/ps1-pipe.test.mjs (CI has
  no pwsh, so pin the pipe-safety contract textually)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:13:19 +02:00
Julius BrusseeandClaude Fable 5 5f079ab7d8 fix(#571): ship commands/*.md so Claude Code discovers /caveman slash commands
Claude Code only scans commands/*.md (YAML frontmatter) for plugin slash
commands — the commands/*.toml files are the Gemini extension format and
are ignored, so /caveman, /caveman-commit, /caveman-review,
/caveman-stats and /caveman-init all returned Unknown command after a
plugin install.

- add commands/{caveman,caveman-commit,caveman-review,caveman-stats,
  caveman-init}.md mirroring the toml prompts ({{args}} -> $ARGUMENTS)
- caveman-stats.md body still matches the mode-tracker intercept regex
  so the hook injects real numbers, model computes nothing
- caveman-init.md uses the standalone-fallback body from #603
- keep every .toml — Gemini CLI extensions only read TOML
- pin the two-format contract + hook-regex + no-{{args}}-leak in
  tests/installer/slash-commands.test.mjs

The namespaced-command side of #599 (/caveman:caveman-commit etc.) is
already handled by the mode-tracker (both bare and namespaced forms,
one-shot restore) — this closes the discovery half.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:11:52 +02:00
Julius BrusseeandClaude Fable 5 51e1990340 fix(#603): caveman-init command no longer needs the caveman repo checkout
commands/caveman-init.toml told every installed Codex/Gemini user to run
node src/tools/caveman-init.js — a path that only exists inside the
caveman clone, so the command failed with Cannot find module for
everyone but caveman developers.

- toml prompt now gates the repo-relative path on the file existing and
  falls back to the standalone script via curl | node -
- src/tools/caveman-init.js: the documented curl|node stdin path was
  silently a no-op — require.main is undefined under node -, so the
  require.main === module guard never ran main() (exit 0, no output,
  no files). Guard now also matches module.id === '[stdin]'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
2026-07-02 15:11:38 +02:00
Julius Brussee 15465d30b5 Merge PR #499: fix(shrink): restore nested protected-segment sentinels (fixes #444) 2026-07-02 15:00:17 +02:00
Julius Brussee daa8ec3732 fix: refresh src/hooks checksums.sha256 after #608/#591/#538 hook changes 2026-07-02 14:57:21 +02:00
Julius Brusseeandousamabenyounes e7ee55f936 fix(#538): handle stdin 'error' in mode-tracker (hand-applied from PR #560)
PR #560 shipped the regression test but its source change never made it
into the diff — the branch only adds tests/test_mode_tracker_stdin.js.
Applied the one-line silent-exit error listener the PR describes and
took the author's test verbatim.

Co-authored-by: ousamabenyounes <ousamabenyounes@users.noreply.github.com>
2026-07-02 14:57:10 +02:00
Julius Brussee bd36cdc953 Merge PR #614: add SECURITY.md, CODE_OF_CONDUCT.md, .editorconfig repository standards 2026-07-02 14:56:16 +02:00
Julius Brussee 452055463b Merge PR #591: fix(hooks): SessionStart SKILL.md path off-by-one (fixes #587, #589) 2026-07-02 14:55:48 +02:00
Julius Brussee a2cb2f95e8 Merge PR #608: fix(hooks): NL trigger misfires + one-shot independent modes (fixes #598, #599) 2026-07-02 14:55:36 +02:00
Julius Brussee c1e1ffdd47 Merge PR #606: fix(compress): never classify build files or shebang scripts as prose (fixes #600) 2026-07-02 14:55:36 +02:00
Julius Brussee 14eea5d5ee Merge PR #586: fix(install): avoid EXDEV cross-device link on claude plugin install (fixes #585)
# Conflicts:
#	bin/install.js
2026-07-02 14:55:21 +02:00
Julius Brussee cdb0d0c613 Merge PR #604: fix(install): treat spawn ENOENT as failure, not success (fixes #592)
# Conflicts:
#	tests/installer/e2e.freshinstall.test.mjs
2026-07-02 14:54:36 +02:00
Julius Brussee 9663344b79 Merge PR #607: fix(settings): string-aware trailing-comma removal in JSONC reader (fixes #595) 2026-07-02 14:52:58 +02:00
Julius Brussee d900443b84 Merge PR #605: fix(uninstall): match managed hook basenames, not substring (fixes #593) 2026-07-02 14:52:58 +02:00
Julius Brussee 5b875b8e2e Merge PR #609: fix(openclaw): stray SOUL.md marker no longer chains into data loss (fixes #596)
# Conflicts:
#	tests/installer/e2e.freshinstall.test.mjs
2026-07-02 14:52:49 +02:00
Julius Brussee 79a14ee79f Merge PR #610: fix(opencode): --force migrates legacy AGENTS.md, never wipes it (fixes #594) 2026-07-02 14:51:43 +02:00
Shiva Vinodkumar c0e3c1e140 Add/Update .editorconfig to improve repository standards 2026-07-02 17:57:43 +05:30
Shiva Vinodkumar 79285a4c00 Add/Update SECURITY.md to improve repository standards 2026-07-02 17:57:40 +05:30
Shiva Vinodkumar 7dc87d0a32 Add/Update CODE_OF_CONDUCT.md to improve repository standards 2026-07-02 17:57:38 +05:30
AmirF194 516f917619 fix(opencode): --force migrates legacy AGENTS.md, never wipes it
--force on an AGENTS.md containing the legacy un-fenced sentinel
replaced the entire file with the fenced block — destroying all
user-authored content around the legacy block. The installer's own
hint told users with mixed files to run exactly that.

Migrate instead: back up once to AGENTS.md.bak, remove the legacy
block (exact match of the current rule body when possible, otherwise
cut from the sentinel's paragraph start — the legacy path appended the
block, so user content precedes it), and write the fenced block after
the preserved user content.

Fixes #594
2026-07-01 23:52:36 -06:00
AmirF194 ae02d2a560 fix(openclaw): stray SOUL.md marker no longer chains into data loss
A truncated/stray marker (interrupted write, partial user edit) chained
into deleting user content: appendBootstrapToSoul saw 'no complete
block' and appended a second one; stripBootstrapFromSoul then cut from
the FIRST begin to the FIRST end — spanning everything between the
stray marker and the appended block. Reported reproduction ended with
the whole SOUL.md deleted.

Replace the single-span cut with a scan that pairs each begin with the
nearest end before the next begin; unpaired markers are removed as just
the marker text, never as a span. Append now detects damaged markers
(orphans, duplicates), strips them safely, and writes one clean block.

Fixes #596
2026-07-01 23:50:28 -06:00
AmirF194 2fb7c91183 fix(hooks): NL trigger misfires + one-shot independent modes
Natural-language matching (#598):
- Deactivation computed first, word-order tolerant: 'turn caveman mode
  off' used to ACTIVATE caveman (and reset the level to default),
  'turn caveman off' was a no-op.
- 'enable caveman and stop apologizing' no longer deactivates (the old
  stop-guard fired on 'stop' anywhere, then the deactivation regex
  matched 'caveman and stop').
- Questions ('what is caveman mode?') no longer arm the mode.
- 'normal mode' deactivates only as a command or with caveman context
  ('how do I exit vim normal mode' no longer kills the session mode).
- Prompts normalized to one line so multiline input matches.
- Scoped brevity ('be brief in the summary section') is a one-off
  instruction, not a session-wide switch.

One-shot modes (#599):
- /caveman-commit|-review|-compress save the displaced prose mode to
  .caveman-active.prev and the next ordinary prompt restores it (or
  deactivates if caveman wasn't active before) — SKILL.md's 'level
  persist until changed or session end' holds again.
- Plugin-namespaced /caveman:caveman-commit and -review recognized
  (only compress and stats had the variant).
- Deactivation clears the saved prev so nothing resurrects the mode.

Fixes #598, fixes #599
2026-07-01 23:47:59 -06:00
AmirF194 704a460ef8 fix(settings): string-aware trailing-comma removal in JSONC reader
The trailing-comma sweep ran a global regex over the whole
comment-stripped output, including string contents — a JSONC
settings.json with a value containing ',}' or ',]' (shell brace
expansion, inline JSON in hook args) was silently corrupted on read
and persisted corrupted on the next write.

Replace the regex with a scan that tracks string state (same approach
as the comment stripper above it) and only drops commas outside
strings.

Fixes #595
2026-07-01 23:44:57 -06:00
AmirF194 4dad1afe84 fix(compress): never classify build files or scripts as prose
detect.py listed .dockerfile/.makefile in SKIP_EXTENSIONS, but real
files are named Dockerfile/Makefile with no extension, so they fell
through to the content heuristic and came back compressible —
/caveman-compress Dockerfile overwrote a Dockerfile with caveman prose.

Add a basename guard (Dockerfile, Makefile, Jenkinsfile, Vagrantfile,
CMakeLists.txt, ...) checked before any extension rule — CMakeLists.txt
would otherwise ride the compressible .txt rule — and a shebang check
in the extensionless branch so executable scripts are always code.

Mirror synced to plugins/caveman (sync workflow only triggers on
SKILL.md changes, so scripts/ must ride along).

Fixes #600
2026-07-01 23:36:17 -06:00
AmirF194 70ea40dced fix(uninstall): match managed hook basenames, not substring
removeCavemanHooks stripped any settings.json hook whose command
contained the substring 'caveman' anywhere — a user-authored hook like
'node ~/Projects/caveman-notes/my-hook.js' was silently deleted by
--uninstall.

Match tokens against MANAGED_HOOK_BASENAMES by exact basename instead
(win32.basename so Windows-written configs match anywhere), the same
pattern pruneOrphanedManagedHooks already uses. Hoist the tokenizer to
module scope and reuse it in the prune pass. Add caveman-statusline.ps1
to the managed set so the Windows statusline wiring is covered by
removal and orphan-pruning too.

Fixes #593
2026-07-01 23:34:41 -06:00
AmirF194 959b943ad4 fix(install): treat spawn ENOENT as failure, not success
spawnSync reports a missing binary as { status: null, error }, and the
(r.status || 0) === 0 checks coerced that null to success. On a machine
without the claude CLI, --only claude printed 'installed: claude',
skipped the standalone-hook fallback (which works offline), and left
nothing installed. Same pattern at 8 sites: claude, gemini, npx-skills
providers, mcp-shrink, runInit (both paths), uninstall, skills-auto.

Route every spawn result through spawnOk() (!r.error && r.status === 0)
and warn when the claude CLI itself could not be spawned. Regression
test runs the installer with an empty PATH and asserts failure is
reported and standalone hooks get wired.

Fixes #592
2026-07-01 23:31:35 -06:00
AmirF194 6d71b9ec6f fix(hooks): SessionStart SKILL.md path off-by-one
Hook lives at <plugin_root>/src/hooks/ but read SKILL.md via a single
'..' — resolving to nonexistent src/skills/, so every plugin install
silently fell back to the stale hardcoded ruleset (missing language
preservation, no-self-reference, intensity table).

Resolve via candidates in order: $CLAUDE_PLUGIN_ROOT/skills/,
__dirname/../../skills/ (plugin + repo layout), __dirname/../skills/
(standalone $CLAUDE_CONFIG_DIR layout). Sync the two missing rules
into the fallback for installs with no SKILL.md at all.

Fixes #587, fixes #589
2026-07-01 22:42:53 -06:00
Paul Jobson e0bb39f267 Fix for: https://github.com/JuliusBrussee/caveman/issues/585
This fixes the edge case for the following error by instantiating the
paths differently.

✘ Failed to install plugin "caveman@caveman": EXDEV: cross-device link not permitted
2026-06-30 21:44:27 -04:00
Ousama Ben YounesandClaude 93d9a439b4 fix(shrink): restore nested protected-segment sentinels (#444)
withProtectedSegments built sentinels in a single pass and restored them
in a single pass. When two PROTECTED_PATTERNS matched the same span
(e.g. path rule swallows STARTER/BUSINESS, then function-call rule
swallows the resulting type ( 0 )), the outer sentinel restored to
"type ( 0 )" but the inner " 0 " was never substituted back — enum
values reaching the model became literal "( 0 )".

Restore now loops with MAX_RESTORE_PASSES bound; same depth is reached
on the actual #444 inputs in <=2 passes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-24 08:55:49 +00:00
Julius BrusseeandClaude Opus 4.8 e8139f86e3 fix(#524): make Hermes install/uninstall symmetric + verify against real Hermes
Adversarial review against a live Hermes Agent install (v0.11.0) found the
forward install was correct (skills land in the real ~/.hermes/skills/
productivity/<skill>/ layout, all 7 load as 'enabled' via 'hermes skills
list' — verified empirically, no version: field required), but uninstall had
NO Hermes handling: --uninstall silently orphaned all 7 skill folders forever.

- add Hermes block to uninstall() honoring HERMES_HOME (mirrors opencode/openclaw)
- tests/installer/hermes.test.mjs: install lands 7 skills, uninstall removes
  them (regression guard for the asymmetry), dry-run uninstall is a no-op
- INSTALL.md: add Hermes Agent row to the per-agent install table (CLAUDE.md
  mandates the install table stay complete)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:25:44 +02:00
Julius BrusseeandClaude Opus 4.8 6ebdb375c4 fix(#524): clean up Hermes provider entry
- restore 2-space PROVIDERS indent and 4-space dispatch-loop indent (PR
  re-indented existing gemini/opencode/openclaw lines as churn)
- correct mech label: 'native hermes skills copy' (it does a native dir
  copy via installHermes, not 'npx skills add')
- drop dead profile: 'hermes' — shadowed by the installHermes special-case
  and not a valid upstream vercel-labs/skills slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:05:27 +02:00
Julius Brussee 62446eb525 Merge PR #524: add Hermes Agent install support 2026-06-14 22:03:00 +02:00
Julius BrusseeandClaude Opus 4.8 62e77e66ff fix(#527): correct stale checksums for caveman-activate.js + cavecrew-model-overrides.js
PR #527 added cavecrew-model-overrides.js with a wrong sha256 (eae3... vs
actual 9f26...) and modified caveman-activate.js without regenerating its
entry. bin/install.js verifies downloaded hooks against this manifest and
aborts on mismatch, so both were latent install-breakers on the remote path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:02:49 +02:00
Julius Brussee f60a45a98d Merge PR #527: per-agent cavecrew model overrides (#521) 2026-06-14 22:01:44 +02:00
Julius Brussee c1ac0664c9 Merge PR #505: register /caveman-stats slash command (#470) 2026-06-14 22:01:18 +02:00
Ijtihed Kilani a325c64dd4 feat(cavecrew): add per-agent model overrides 2026-06-14 13:16:56 +03:00
vagesh 8753bf90d5 feat(install): add Hermes Agent support
Adds Hermes Agent as a supported provider with native skill installation.
Hermes Agent uses SKILL.md format (same as upstream), so the installer
copies all 7 caveman skills directly to ~/.hermes/skills/productivity/.

Closes #45914 (related)
2026-06-14 12:29:42 +10:00
Julius Brussee 25d22f864a Update README.md 2026-06-12 15:51:04 +02:00
Julius BrusseeandClaude Opus 4.8 32f37af81a chore(release): pin remote fetches to v1.9.0
First tag shipping src/hooks/checksums.sha256 — with PINNED_REF now
pointing at it, curl|bash / detached installs fetch hook files from the
immutable v1.9.0 ref and SHA-256 enforcement activates fully (#261,
#262). Manifest verified current against src/hooks/ before the bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:57:52 +02:00
Julius BrusseeandClaude Opus 4.8 ddf212173a fix(opencode): plugin now loads in compiled Bun runtime
Smoke-tested against real opencode 1.4.0 (the missing verification
flagged in 22f75e3) — the plugin never loaded: opencode runs plugins
inside a compiled Bun binary where require() of on-disk files is
rejected ('require() async module is unsupported') and await import()
of a CJS file returns an empty namespace. caveman-config.cjs is now
evaluated as CommonJS by hand (readFileSync + Function wrapper with a
createRequire shim — built-ins still resolve fine).

Three more real-runtime gaps found and fixed in the same pass:
- session-init flag write now also happens at plugin factory time; in
  one-shot 'opencode run' the first session.created publishes before
  plugin event dispatch is wired, so the event handler alone missed it
- the TUI expands '/caveman <level>' into the command template before
  chat.message fires; the parser now recovers the level from the
  expanded 'Activate caveman mode: <level>' text
- the non-interactive run path wraps messages in literal quotes
  ('"/caveman ultra"'); the parser unwraps symmetric quotes

Verified end-to-end against opencode 1.4.0: plugin loads clean,
session-init writes the flag, /caveman ultra flips it, 'stop caveman'
deletes it, and the reinforcement line shows up in the outgoing LLM
system prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:57:15 +02:00
github-actions[bot] 22e59bf067 chore: sync SKILL.md copies [skip ci] 2026-06-12 12:17:17 +00:00
Julius BrusseeandClaude Opus 4.8 efd490a5fc test: fix caveman-init counts, isolate openclaw ws
Counts were stale after the opencode + openclaw targets landed (5 -> 6
repo files, 7 with the openclaw line). Tests also ran the openclaw
installer against the developer's real ~/.openclaw/workspace; runInit()
now pins OPENCLAW_WORKSPACE inside the fixture tmp dir so nothing
escapes the sandbox. verify_repo.py drops the removed
.agents/plugins/marketplace.json from its manifest list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:13:28 +02:00
Julius BrusseeandClaude Opus 4.8 eb447dad2e chore: remove stale dotdir cavecrew mirrors
.junie/, .kiro/, .roo/, .agents/ held pre-cleanup cavecrew/SKILL.md
mirrors (plus an unused .agents/plugins/marketplace.json). Nothing in
the current install path reads them — bin/install.js handles Codex via
npx skills add, and Claude Code marketplace discovery uses
.claude-plugin/marketplace.json. CLAUDE.md marks these remove-on-sight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:12:04 +02:00
Julius Brussee baf10366f6 Add Sponsors section and Atlas Cloud logos
Add a Sponsors section to README promoting Atlas Cloud with a centered logo, link, and a call to sponsor the project. Include two new SVG assets (atlas-cloud.svg and atlas-cloud-dark.svg) under docs/assets for light/dark mode logo display.
2026-06-12 14:05:15 +02:00
Ousama Ben YounesandClaude Opus 4.7 b5e8cdaf1f fix(commands): add caveman-stats.toml so Claude Code registers /caveman-stats
Without commands/caveman-stats.toml on disk Claude Code rejects
/caveman-stats as 'Unknown command' before the UserPromptSubmit
hook in src/hooks/caveman-mode-tracker.js can intercept it. README
and INSTALL.md both advertise the command; this brings the slash
command registry in line with the docs.

Fixes #470

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 10:04:13 +00:00
Julius BrusseeandClaude Opus 4.8 bdcba4c6ef chore: sync plugin mirror + dist locally (replicates CI sync-skill)
Verbatim copies of edited sources into the CI-managed plugins/caveman mirror and rebuilt dist/caveman.skill, so local main is self-consistent without a push. CI's sync-skill.yml regenerates these on push; this just keeps verify_repo green locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:05:24 +02:00
Julius BrusseeandClaude Opus 4.8 22f75e3de6 fix(opencode): real lifecycle hooks + ship caveman-compress command
Folds in #419 (replace the non-existent session.created/tui.prompt.append hooks with real opencode hooks: event dispatcher for session.created, chat.message for mode parsing, experimental.chat.system.transform for reinforcement; fixes #418/#421), #398 (ship the missing caveman-compress.md command + un-ignore it; fixes #426/#451/#464), and #376 plugin-side (drop %APPDATA% branch). Smoke test rewritten for the new hook shapes. NOTE: not smoke-tested against a real opencode runtime here — verify before release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:05:24 +02:00
Julius BrusseeandClaude Opus 4.8 f0dd780305 fix(install): Windows/Copilot/skills fixes, hook-wiring + MCP-shrink, security hardening
Installer fixes: #414 (rename PS1 $Args->$InstallerArgs), #437 (detect Copilot via extension dirs, fixes #336), #395 (--skill '*' instead of --all so -a <agent> is honored, fixes #389), #472 (prune orphaned managed hooks from settings.json, fixes #471), #393 (don't double-wire hooks when the plugin manifest already does, fixes #392), #380 (MCP-shrink off by default, requires an upstream, fixes #474), #376 install-side (opencode uses ~/.config/opencode, drop %APPDATA%), #443 (strip tools: from cavecrew agent copies for opencode, #386), #434 (existsSync guard on command copy), #396 (doc: discover profile slugs via --list).

Security hardening: #261 (pin remote fetch to release tag PINNED_REF=v1.8.2, not moving main) and #262 (SHA-256-verify downloaded hook files against src/hooks/checksums.sha256 before they execute; abort on mismatch). #260 (inspect-before-run note). NOTE: enforcement activates fully once a release tag shipping checksums.sha256 is published and PINNED_REF is bumped; v1.8.2 predates the manifest so downloads there warn-and-proceed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:05:24 +02:00
Julius BrusseeandClaude Opus 4.8 cd4009effa feat(config): repo-local .caveman/config.json + NL brevity triggers
Folds in #429 (repo-local <repo>/.caveman/config.json resolution layer between env and user config; symlink-safe, bounded walk) and #248 intent (recognize 'less tokens'/'be brief'/'be terse' as natural-language caveman activation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:04:58 +02:00
Julius BrusseeandClaude Opus 4.8 6ce47d4445 fix(mcp-shrink): shell:true on Windows so npx/.cmd upstream resolves
Folds in #387 — extract spawn options to spawn-options.js; shell:true only on win32 (PATHEXT resolution), POSIX unchanged. Args still come from installer-controlled config (trust boundary unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:04:58 +02:00
Julius BrusseeandClaude Opus 4.8 46de578a7e fix(docs): escape user input in demo terminal (XSS)
Folds in #438 — the docs demo terminal interpolated user input via innerHTML (real reflected/DOM XSS); build nodes with textContent instead. (PR title 'CLI input handler' was a misnomer.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:04:58 +02:00
Julius BrusseeandClaude Opus 4.8 f68111acc3 fix(stats): Opus output price $75->$25 for 4.5+ era; Windows statusline UTF-8
Folds in #466 (correct Opus 4.5-4.8 output price to $25/M, keep legacy 4.0/4.1 carve-outs at $75/M; fixes #465) and #459 (UTF-8 console + Get-Content so the pickaxe renders on Windows; +spacing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:04:58 +02:00
Julius BrusseeandClaude Opus 4.8 f06348cbd3 fix(skill): preserve user language, no self-reference, full-mode guardrails
Folds in #446 (preserve user's dominant language, keep code/CLI/commit-type/error strings verbatim; fixes #445), #469 (no self-reference / caveman-only output; fixes #468), #322 intent (full-mode output guardrails), #238 (ultra-row prose-abbreviation clarification), #230 (allow Assisted-by commit trailer). Also fixes the corrupted wenyan-full useMemo example in SKILL.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:04:58 +02:00
Julius BrusseeandClaude Opus 4.8 e8eae0ff28 fix(compress): utf-8 pin, Windows .cmd resolve, frontmatter + backup-dir
Folds in #388 (pin claude subprocess to utf-8, fixes #152 Windows cp1252 crash), #435 (resolve claude via shutil.which for .cmd shims), #424 (preserve YAML frontmatter across compression), #420 (write .original.md backups outside the source tree; cross-platform base dir incl. Windows %LOCALAPPDATA%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:04:58 +02:00
Julius BrusseeandClaude Opus 4.7 655b7d9c54 docs: feature caveman-code with a callout card after Before/After
Replace the top-of-README alert banner with a centered callout card
placed right after the savings proof, where the reader is most sold.
Cross-sells caveman-code (the full terminal coding agent) on the
momentum of the compression demo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:43:44 +02:00
Julius BrusseeandClaude Opus 4.7 2422bbe59c docs: add caveman-code promo banner + dancing-rock branding
Add a prominent caveman-code callout to the README hero and a
caveman-code row to the ecosystem table. Swap the rock emoji for
the dancing-rock SVG logo across the README, the compress skill
README, and the docs-site cursor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:36:09 +02:00
Julius Brussee 18e45320a0 docs: add cavegemma to ecosystem table
Fourth rock in the family — Gemma 4 31B fine-tuned on caveman pairs.
2026-05-18 00:37:23 +02:00
Julius BrusseeandClaude Opus 4.7 63a91ecadb fix(install): force --yes --all on npx skills add (issue #370)
curl|bash one-liner stdin is not a TTY, so the upstream skills CLI
renders its interactive skill-picker TUI with nothing selected, exits
0, and installs zero skills — while our installer reports "done".
Pass --yes --all to skip the picker and confirmation prompts in both
the per-provider call and the auto-detect fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:44:32 +02:00
Julius Brussee e8b69797a8 Merge critical PR fixes locally (no push)
- PR #372 (Dave): strip YAML fences from commands/caveman-init.toml — Gemini CLI's FileCommandLoader rejected the file with 'Failed to parse TOML' on extension install. Verified main was broken.
- PR #350 intent (David): rename codex_hooks → hooks in .codex/config.toml. Codex source confirms canonical key is now 'hooks'; codex_hooks is a legacy alias that emits a deprecation log.

Closes #372 #326 (duplicate) #350 locally — push deferred.
2026-05-12 21:26:15 +02:00
279310971a fix(codex): rename codex_hooks to hooks per latest config schema
Codex's CodexHooks feature now exposes key 'hooks' (Stable). codex_hooks
kept as legacy alias in codex-rs/features/src/legacy.rs but emits a
deprecation log. Match the canonical name.

Verified against openai/codex codex-rs/features/src/lib.rs:
  FeatureSpec { id: Feature::CodexHooks, key: "hooks", stage: Stage::Stable, default_enabled: true }

Co-Authored-By: David <davidbits@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:26:08 +02:00
DaveandJulius Brussee 21b15183ed chore: remove unnecessary comments from caveman-init.toml 2026-05-12 21:25:53 +02:00
Julius BrusseeandClaude Opus 4.7 754795ada4 fix(install): unbreak curl|bash one-liner (regression in v1.8.0)
Two bugs at the curl|bash entry point made the headline install command
fail immediately:

1. install.sh used `${BASH_SOURCE[0]}` under `set -u`. That variable is
   unset when bash is invoked from stdin (curl | bash), tripping the
   nounset trap before we ever reached the npx fallback.
2. install.sh + install.ps1 passed `--` between npx and the package args.
   On modern npm, npx forwards the literal `--` to bin/install.js, which
   parseArgs rejected as an unknown flag.

Fix:
- install.sh: default BASH_SOURCE[0] to empty so the curl-pipe path falls
  through cleanly under set -u.
- install.sh + install.ps1: drop the `--` separator. npm 7+ npx already
  forwards trailing args correctly.
- bin/install.js parseArgs: accept a bare `--` as a no-op (POSIX
  end-of-options marker) so future shim drift can't re-break this.
- New regression test asserts `--` is accepted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:04:44 +02:00
github-actions[bot] dce88c2f2e chore: sync SKILL.md copies [skip ci] 2026-05-10 13:52:06 +00:00
Julius Brussee e843518438 Improve installer idempotency and opencode fencing
Refactor and harden the unified Node installer and related docs. Major changes:

- bin/install.js: validate --only ids, expand ~ for --config-dir, make Claude/install flows async (await hooks/init), add many CLI flags (--no-mcp-shrink, --with-hooks/--no-hooks, --skip-skills, --config-dir docs), preserve original backups once, avoid overwriting plugin/command files unless --force, and add opencode AGENTS.md fenced begin/end markers so installs/uninstalls can append/strip cleanly. Also add opencode idempotency probes for claude/gemini uninstall paths and better handling of opencode plugin payload.
- bin/lib/settings.js: safer removeCavemanHooks that validates shapes before mutating hooks.
- runInit/installHooks/downloadTo calls made async and awaited; runInit returns promise now.
- Docs and README/INSTALL/CONTRIBUTING/CLAUDE.md/src/hooks/README.md: update user-facing text to match new flags/behaviour, standardize use of $CLAUDE_CONFIG_DIR, clarify --with-init semantics, and note opencode/openclaw handling. Change command name usages from `/caveman:compress` to `/caveman-compress`.
- opencode: write fenced caveman block to AGENTS.md and handle legacy unfenced blocks; tests updated to expect fence markers.

These changes improve idempotency, safer upgrades/uninstalls, clearer UX around per-repo vs per-user init, and make the opencode rule block removable without destroying user content.
2026-05-10 15:50:33 +02:00
Julius BrusseeandClaude Opus 4.7 7b2bed2d0b feat(openclaw): add OpenClaw as a first-class agent target
OpenClaw is a self-hosted gateway that orchestrates multiple agents and
loads workspace skills on-demand. To make caveman always-on through it we
write a spec-correct skill folder plus a marker-fenced bootstrap block in
SOUL.md (which OpenClaw auto-injects every turn). Both writes are
idempotent and reachable from `bin/install.js --only openclaw` and
`caveman-init.js --only openclaw`. Side-effect: `--only <id>` now bypasses
the detect-match guard so explicit opt-in works for any provider whose
preconditions can't be probed (e.g. custom OPENCLAW_WORKSPACE paths).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:18:00 +02:00
Julius BrusseeandClaude Opus 4.7 8b8068d8ca feat(opencode): native plugin replaces npx-skills fallback
Replaces opencode's Tier-4 npx-skills entry with a native in-repo plugin
that mirrors the Claude Code hook architecture (session.created +
tui.prompt.append) using opencode's lifecycle hook system. Reaches Tier-1
parity minus the statusline (opencode TUI exposes no plugin-writable
badge). Skill files drop in unchanged — opencode reads SKILL.md natively.

Plugin reuses src/hooks/caveman-config.js for the symlink-safe flag-write
helpers via createRequire (renamed .cjs post-install to coexist with the
plugin dir's "type":"module"). AGENTS.md provides a Tier-3 always-on
fallback if the plugin runtime breaks.

5 new tests cover fresh install, idempotency, JSONC tolerance of
pre-existing opencode.json, uninstall, and a plugin smoke test that fires
synthetic tui.prompt.append events. All 38 installer tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:08:17 +02:00
Julius Brussee 1ce38a2295 refactor: consolidate hooks/ rules/ tools/ mcp-servers/ under src/
Drops top-level entry count from ~26 to ~22 to align with conventions in
mature skill libraries (anthropics/skills, vercel-labs/skills). agents/,
commands/, skills/, and .claude-plugin/plugin.json stay at the repo root
because Claude Code auto-discovers them at the plugin root.

What moved:
  hooks/        → src/hooks/
  rules/        → src/rules/
  tools/        → src/tools/
  mcp-servers/  → src/mcp-servers/

Path references updated atomically in:
  - bin/install.js (HOOKS_REMOTE, INIT_SCRIPT_URL, repoRoot/sourceDir paths,
    user-facing install hints)
  - .claude-plugin/plugin.json (CLAUDE_PLUGIN_ROOT/src/hooks/...)
  - package.json files: array
  - tests/{test_hooks,test_symlink_flag,test_caveman_init,test_mcp_shrink,
    verify_repo}: bash/node command paths and require() args
  - tests/verify_repo.py: also drops the dead .cursor/.windsurf/.clinerules/
    copilot-instructions/caveman dotdir-mirror checks left behind by be714a3
    plus adds bin/install.js + bin/lib/settings.js regression guards
  - commands/caveman-init.toml prompt
  - src/mcp-servers/caveman-shrink/package.json repository.directory
  - CLAUDE.md, CONTRIBUTING.md, README.md, src/hooks/install.{sh,ps1},
    src/hooks/uninstall.{sh,ps1}, src/tools/caveman-init.js: docstrings,
    URL paths, source-of-truth tables

External raw.githubusercontent.com URL updates (referrers, blog posts) are
the maintainer's lane.
2026-05-10 14:26:11 +02:00
Julius Brussee 4f2314ae43 feat: land bin/install.js + JSONC settings helper + installer tests
Brings the long-stashed Node installer onto main. install.sh and install.ps1
shrink to thin shims (~50 lines each) that delegate to bin/install.js, fixing
the cross-platform drift that caused #249-class quoting bugs.

- bin/install.js (850 lines) — unified PROVIDERS-driven installer
- bin/lib/settings.js (221 lines) — JSONC parser + hook validator
  (validateHookFields prevents single bad hook from poisoning settings.json)
- tests/installer/{unit.argv,unit.settings,e2e.dryrun}.test.mjs — npm test
  now actually runs four real tests (was silently passing 0)
- .agents/skills/cavecrew, .junie/, .kiro/, .roo/ — per-agent skill mirrors
- skills-lock.json — vercel-labs/skills slug pinning
- install.{sh,ps1}.legacy escape hatch dropped (git history is the fallback)
- Minor cavecrew agent description refinements

Closes the gap between docs (already merged) describing bin/install.js and
the actual implementation.
2026-05-10 14:11:17 +02:00
Julius BrusseeandClaude Opus 4.7 5786dd56cc docs: update CLAUDE.md for new layout
Reflect the post-cleanup repo layout: consolidated caveman-compress
under skills/, removed agent-dotdir mirrors, build artifacts in dist/,
per-skill READMEs alongside SKILL.md, bin/install.js as the only
installer entry point, and INSTALL.md for the per-agent reference.
Adds a top-level "What lives where" tree, trims the auto-synced table
to what CI actually mirrors, and updates the key rules accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:10:00 +02:00
Julius BrusseeandClaude Opus 4.7 a2dca8194d test(installer): add fresh-install e2e test against temp CLAUDE_CONFIG_DIR
Real install harness — writes hooks, merges settings.json, asserts on-disk
state. Catches regressions a dry-run can't see (missing hooks, malformed
settings, broken statusline). Five cases:

  - Fresh install populates hooks dir + wires SessionStart/UserPromptSubmit
  - Idempotent install does not duplicate hook entries
  - Uninstall strips caveman hooks, preserves user-authored ones
  - Install tolerates JSONC settings.json (#249 regression guard)
  - Lib-level addCommandHook idempotency (always runs, no claude CLI)

Tests requiring `claude` on PATH skip cleanly with a clear reason. Uninstall
test strips claude/gemini from PATH so the user's real plugin/extension
state is never touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:09:55 +02:00
Julius BrusseeandClaude Opus 4.7 b67c404e80 docs: restructure README.md, route install details to INSTALL.md
Trim README from 16.4 KB to 10.2 KB (~38% shorter, 331 -> 220 lines)
so non-technical readers can scan it in 60 seconds. Front door now
keeps the Before/After pitch, one-line install, top-6 manual install
table, condensed feature matrix, real benchmark numbers, and a short
"how it work" section in caveman voice.

Moves to other docs:
- Full 30+ agent install matrix and detailed flag reference -> INSTALL.md
- Hook architecture deep-dive -> CLAUDE.md (already there)
- Eval methodology paragraphs -> evals/ link

Caveman voice phrases preserved ("Brain still big", "Cost go down
forever", "One rock. Two rock. Three rock. That it.", "caveman speak").
Benchmark numbers untouched (verbatim from benchmarks/results/). All
top-of-funnel install commands still work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:09:51 +02:00
Julius BrusseeandClaude Opus 4.7 3c1743ea11 chore: remove agent-dotdir mirrors at repo root
Drop CI-generated mirrors that self-applied caveman to this repo when
opened in Cursor/Windsurf/Cline/Copilot. The installer never read them
(bin/install.js consumes only hooks/, rules/, tools/, agents/, skills/,
mcp-servers/, plugins/caveman/), so removing them just declutters the
root. Devs who want self-application can opt in via npx caveman --only
<agent>.

Removed:
- .cursor/skills/, .cursor/rules/caveman.mdc
- .windsurf/skills/, .windsurf/rules/caveman.md
- .clinerules/caveman.md
- .github/copilot-instructions.md
- caveman/SKILL.md (and empty caveman/ parent)

Updated .github/workflows/sync-skill.yml: drop the cp + mkdir lines and
git add paths for those mirrors, drop the entire "Sync auto-activation
rules" step, and drop rules/caveman-activate.md from the paths: trigger
since the workflow no longer consumes it. plugins/caveman/, cavecrew,
compress, and caveman.skill ZIP sync steps are preserved.

Updated CONTRIBUTING.md note to reflect the smaller auto-synced set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:08:51 +02:00
Julius BrusseeandClaude Opus 4.7 086c43077d refactor: consolidate caveman-compress into skills/caveman-compress, eliminate compress mirror
- Move caveman-compress/ source (SKILL.md, scripts/, README.md, SECURITY.md) to skills/caveman-compress/.
- Delete skills/compress/ — the CI-generated rename mirror that caused dual-source confusion.
- Move plugins/caveman/skills/compress/ to plugins/caveman/skills/caveman-compress/. Plugin keeps the consolidated name; CI no longer rewrites the frontmatter.
- Replace the two sed-heavy CI sync steps with one verbatim cp -r from source to plugin.
- Update verify_repo.py, test_compress_safety.py, test_validate_inline.py, GEMINI.md, AGENTS.md, CONTRIBUTING.md, README.md, and the workflow paths to reference the new location.
- Use Path.resolve().parents[N] for the benchmark.py repo-root walk now that the directory depth changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:07:58 +02:00
Julius Brussee 4e1fdca273 Merge PR #357: chore: move caveman.skill ZIP into dist/
# Conflicts:
#	CONTRIBUTING.md
2026-05-10 13:27:40 +02:00
Julius Brussee f62ac792ac Merge PR #361: docs: refresh CONTRIBUTING.md 2026-05-10 13:27:18 +02:00
Julius Brussee 37ffdd4253 Merge PR #354: chore: remove legacy installers and dead placeholder dirs 2026-05-10 13:27:18 +02:00
Julius Brussee abaad10665 Merge PR #358: docs: add dedicated INSTALL.md 2026-05-10 13:27:18 +02:00
Julius Brussee b3468f4333 Merge PR #356: docs: add human-facing README.md to each skill 2026-05-10 13:27:13 +02:00
Julius BrusseeandClaude Opus 4.7 1b2ee4128e docs: refresh CONTRIBUTING.md for new layout
Expand the 25-line stub to a scannable 190-line guide reflecting the
post-cleanup repo layout. Adds:

- Sources-of-truth table covering all editable skill, agent, hook, and
  installer files (skills/caveman-compress/, bin/install.js PROVIDERS,
  tools/caveman-init.js, mcp-servers/caveman-shrink/).
- CI-mirrors table calling out plugins/caveman/* and dist/caveman.skill
  as auto-rebuilt, edits-will-be-reverted.
- Step-by-step recipes for adding a new agent (PROVIDERS row +
  --list verification) and a new skill (frontmatter + sync workflow).
- Test commands (npm test, compress safety, init, symlink), benchmark
  and eval invocations, PR guidelines, and the load-bearing code-style
  invariants (silent-fail hooks, JSONC-tolerant settings.js,
  safeWriteFlag, CLAUDE_CONFIG_DIR).

Preserves caveman voice in the framing and Ideas section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:22:54 +02:00
Julius BrusseeandClaude Opus 4.7 eda0a1e7c6 docs: add INSTALL.md with per-agent install instructions
Splits install guidance out of README into a dedicated INSTALL.md so
users can find the install path in one glance. Covers the one-liner,
per-agent table for all 33 providers, manual install, verify, uninstall,
troubleshooting, and a privacy note. README and installer source are
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:20:46 +02:00
Julius BrusseeandClaude Opus 4.7 c28067e22d chore: move caveman.skill build artifact to dist/
The caveman.skill ZIP is regenerated by CI on every push and lived at
the repo root, where users mistook it for an editable source. Moving it
to dist/ makes its build-product nature obvious while keeping it
tracked so GitHub release links and npm packaging keep working.

Updates:
- git mv caveman.skill -> dist/caveman.skill
- .github/workflows/sync-skill.yml rebuild step now writes to dist/
- .gitignore ignores dist/* but allows dist/caveman.skill
- package.json files array includes dist/caveman.skill for npm pack
- CONTRIBUTING.md and tests/verify_repo.py path references updated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:20:35 +02:00
Julius BrusseeandClaude Opus 4.7 ed706fa098 docs: add per-skill READMEs for skills/
Add a human-facing README.md to each skill directory so users browsing
skills/ can quickly see what each skill does, how to invoke it, and an
example output. Mirrors the per-skill README pattern used by upstream
skill libraries (vercel-labs/skills, claude-code-sdk).

SKILL.md remains the LLM-facing system prompt; README.md is the human
front door.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:20:13 +02:00
Julius BrusseeandClaude Opus 4.7 509935697b chore: remove legacy installers, backup CLAUDE.md, empty placeholder dirs
CLAUDE.original.md was a backup snapshot from the initial caveman-compress
of the project's own CLAUDE.md. It is not referenced by any tooling — the
mentions in caveman-compress/README.md and tests/test_caveman_stats.js
describe the compress skill's output naming convention, not this file.

The other deletion targets from the cleanup brief (install.sh.legacy,
install.ps1.legacy, .junie/, .kiro/, .roo/, .agents/) either don't exist
in this branch or — in the case of .agents/plugins/marketplace.json — are
real manifests verified by tests/verify_repo.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:18:36 +02:00
github-actions[bot] ef6050c5e1 chore: sync SKILL.md copies and auto-activation rules [skip ci] 2026-05-01 00:27:54 +00:00
Julius BrusseeandClaude Opus 4.7 e031c1e440 fix(installer): re-enable --with-mcp-shrink default ON, npm probe stays
caveman-shrink@0.1.0 is now live on npm (pre-1.0). Restore the original
default-on behavior for --with-mcp-shrink / -WithMcpShrink. Keep the
`npm view caveman-shrink` probe — a transient registry outage now degrades
to a clean manual-config skip instead of registering a `npx -y caveman-shrink`
entry that would have spawned-failed.

Also: sync `model: haiku` onto cavecrew investigator/reviewer canonicals
so the top-level agents/ matches the synced plugins/caveman/agents/.

README + CLAUDE.md flipped back to "On by default."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:27:38 +02:00
Julius BrusseeandClaude Opus 4.7 83ec61c509 fix(release): production-ready installer + cavecrew refinements
install.sh:
- detect_match: replace `awk -v RS='||'` (rejected by macOS BSD awk:
  "illegal primary in regular expression") with bash parameter expansion.
  Compound detection specs were silently failing, so cursor / windsurf /
  continue / and 28 other compound-spec providers were never detected.
- --with-mcp-shrink: flip default OFF + probe `npm view caveman-shrink`
  before registering. Was registering a config that 404s on first spawn.

install.ps1: mirror the MCP-shrink default flip + npm probe.

tests: update statusline tests for default-on suffix behavior. Add a
       regression for fresh installs where the suffix file is absent.
       Add npm-pkg-fix formatting to package.json.

cavecrew: promote agents/cavecrew-*.md to top-level canonical, refine
          subagent contracts (sharper output formats, terminal refusal
          lines, model: haiku for read-only roles). CI workflow syncs
          agents/ + skills/cavecrew/ into plugins/caveman/.

docs: README & CLAUDE.md align with new opt-in MCP-shrink policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:25:28 +02:00
Julius Brussee ec7ce3614b Better install docs+scripts 2026-05-01 02:14:49 +02:00
Julius Brussee 47cd8d72de merge: stats receipts, smart installer, cavecrew, cavepack, MCP-shrink 2026-05-01 01:36:32 +02:00
Julius BrusseeandClaude Opus 4.7 56875e883f feat: stats receipts, smart installer, cavecrew, cavepack, MCP-shrink
- caveman-stats: dollar math via per-million model pricing, --share
  tweetable line, lifetime log via symlink-safe appendFlag, --all and
  --since N[d|h] aggregation, opt-in statusline savings suffix, and
  detection of *.original.md compress backups for input-side savings.
- install.sh / install.ps1 at repo root: smart multi-agent installer
  that detects Claude Code, Gemini, Codex, Cursor, Windsurf, Cline,
  Copilot and runs each one's native install. Idempotent, --dry-run,
  --only, --force.
- cavecrew: skills/cavecrew + three Claude Code subagents
  (investigator / builder / reviewer) for caveman-style machine-to-
  machine handoffs.
- cavepack: tools/caveman-init.js drops the always-on caveman rule
  into Cursor / Windsurf / Cline / Copilot / AGENTS.md in one shot,
  idempotent, with a sentinel check so re-runs never duplicate.
- caveman-shrink: MCP middleware proxy + pure-Node prose compressor
  that strips articles/filler/hedging from tools/list descriptions
  while preserving code, URLs, paths, and identifiers byte-for-byte.

59 tests passing (27 stats, 8 init, 12 mcp-shrink, 12 symlink-flag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:36:18 +02:00
Julius Brussee de331644ef Merge branch 'main' of https://github.com/JuliusBrussee/caveman 2026-05-01 01:18:14 +02:00
Julius Brussee b685570740 Update .gitignore 2026-05-01 01:18:06 +02:00
github-actions[bot] 13f0d4c49a chore: sync SKILL.md copies and auto-activation rules [skip ci] 2026-04-30 23:15:34 +00:00
31fa95478d feat: /caveman-stats — real session token usage + savings estimate
New slash command that reads the active Claude Code session JSONL
(.claude/projects/**/*.jsonl), sums output_tokens and
cache_read_input_tokens from assistant turns, and shows an estimated
savings figure when the active mode is 'full'.

Real numbers, not the model's guess:

  Caveman Stats
  ──────────────────────────────────
  Session:  ...projects/my-app/abc123.jsonl
  Turns:    47
  ──────────────────────────────────
  Output tokens:         3,210
  Cache-read tokens:     128,400
  ──────────────────────────────────
  Est. without caveman:  9,171
  Est. tokens saved:     5,961 (~65%)
  Savings est. from benchmarks/ (mean per-task). Actual varies by task.

Implementation:
* hooks/caveman-stats.js — script. Run directly with
  `node hooks/caveman-stats.js`, or via `--session-file <path>`.
  Falls back to most-recent JSONL under .claude/projects/ when no
  session file is passed.
* hooks/caveman-mode-tracker.js — `/caveman-stats` triggers an
  execFileSync call to caveman-stats.js with the hook's transcript_path,
  and the output is returned via `decision: "block"` so the user sees
  the stats inline without a model round-trip.
* install.sh / install.ps1 / uninstall.{sh,ps1} include
  caveman-stats.js in HOOK_FILES.
* skills/caveman-stats/SKILL.md (+ plugin mirror) for skill listing.
* README install matrix and Caveman Skills section updated.

Compression ratio (0.65) is the mean per-task figure from
benchmarks/results/*.json (avg_savings: 65 across 10 tasks). Only 'full'
mode has measured data — lite/ultra/wenyan show no estimate.

Tests: 6 passing in tests/test_caveman_stats.js covering direct
invocation, full-mode estimate math, non-full skip, no-session error,
mode-tracker block behavior, and flag preservation.

Closes #305 (re-implementation; takes the design from
@DeeptimaanB but rewritten against current main).

Co-Authored-By: Deeptimaan Banerjee <DeeptimaanB@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:14:53 +02:00
a9dc067796 docs(readme): clarify Codex install + add Windows manual fallback
* Codex install line now says "Open Codex in repo" before /plugins,
  removing first-time-user ambiguity (#226 by @cirops).
* Windows manual fallback section: PowerShell block to copy SKILL.md
  into the plugin path and patch marketplace.json by hand when
  automated install fails (#258 by @Abbasam8910). Plugin-skill only;
  doesn't install standalone hooks/statusline.

Skipped from this batch:
* #290 (75% → 50% headline numbers) — leaving the headline figure as is.
* #295 (Pages link) — not advertising the Pages site in the README.

Co-Authored-By: Ciro Plá <cirops@users.noreply.github.com>
Co-Authored-By: ABBAS A M <Abbasam8910@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:10:47 +02:00
31d804e5f2 docs(skill): ultra-mode code-symbol guard, expand auto-clarity, Typst+LaTeX
Three small SKILL.md edits batched:

* Ultra mode: spell out that abbreviation applies to prose only, never to
  code symbols, function names, API names, or error strings (#238 by
  @AnthoHansen). Stops models from rendering literal `fn` in code where
  the user wrote `function`.
* Auto-clarity (#239 by @AnthoHansen): bullet list replaces run-on
  sentence; adds explicit trigger for compression-induced ambiguity in
  technical sequences (e.g. "migrate table drop column backup first").
* Typst + LaTeX (#243 by @wildwestrom): add .typ, .typst, .tex to
  COMPRESSIBLE_EXTENSIONS in caveman-compress/scripts/detect.py and to
  the boundaries lists in SKILL.md / README.md.

Synced SKILL.md to caveman/, plugins/caveman/skills/caveman/,
.cursor/skills/caveman/, .windsurf/skills/caveman/, and rebuilt
caveman.skill ZIP.

Co-Authored-By: Anthony Domínguez <AnthoHansen@users.noreply.github.com>
Co-Authored-By: West <wildwestrom@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:08:03 +02:00
a660db02c9 fix(hooks): /caveman arg whitelist + symlinked-parent ~/.claude support
Two converging hook fixes:

* /caveman arg parser uses VALID_MODES whitelist (#314 by @jgttech).
  Drops the silent fallback to getDefaultMode() on unknown args. Three
  bugs collapsed: /caveman full was impossible if defaultMode was set
  to anything else, /caveman off had no effect, and typos like
  /caveman fulll silently clobbered state. Now: bare /caveman activates
  default; /caveman off|stop|disable removes flag; unknown args leave
  flag untouched (no silent overwrite); independent modes (commit,
  review, compress) cannot be selected as args.

* safeWriteFlag through symlinked ~/.claude (#224 by @voidborne-d,
  closes #207). Earlier hardening (5ad8f6d) refused every symlinked
  parent including legitimate ~/.claude → /opt/shared-claude or
  /mnt/d/claude-config patterns. Now resolves the parent via
  realpathSync, verifies ownership on Unix (uid match) or under-home
  on Windows, and uses the resolved path for the atomic write. Flag
  file itself still must not be a symlink — that's the actual clobber
  vector. CAVEMAN_DEBUG=1 emits stderr diagnostics on refusal.

  12 regression tests in tests/test_symlink_flag.js — all pass.

Verified:
  /caveman / lite / full / ultra / wenyan{,-lite,-full,-ultra} → mode set
  /caveman off / stop / disable                                → flag deleted
  /caveman commit                                              → no flag (rejected)
  /caveman fulll (typo)                                        → no flag (rejected)
  symlink ~/.claude → owned dir                                → write succeeds in real dir
  symlink ~/.claude → other-user dir                           → refused

Co-Authored-By: Jonathon Tech <jgttech@users.noreply.github.com>
Co-Authored-By: voidborne-d <voidborne-d@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:06:43 +02:00
b24b915e0a fix(windows): write PowerShell node script to temp file (#250)
PowerShell 5.1 mangles the multiline $nodeScript when passed to
node -e — the embedded `"` after `node ` terminates the argument
early, causing SyntaxError mid-install. Hook files copy but
settings.json never updates, leaving caveman non-functional.

Write the script to %TEMP%\caveman-install-<pid>.js, run it, clean
up in a finally block. Verified clean install on Windows 11 /
PowerShell 5.1 / Node 24.

Closes #249.

Co-Authored-By: Scott Converse <scottconverse@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:04:40 +02:00
57a9b489a1 fix(compress): UTF-8 stdout, empty/identical guards, inline-code validation, frontmatter cleanup
Five user-contributed fixes consolidated:

* UTF-8 stdout (#289 by @ousamabenyounes) — reconfigure stdout/stderr at the
  top of cli.py so Windows cp1252 consoles don't crash on the  glyph in
  error/validation branches and mask the real error.
* Empty / unchanged compression guards (#292 by @ousamabenyounes, closes #237)
  — refuse empty input, refuse empty/whitespace/identical Claude output,
  read back the backup before touching the input. Five regression tests in
  tests/test_compress_safety.py.
* Inline backtick validation (#309 by @hireblackout) — validate_inline_codes
  closes the silent-overwrite gap where `npm install` → `yarn install`
  passed validation. Wired into validate(); 11 unit tests in
  tests/test_validate_inline.py.
* Frontmatter angle-bracket fix (#268 by @Bortlesboat, closes #266) —
  caveman-compress/SKILL.md description now uses FILEPATH instead of
  <filepath>, plus verify_repo gains a new
  verify_skill_frontmatter_upload_compatibility check, UTF-8 hardening for
  Windows, and the activation-banner regex no longer requires a trailing
  period.
* Two test fixtures (claude-md-project.md, mixed-with-code.md) updated so
  the new inline-backtick validator passes — they were silently dropping
  `server/src/`, `type(scope): description`, and `status` references in
  compression. The fixture is documentation of "good" compression, so the
  fix is to preserve those references.

Co-Authored-By: Ben Younes <ousama.benyounes@oratelecom.net>
Co-Authored-By: hireblackout <hireblackout@users.noreply.github.com>
Co-Authored-By: Andrew Barnes <Bortlesboat@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 01:03:59 +02:00
84cc3c14fa docs: cross-link caveman / cavemem / cavekit ecosystem (#223)
Add consistent ecosystem banner near the top and a unified
"Caveman Ecosystem" section near the bottom so visitors of any repo
can discover and reach the others.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:11:38 +02:00
github-actions[bot] c2ed24b3e5 chore: sync SKILL.md copies and auto-activation rules [skip ci] 2026-04-15 12:50:24 +00:00
Julius BrusseeandClaude Opus 4.6 5ad8f6d684 fix(security): harden flag-file reads and refuse sensitive-file compression
Writes were hardened via safeWriteFlag (PRs #70/#71) but readers still
trusted whatever the flag contained. A local attacker with write access
to ~/.claude/ could symlink the flag at a secret file and have the
per-turn reinforcement inject its bytes into model context, or the
statuslines echo ANSI escapes to the terminal on every keystroke.

- caveman-config.js: new readFlag() — lstat symlink refuse, 64-byte cap,
  O_NOFOLLOW, VALID_MODES whitelist. Returns null on any anomaly.
- caveman-mode-tracker.js: per-turn reinforcement routes through
  readFlag() instead of fs.readFileSync.
- caveman-statusline.sh / .ps1: symlink + size refuse, strip to
  [a-z0-9-], whitelist-validate before rendering.
- compress.py (3 synced copies): is_sensitive_path() denylist refuses
  .env*, .netrc, keys/certs, ~/.ssh|.aws|.gnupg|.kube|.docker, and any
  basename containing secret/credential/password/apikey/token/privatekey
  (separator-insensitive). Fails loudly before read — no silent exfil
  of credentials to the Anthropic API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:49:26 +02:00
Julius Brussee 4c82699c26 docs(CLAUDE.md): document hook system updates from merged PRs
- safeWriteFlag helper in caveman-config.js
- CLAUDE_CONFIG_DIR env var support
- hooks/package.json CommonJS marker
- natural-language activation in mode tracker
- per-turn reinforcement output
- key rules: route writes through safeWriteFlag, respect CLAUDE_CONFIG_DIR
2026-04-15 14:44:38 +02:00
Julius Brussee d8d53115d0 fix(hooks): safeWriteFlag check immediate parent not full chain
Parent-chain walk produced false positives: macOS /tmp -> /private/tmp
and symlinked home dirs broke all flag writes. Check only the immediate
parent directory and the target itself — that covers the threat model
(attacker replacing the flag with a symlink) without breaking legit
paths.
2026-04-15 14:44:38 +02:00
Julius Brusseeandtuanaiseo 205e537103 Merge PR #71: strengthen safeWriteFlag with parent-chain check
Extend safeWriteFlag() in caveman-config.js with:
- hasSymlinkInPath() walks every ancestor component and refuses
  if any is a symlink (protects against parent-dir redirect)
- Atomic write via temp file + rename
- O_EXCL on temp open to prevent race

Removes duplicate local helper from mode-tracker.

Co-Authored-By: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
2026-04-15 14:44:38 +02:00
Julius Brusseeandtuanaiseo adafaba5cd Merge PR #70: symlink-safe flag file writes
Consolidate write protection into safeWriteFlag() helper in
caveman-config.js. Applied to all flag write sites:
- caveman-activate.js (SessionStart)
- caveman-mode-tracker.js slash command
- caveman-mode-tracker.js natural-language activation

Refuses symlink targets via lstat, opens with O_NOFOLLOW where
supported, writes with 0600.

Co-Authored-By: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
2026-04-15 14:44:34 +02:00
Julius Brussee e50325b040 Merge PR #119: per-turn reinforcement in UserPromptSubmit 2026-04-15 14:44:20 +02:00
Julius Brussee 23ce800ad6 Merge PR #120: natural language activation/deactivation 2026-04-15 14:44:20 +02:00
Julius Brussee 4fe5a5d70a Merge PR #169: fix README links 2026-04-15 14:27:57 +02:00
Julius Brussee 507ec0af22 Merge PR #153: replace broken symlinks with real files 2026-04-15 14:27:50 +02:00
Julius Brussee 4345efd45a Merge PR #146: respect CLAUDE_CONFIG_DIR 2026-04-15 14:27:23 +02:00
Julius Brussee 7e89c6a3cd Merge PR #148: fix codex hook config shape 2026-04-15 14:27:13 +02:00
Julius Brussee b1ca6818f9 Merge PR #171: quote CLAUDE_PLUGIN_ROOT for paths with spaces 2026-04-15 14:27:07 +02:00
Julius Brussee 5af591a028 Merge PR #174: isolate hooks as CommonJS 2026-04-15 14:27:02 +02:00
Dmitrii Malakhov b50aa6d4b3 fix: isolate hooks as CommonJS so they survive ESM parent package.json
When ~/.claude/package.json (or any ancestor) contains "type": "module",
Node treats every .js file under that tree as an ES module. The caveman
hooks use require() and crash with:

  ReferenceError: require is not defined in ES module scope

surfaced as:

  SessionStart:clear hook error / UserPromptSubmit hook error
  Failed with non-blocking status code: .../caveman-activate.js:9

This pins the hooks directory to CommonJS via a local package.json, so
module resolution no longer depends on whatever the user's ~/.claude
directory declares. Also wires the new file into install/uninstall
scripts so standalone installs (curl | bash / Invoke-WebRequest)
copy it into ~/.claude/hooks/ alongside the JS files.

Addresses the ESM sub-case flagged in #167 (comment by mrx-arafat).
Does not fix the Windows path-with-spaces expansion issues in #167/#78/#72
which have a separate root cause in plugin.json ${CLAUDE_PLUGIN_ROOT}
quoting.
2026-04-15 10:49:52 +02:00
mukunda katta fea439dc14 Fix: quote ${CLAUDE_PLUGIN_ROOT} so hooks work on paths with spaces
Both SessionStart and UserPromptSubmit hook commands passed
${CLAUDE_PLUGIN_ROOT} unquoted, so the shell split the expansion
on whitespace whenever a user's plugin root contained a space
(e.g. /Users/Tyler Laprade/...). Node received a truncated path
and errored with "Cannot find module '/Users/Tyler'", preventing
the plugin from loading.

Wrapping the variable in double quotes keeps the path intact on
any POSIX-style shell and has no effect on paths without spaces.

Fixes #157
2026-04-14 23:04:28 -07:00
nervana21 d63b47673c docs: Fix skill anchor links in README
Fragment targets #caveman-commit / #caveman-review were missing.
Replace skills table with ### subsections, triggers inline on each
blurb; add /caveman:compress trigger on compress lead-in.
2026-04-14 21:21:21 -04:00
Arkadiusz Gładki 3897f40b97 docs: clarify source of truth for compress skill 2026-04-14 13:15:35 +02:00
Arkadiusz Gładki 52fec218a5 fix(compress): replace broken symlinks with real files
Gemini CLI fails to load skills through symlinks. Replace symlinks
in skills/compress/ with actual file copies and update CI to keep
them in sync. Remove redundant root compress/ directory.
2026-04-14 13:08:42 +02:00
Arkadiusz Gładki 1e0fb7f3dd fix(compress): replace broken symlinks with real files
Gemini CLI fails to load skills through symlinks. Replace symlinks
in skills/compress/ with actual file copies and update CI to keep
them in sync. Remove redundant root compress/ directory.
2026-04-14 12:59:00 +02:00
David Young e192972632 fix(codex): update hook config shape
Enable repo-local Codex hooks and use the current nested SessionStart
matcher format. Document macOS/Linux auto-start, the Windows hook limit,
and the feature flag needed when copying the hook to other repos.
2026-04-14 09:18:29 +02:00
Brendan Izu 7b4649fad0 fix: respect CLAUDE_CONFIG_DIR env var across all hooks
All hook files hardcoded ~/.claude as the Claude config directory.
Users who set CLAUDE_CONFIG_DIR (e.g. for XDG compliance) had hooks
writing to the wrong location. Now all hooks check CLAUDE_CONFIG_DIR
first, falling back to ~/.claude.

Closes #140
2026-04-13 21:21:48 -07:00
Julius BrusseeandGitHub 63e797cd75 Update GitHub Sponsors username in FUNDING.yml 2026-04-12 19:59:23 +02:00
Matthias HumtandClaude Opus 4.6 f7ef823b91 fix: guard natural language activation against off default mode
Mirror the /caveman command path: skip writing flag file when
getDefaultMode() returns 'off'. Without this, "talk like caveman"
with CAVEMAN_DEFAULT_MODE=off would write 'off' to the flag file,
making statusline/reinforcement inconsistent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:19:09 +02:00
Matthias HumtandClaude Opus 4.6 4c6dad829a fix: skip reinforcement for independent modes (commit/review/compress)
Independent modes have their own skill behavior — emitting base caveman
rules ("Drop articles, fragments OK") on every turn conflicts with the
specialized format those modes expect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:18:45 +02:00
Matthias HumtandClaude Opus 4.6 56f4047ab3 fix: detect natural language activation/deactivation in mode tracker
README tells users they can say "talk like caveman" to activate, but the
UserPromptSubmit hook only matched /caveman commands. This meant the flag
file and statusline badge stayed out of sync when users activated via
natural language — the model would speak caveman (it reads the prompt)
but the hook never wrote the flag file.

Now matches: "activate caveman", "turn on caveman mode", "talk like
caveman", "disable caveman", "turn off caveman", etc. Uses a negative
guard so "stop caveman" doesn't trigger activation first.

Uses getDefaultMode() for natural language activation to respect
CAVEMAN_DEFAULT_MODE and config.json, same as /caveman command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:12:11 +02:00
Matthias HumtandClaude Opus 4.6 3ed7b732b0 fix: add per-turn reinforcement to UserPromptSubmit hook
The SessionStart hook injects caveman rules once, but models lose them
when other plugins (output styles, learning modes) inject competing
style instructions on every turn. Recency and repetition win in LLM
attention — a one-shot injection loses to per-turn reinforcement.

Emit a short structured reminder via hookSpecificOutput on every user
message when caveman is active. Uses the same JSON format Claude Code
expects from hooks, keeping the full ruleset from SessionStart in
context while this reminder keeps it top-of-mind.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:11:15 +02:00
Julius Brussee 600e8efcd6 Load SKILL.md; handle independent modes
Replace hardcoded intensity table and examples with runtime loading of skills/caveman/SKILL.md (single source of truth). Strip YAML frontmatter and filter the intensity table and examples to include only the active mode; fall back to a minimal built-in ruleset when SKILL.md is missing (standalone installs). Add INDEPENDENT_MODES (commit, review, compress) to short-circuit and emit a brief activation line for modes that use their own skill files. Normalize the wenyan alias to wenyan-full. Overall simplifies maintenance and prevents duplicated, stale rule text in the hook.
2026-04-12 00:17:07 +02:00
Julius Brussee 80a317e472 Expand caveman ruleset and add intensity levels
Replace the terse one-line caveman activation message with a full, structured ruleset and examples. Introduces INTENSITY and EXAMPLES maps (lite/full/ultra + wenyan variants), selects the active level from mode with a fallback, and builds a detailed output covering persistence, rules, examples, auto-clarity and boundaries. Rationale: anchor terse response behavior more reliably to prevent drift and provide selectable verbosity/compression profiles while preserving exact code/commit formatting.
2026-04-12 00:11:14 +02:00
Jayesh PatelandGitHub 6a6c144f28 docs: consolidate Cursor/Windsurf/Cline/Copilot install details into one block (#106)
Consolidate four near-identical Cursor/Windsurf/Cline/Copilot detail blocks into one table. -53/+9 lines.
2026-04-12 00:02:30 +02:00
Jimmy CrakCrnandGitHub 8be1bdd4c7 fix(docs): use ANSI-C quoting for bash statusline badge example (#57)
Fix ANSI-C quoting in hooks/README.md statusline example — escape sequences need $'...' syntax to render in bash.
2026-04-12 00:02:27 +02:00
Julius BrusseeandClaude Opus 4.6 8bab5c739e fix: prevent /caveman from writing "off" to flag file
Mode tracker wrote getDefaultMode() result to flag file even when it
returned "off". Now guards against it — same pattern as caveman-activate.
Added test coverage for off mode in both activate and tracker paths.

Also: fix swapped step comments in uninstall.sh, update stale CLAUDE.md
description for /caveman default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:27:41 +02:00
Julius BrusseeandClaude Opus 4.6 c80f8d7fe1 feat: add "off" default mode to disable auto-activation
Set CAVEMAN_DEFAULT_MODE=off or {"defaultMode":"off"} in config to
skip session-start activation. No flag file written, no rules injected.
User can still manually activate with /caveman.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:23:42 +02:00
Julius BrusseeandClaude Opus 4.6 12e78c5201 feat: add caveman-help skill for quick-reference card
/caveman-help displays all modes, skills, triggers, config options,
and deactivation — one-shot display, no mode change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:03:23 +02:00
Julius BrusseeandClaude Opus 4.6 ea89000141 feat: make default caveman mode configurable
Add caveman-config.js shared config resolver. Default mode resolves via:
CAVEMAN_DEFAULT_MODE env var > XDG config file > 'full'.

Updated all install/uninstall scripts (bash + PowerShell) and tests.

Closes #86

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:56:45 +02:00
Julius BrusseeandClaude Opus 4.6 24e6ee9ea8 fix: replace compress skill symlinks with real files for Windows/Codex compatibility
Symlinks in plugins/caveman/skills/compress/ were checked out as plain
text path stubs on Windows (core.symlinks=false), causing Codex to reject
SKILL.md as missing YAML frontmatter. Replace with verbatim copies of
source files and add CI sync step to keep them in sync.

Closes #92

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:31:01 +02:00
Julius BrusseeandGitHub 0bbd46c390 Remove local verification section from README
Removed local verification instructions and related text.
2026-04-11 12:33:04 +02:00
github-actions[bot] a311eab867 chore: sync SKILL.md copies and auto-activation rules [skip ci] 2026-04-11 10:30:01 +00:00
Julius Brussee bce42f7294 docs(readme): fix install behavior claims 2026-04-11 12:27:39 +02:00
Julius Brussee 8783f63296 Require statusLine and fix PS5.1 parsing
Make the installer require a configured statusLine before treating hooks as already installed and ensure PowerShell 5.1 compatibility. hooks/install.ps1: stop using -AsHashtable, adjust hooks access, track presence of settings.statusLine and only skip installation when SessionStart, UserPromptSubmit and statusLine are present. hooks/install.sh: add HAS_STATUSLINE check and update the Node-based settings validation to require settings.statusLine. tests/test_hooks.py: add test_install_reconfigures_missing_statusline to verify installer adds a missing statusLine. tests/verify_repo.py: add a manifest path to the verification list and assert that -AsHashtable is not used in install.ps1 to preserve compatibility with Windows PowerShell 5.1.
2026-04-11 12:22:07 +02:00
Julius Brussee 7e2c94c541 Add Windows statusline & robust hook install
Add a PowerShell statusline script (hooks/caveman-statusline.ps1) and update activation/installer logic to support Windows and avoid clobbering custom statuslines. install.sh and install.ps1 now check for a full set of hook files and wired hook entries before short-circuiting, wire a managed statusline path, and prefer not to overwrite user statusLine settings. Uninstall scripts now remove the managed statusline only when it was installed. caveman-activate.js updated to detect existing statusLine entries and emit a platform-appropriate snippet. Add tests (tests/test_hooks.py) and a local verification runner (tests/verify_repo.py) to validate install/uninstall/activation flows and caveman-compress fixtures. Also update README and caveman-compress docs and stats (token savings ~45%→~46%) and clarify auto-activation and statusline behavior.
2026-04-11 12:14:46 +02:00
Julius Brussee 8fb2f013a9 Improve hooks safety, validation, and docs
Make several robustness and clarity fixes across scripts and docs:

- README: Clarify agent-specific behavior (Codex, Cursor/Windsurf) and note Codex uses $caveman and lacks some bundled plugins.
- caveman-compress: Avoid division-by-zero when computing token savings; skip unclosed markdown fences when extracting code blocks to prevent false positives.
- hooks (install/uninstall, PowerShell and shell): Pass settings and hooks paths via environment variables to avoid injection issues with special characters, quote generated command paths, use safer here-strings in PowerShell, and ensure settings.json is written via the resolved path. Also remove installer backup files during uninstall and add explanatory comments.

These changes improve security, correctness, and developer-facing documentation.
2026-04-11 11:59:05 +02:00
Julius Brussee e8015c384c Add CLAUDE.md (caveman README) and backup
Add a new caveman-styled README (CLAUDE.md) documenting project overview, file ownership, CI sync workflow, hook system, skill system, agent distribution, evals, and benchmarks. Also add CLAUDE.original.md as the original, more formal copy for reference/backup.
2026-04-11 11:56:02 +02:00
Julius Brussee b6a7a3f584 Add auto-activation rules and sync workflow
Enable automatic caveman mode activation across agents and keep skill copies in sync. Adds activation rule files (.clinerules, .codex/hooks.json, .cursor/rules/caveman.mdc, .windsurf/rules/caveman.md, .github/copilot-instructions.md, rules/caveman-activate.md), new command prompts (commands/caveman{,-commit,-review}.toml), and updates SKILL.md copies (skills/, caveman/, plugins/, .cursor/, .windsurf/) to add persistent behavior and tweak auto-clarity wording. Updates README with agent-specific auto-activation docs and expands compatibility table. Updates GitHub Actions workflow to sync SKILL.md, copy activation rules, rebuild caveman.skill, and commit any changes. Includes updated caveman.skill binary.
2026-04-11 11:48:34 +02:00
Julius Brussee 79fa46a4a0 Add windsurf skill, hooks, and install docs
Add .windsurf/skills/caveman SKILL.md and update the sync workflow to copy and commit the new file. Expand README install docs with agent-specific install instructions and standalone hook usage; update AGENTS.md and GEMINI.md to use the new caveman-compress path. Add Windows PowerShell installers (hooks/install.ps1 and hooks/uninstall.ps1) and enhance shell installers (hooks/install.sh, hooks/uninstall.sh) to support --force, plugin detection, clearer statusline wiring messages, and idempotent JSON merging/removal of hook entries. Provides uninstall guidance for other agents and improves overall install/uninstall robustness.
2026-04-11 11:30:34 +02:00
Julius Brussee cfd76590bd Add statusline badge, hooks, and compress fixes
Add statusline support and installer/uninstaller hooks, improve compression tooling and docs. Key changes:

- Add caveman statusline badge script (hooks/caveman-statusline.sh) and wire it into install/uninstall (hooks/install.sh, hooks/uninstall.sh); installer now requires node, backs up settings.json, and makes the statusline executable.
- Enhance SessionStart hook (hooks/caveman-activate.js) to detect missing statusline config and emit a setup nudge; update hooks README with statusline usage and setup instructions.
- Add gemini-extension.json, AGENTS.md, and GEMINI.md to register skills and support Gemini CLI installation.
- Update README to mention statusline badge, Gemini CLI install, and agent list.
- Improve caveman-compress: rename skill (caveman-compress), clarify CLI path/usage in SKILL.md, return on validation failure without overwriting, bump benchmark encoding, and make compressor robust to LLM outer fences by stripping wrapping fences and expanding max_tokens; add explicit rule to avoid wrapping entire output in fences.
- Strengthen code-block extraction in validate.py to handle variable-length fences (```/~~~), nested fences, and CommonMark rules.
- Minor SKILL.md tweak: resume caveman mode when user asks to clarify or repeats question.

These changes integrate a visible status badge for Claude Code users, improve install/uninstall reliability, extend Gemini support, and make the compressor/validator more robust when interacting with LLM outputs.
2026-04-11 11:15:03 +02:00
tuanaiseo 5580f8e28f fix(security): mode tracker flag file write also vulnerable to sy
The UserPromptSubmit hook similarly writes to `~/.claude/.caveman-active` without checking whether the path is a symlink. This duplicates the same local file-clobber risk in another execution path.

Affected files: caveman-mode-tracker.js

Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
2026-04-10 20:34:08 +07:00
tuanaiseo 7906016383 fix(security): hook writes to predictable path without symlink pr
The SessionStart hook writes to `~/.claude/.caveman-active` using `fs.writeFileSync` on a predictable path. If that path is replaced with a symlink, Node will follow it and overwrite the symlink target. A local attacker (or another process running as the same user) could abuse this to modify unintended files writable by the user.

Affected files: caveman-activate.js, caveman-mode-tracker.js

Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
2026-04-10 20:33:24 +07:00
tuanaiseo 9449c8a05a fix(security): hook writes to predictable path without symlink pr
The SessionStart hook writes to `~/.claude/.caveman-active` using `fs.writeFileSync` on a predictable path. If that path is replaced with a symlink, Node will follow it and overwrite the symlink target. A local attacker (or another process running as the same user) could abuse this to modify unintended files writable by the user.

Affected files: caveman-activate.js, caveman-mode-tracker.js

Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
2026-04-10 20:33:23 +07:00
Julius Brussee 92f892f2b9 Update README.md 2026-04-09 21:25:34 +02:00
Julius Brussee bc351bb7c5 Merge branch 'main' of https://github.com/JuliusBrussee/caveman 2026-04-09 21:23:57 +02:00
Julius Brussee 558576cdcd Update README.md 2026-04-09 21:23:55 +02:00
Julius Brussee 1be0340955 Add plugin hooks and update docs
Register SessionStart and UserPromptSubmit hooks in .claude-plugin/plugin.json to auto-load caveman rules and track active mode (writes to ~/.claude/.caveman-active). Update README and hooks/README.md to reflect automatic activation when installed as a plugin, remove the one-line install snippet, clarify the optional statusline badge, and adjust uninstall instructions (replace PostToolUse with UserPromptSubmit and note disabling the plugin deactivates hooks).
2026-04-09 21:22:10 +02:00
Julius BrusseeandGitHub 782d17c24a Replace Blueprint with Cavekit in README
Updated project reference from Blueprint to Cavekit.
2026-04-09 21:16:06 +02:00
github-actions[bot] 18427c26ab chore: sync SKILL.md copies and caveman.skill [skip ci] 2026-04-09 19:14:52 +00:00
Julius Brussee 602d1639c6 Use UserPromptSubmit for caveman mode tracking
Switch caveman mode tracking from PostToolUse to UserPromptSubmit: update README to document the new hook, adjust caveman-mode-tracker.js to read the incoming `prompt` field (removing `user_prompt`), and modify install.sh to register the UserPromptSubmit hook with timeouts and status messages (and add a timeout/status for the activate hook). This ensures mode detection runs on every user prompt and prevents hook stalls by setting command timeouts and status text. Files changed: hooks/README.md, hooks/caveman-mode-tracker.js, hooks/install.sh.
2026-04-09 21:14:43 +02:00
Julius Brussee a9e62b48c3 Add mode-tracker hook and update install/docs
Add a new caveman-mode-tracker.js hook to track active /caveman modes and write them to ~/.claude/.caveman-active (supports full/lite/ultra/wenyan/commit/review/compress and detects deactivation). Update caveman-activate.js to write 'full' to the flag file on SessionStart. Enhance hooks/install.sh to install both hooks and idempotently wire SessionStart and UserPromptSubmit hooks into ~/.claude/settings.json. Revise hooks/README.md and top-level README.md to document the two hooks, improved statusline badge snippet (shows [CAVEMAN] or [CAVEMAN:MODE]), and how the flag file bridges hooks and the statusline. Minor SKILL.md whitespace/newline cleanups.
2026-04-09 21:09:56 +02:00
Julius Brussee 4e919547fb Add Claude SessionStart hook installer and docs
Replace the long README hook section with a concise "Auto-Load Hook (Claude Code only)" install hint and link to new docs. Add hooks/README.md documenting the optional SessionStart hook, statusline badge, and quick install. Add hooks/install.sh: a one-command installer that copies or downloads caveman-activate.js into ~/.claude/hooks, idempotently wires a SessionStart entry into ~/.claude/settings.json (using node), and prints usage/uninstall notes. Restart Claude Code to activate the hook.
2026-04-09 20:53:28 +02:00
Jimmy CrakCrn a62d6602c3 feat: optional SessionStart hook + visible statusline badge pattern
Adds hooks/caveman-activate.js — an optional Claude Code SessionStart
hook users can wire up via ~/.claude/settings.json to auto-load the
caveman ruleset at session start. The hook also writes a flag file
at ~/.claude/.caveman-active that a statusline script can read to
render a persistent [CAVEMAN] badge.

Why: SessionStart hook stdout is injected as hidden system-reminder
context — useful for Claude, invisible to users in the terminal. This
means users have no way to confirm the hook fired. The flag file is
a minimal bridge between 'the hook ran' and 'the statusline can
prove it', giving users persistent visual confirmation that caveman
mode is loaded.

This is a pure addition:

- New file: hooks/caveman-activate.js (~30 lines, no dependencies
  beyond Node.js stdlib)
- README section 'Optional: SessionStart Hook + Visible Mode Badge'
  documents both the hook install and the statusline snippet
- Marked Claude Code-only since it relies on Claude Code's hooks
  and statusline mechanisms — Cursor/Codex/other users can ignore
- No existing files changed except README

If you don't wire up the hook, nothing changes. Skill install works
exactly as before.
2026-04-09 07:37:18 -05:00
github-actions[bot] 26c25e39b3 chore: sync SKILL.md copies and caveman.skill [skip ci] 2026-04-08 23:23:26 +00:00
Julius Brussee 79ef2933ac Update README.md 2026-04-09 01:23:17 +02:00
Julius Brussee fecea29b1f Update README: restructure and add compress
Rework README layout and copy: reorder navbar links, rename sections (Levels, 文言文), simplify intensity triggers, and tighten many examples/phrasing. Add caveman-compress docs and compression benchmarks (file-savings table), move/reinsert API benchmark table, and replace long "Why/How" prose with concise benefits and visuals. Misc fixes: Wenyan wording, CLAUDE.md compress examples, and various formatting cleanup for clarity and token-efficiency.
2026-04-09 01:22:31 +02:00
Julius Brussee 6f6772ab1b README: add Wenyan mode, skills, and evals
Expand README with new features and docs: add TOC links for Skills and Evals, update intro to mention Wenyan mode/terse commits/reviews and compression tool, and consolidate Windows Codex install/symlink notes into a short note. Add a new Wenyan (文言文) section with levels and example, introduce Caveman Skills (caveman-commit and caveman-review) with usage examples and severity prefixes, and add an Evals section describing the eval harness and commands. Minor wording/ordering tweaks to installation text.
2026-04-09 01:18:09 +02:00
Julius BrusseeandClaude Opus 4.6 e6df0d6055 Remove language-specific skills (caveman-cn, caveman-es, caveman-pt)
Separate language translations are unnecessary — Claude already responds
in the user's language. These added maintenance surface without real value.
Wenyan mode is kept as it's a genuinely different compression technique
built into the core skill.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:10:22 +02:00
Julius Brussee 42b3030c69 Merge PR #47: Add eval harness with three-arm methodology 2026-04-09 01:08:14 +02:00
Julius Brussee bd671cc907 Merge PR #41: Add caveman-pt skill (Portuguese) 2026-04-09 01:07:57 +02:00
Julius Brussee 4591fb5396 Merge PR #45: Add caveman-review skill 2026-04-09 01:07:52 +02:00
Julius Brussee e0c7501619 Merge PR #44: Add caveman-commit skill 2026-04-09 01:07:51 +02:00
Julius Brussee a5b624ca4f Merge PR #36: Add 文言文 (Classical Chinese) wenyan mode 2026-04-09 01:07:44 +02:00
Julius Brussee 3e8b60e570 Merge PR #30: Codex plugin Windows install and assets 2026-04-09 01:07:39 +02:00
Julius Brussee 7d6aa3ac65 Merge PR #38: Security patch, SDK support, and file size limit 2026-04-09 01:07:16 +02:00
Julius Brussee e1e5e8b13a Merge PR #39: Fix path regex and prevent backup file overwriting 2026-04-09 01:06:43 +02:00
Julius BrusseeandClaude Opus 4.6 8195e1a4a0 fix: PATH_REGEX must require path separator to avoid false positives
Making the prefix optional matched every English word as a path.
Now requires either a path prefix (./ ../ / drive:) or contains / or \.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:05:31 +02:00
Julius BrusseeandClaude Opus 4.6 a97af68741 fix: correct inverted sign in fmt_pct eval output
Positive savings (skill reduces tokens) should display as "+" not "−".
The sign logic was backwards, making all eval results misleading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:05:25 +02:00
Julius BrusseeandClaude Opus 4.6 f6ca007f49 Remove generated eval snapshot files from git tracking
These are build artifacts (HTML report and PNG plot) that should not be
version-controlled. Added evals/snapshots/*.html and *.png patterns to
.gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:00:06 +02:00
Julius BrusseeandClaude Opus 4.6 bf828cd0fa Remove duplicate caveman-pt/SKILL.md from root level
The Portuguese caveman skill was duplicated at both caveman-pt/SKILL.md (root)
and skills/caveman-pt/SKILL.md. Remove the root-level copy since skills belong
under the skills/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 00:59:52 +02:00
sebastianbreguel 64c4e2113f Switch eval plot to real boxplot with per-prompt distribution 2026-04-08 18:44:27 -04:00
sebastianbreguel 6588ad1e26 Flip eval plot to vertical orientation 2026-04-08 18:36:03 -04:00
sebastianbreguel 784df686b4 Switch eval plot to dot+whiskers (median, IQR, min/max) 2026-04-08 18:32:06 -04:00
sebastianbreguel 7de87db9e6 Reframe eval plot as 'tokens saved' stacked bars 2026-04-08 18:28:58 -04:00
sebastianbreguel 1254e714e6 Switch eval plot to plotly, more intuitive horizontal layout 2026-04-08 18:26:08 -04:00
sebastianbreguel 6391a702b7 Add bar chart of skill compression with median/mean/range 2026-04-08 18:24:28 -04:00
sebastianbreguel 2b10489888 Add token-compression eval harness with three-arm methodology
Closes #18 by replacing the unverified "~75% savings" claim with a real,
auditable measurement.

Methodology:
- Three arms per prompt: baseline (no system prompt), terse control
  ("Answer concisely."), and terse+SKILL.md. The honest delta is
  skill vs terse — this isolates the skill's contribution from the
  generic "be terse" effect.
- Real LLM in the loop via `claude -p --system-prompt`. No hand-written
  baselines, no circularity.
- Snapshot of LLM outputs committed to git as the source of truth.
  measure.py runs in CI with no network and no auth.
- Reports median, mean, min, max, stdev across prompts so noise is visible.
- Metadata pinned in the snapshot: model, CLI version, generation timestamp.

Initial run on claude-opus-4-6 (n=10 prompts) shows the real numbers
sit in the −22% to −49% mean range, not the previously claimed ~75%.
The README's headline number should be updated to match.
2026-04-08 18:20:09 -04:00
sebastianbreguel 001075b420 feat: add caveman-review skill for terse PR review comments 2026-04-08 16:19:31 -04:00
sebastianbreguel 03f3dcd723 feat: add caveman-commit skill for terse commit messages 2026-04-08 16:14:54 -04:00
Leonardo Colman LopesandClaude Opus 4.6 f3c710377c Add caveman-pt skill (Portuguese)
Add Portuguese localization of the caveman skill, following the same
structure as caveman-es and caveman-cn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:35:50 -03:00
Julius BrusseeandGitHub 9ee0e352c6 Merge pull request #37 from leoz32/feat/caveman-cn-skill
Add Chinese caveman skill (caveman-cn)
2026-04-08 11:36:39 +02:00
Julius BrusseeandGitHub d6fbf67db3 Merge pull request #35 from akshayjpatil/feature/bundle-compress-skill
Bundle caveman-compress as /caveman:compress sub-skill
2026-04-08 11:36:26 +02:00
Julius BrusseeandGitHub 1b15fc7170 Merge pull request #27 from mvanhorn/osc/8-spanish-caveman
Add Spanish caveman mode (caveman-es)
2026-04-08 11:36:21 +02:00
Julius BrusseeandGitHub dfcdbc0eee Merge pull request #26 from JoaoVasques/add-cursor-support
Add Cursor skill support with CI sync
2026-04-08 11:36:13 +02:00
Julius BrusseeandGitHub 7e8b5f36cf Merge pull request #24 from FezanMuhammadAli/patch-1
Correct copilot → github-copilot in install command
2026-04-08 11:36:09 +02:00
Test UserandClaude Opus 4.6 d37c921f29 Fix path regex and prevent backup file overwriting
- Fix PATH_REGEX in validate.py: The regex was incorrectly matching
  relative paths like 'src/file.js' as 'rc/' instead of the full path.
  Changed to use non-capturing group with optional prefix.

- Add safety check in compress.py: Prevent silent overwriting of
  existing .original.md backup files. Now aborts with a warning
  instead of overwriting potentially important backup content.

- Add proper module initialization in __init__.py: Previously empty,
  now includes __all__ exports and __version__ for proper imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:32:37 +08:00
Leo 9a8e9c4959 Add caveman-cn skill 2026-04-08 14:25:49 +08:00
Leo 636695bd0f Add caveman-cn skill 2026-04-08 14:25:47 +08:00
Leo 071926541d Add caveman-cn skill 2026-04-08 14:25:45 +08:00
Akshay Patil 0875a71f8d fix: align built-in compress wiring and docs 2026-04-07 20:44:27 -04:00
Akshay Patil c512cb8487 Merge main into feature/bundle-compress-skill 2026-04-07 17:31:57 -04:00
Akshay Patil 981b6069d8 feat: bundle caveman-compress as /caveman:compress skill
- Update caveman-compress/SKILL.md name to 'compress' and trigger to '/caveman:compress'
- Create symlinks from compress/ (Claude Code) and plugins/caveman/skills/compress/ (Codex/npx) to caveman-compress/
- Single source of truth: caveman-compress/ holds all scripts and SKILL.md
- Update README to reflect compress as built-in skill

Fixes #33
2026-04-07 17:11:54 -04:00
fuleinist 4863bc20d9 feat: add 文言文 (Classical Chinese) mode with wenyan-lite/full/ultra intensities 2026-04-08 05:09:51 +08:00
Julius BrusseeandGitHub a3b255235d Merge pull request #29 from nisrulz/patch-1 2026-04-07 22:52:01 +02:00
Julius BrusseeandGitHub 4cfbd9c4ba Merge pull request #31 from Juris-S/patch-1 2026-04-07 22:50:40 +02:00
Juris SandGitHub c3ca528c76 Update GitHub Copilot reference in README
`copilot` is an invalid agent arg when calling `npx skills add`, use `github-copilot`.

Check:
Valid agents: amp, antigravity, augment, bob, claude-code, openclaw, cline, codebuddy, codex, command-code, continue, cortex, crush, cursor, deepagents, droid, firebender, gemini-cli, github-copilot, goose, junie, iflow-cli, kilo, kimi-cli, kiro-cli, kode, mcpjam, mistral-vibe, mux, opencode, openhands, pi, qoder, qwen-code, replit, roo, trae, trae-cn, warp, windsurf, zencoder, neovate, pochi, adal, universal
2026-04-07 14:14:12 +01:00
cbarrado 984f63fa9e docs: add windows codex install steps 2026-04-07 15:03:40 +02:00
cbarrado 58ca4a6d3e fix: add codex plugin install metadata 2026-04-07 14:55:57 +02:00
Nishant SrivastavaandGitHub 942ecb87a8 Update README with adding skill to Codex
- Using the `npx skill add` approach avoids having to clone the repo.
- Added instructions for installing to Codex and updated usage section.
2026-04-07 13:04:39 +02:00
Matt Van Horn edcefa328e feat: add Spanish caveman mode (caveman-es)
Add skills/caveman-es/SKILL.md following the same structure as the
English caveman skill. Covers the same three intensity levels (lite,
full, ultra) with Spanish grammar rules and examples.

Technical terms stay in English (useMemo, connection pooling, etc.)
since that's how most Spanish-speaking devs use them. Only the
surrounding prose becomes terse Spanish caveman.

Fixes #8
2026-04-07 02:12:07 -07:00
Joao Vasques bca1e756d2 Add native Cursor skill support and sync automation
Add project-level Cursor skill discovery support by introducing
`.cursor/skills/caveman/SKILL.md` as a copy of the canonical
`skills/caveman/SKILL.md`, without changing skill behavior.

Update the sync workflow to keep the new Cursor copy aligned with the
canonical source alongside existing synced targets, and include it in the
CI commit step. Also update CONTRIBUTING notes to document that this new
path is auto-synced and should not be edited directly.
2026-04-07 09:43:19 +01:00
Fezan Muhammad AliandGitHub 6971824898 Update GitHub Copilot command in README 2026-04-07 01:52:08 +05:00
160 changed files with 16437 additions and 633 deletions
-20
View File
@@ -1,20 +0,0 @@
{
"name": "caveman-repo",
"interface": {
"displayName": "Caveman Repo"
},
"plugins": [
{
"name": "caveman",
"source": {
"source": "local",
"path": "./plugins/caveman"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "caveman",
"description": "Ultra-compressed communication mode for Claude Code. Cuts ~75% of tokens while keeping full technical accuracy.",
"description": "Ultra-compressed communication mode for Claude Code. Cuts 65% of output tokens (measured) while keeping full technical accuracy.",
"owner": {
"name": "Julius Brussee",
"url": "https://github.com/JuliusBrussee"
@@ -9,7 +9,7 @@
"plugins": [
{
"name": "caveman",
"description": "Talk like caveman. Cut ~75% tokens. Keep all technical accuracy.",
"description": "Talk like caveman. Cut 65% output tokens (measured). Keep all technical accuracy.",
"source": "./",
"category": "productivity"
}
+27 -1
View File
@@ -1,8 +1,34 @@
{
"name": "caveman",
"description": "Ultra-compressed communication mode. Cuts ~75% of tokens while keeping full technical accuracy by speaking like a caveman.",
"description": "Ultra-compressed communication mode. Cuts 65% of output tokens (measured) while keeping full technical accuracy by speaking like a caveman.",
"author": {
"name": "Julius Brussee",
"url": "https://github.com/JuliusBrussee"
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/src/hooks/caveman-activate.js\"",
"timeout": 5,
"statusMessage": "Loading caveman mode..."
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/src/hooks/caveman-mode-tracker.js\"",
"timeout": 5,
"statusMessage": "Tracking caveman mode..."
}
]
}
]
}
}
+5
View File
@@ -0,0 +1,5 @@
[features]
# Both keys: codex-cli renamed codex_hooks → hooks; old versions (<=0.120.0)
# silently ignore unknown keys, so shipping both activates on either (#617).
hooks = true
codex_hooks = true
+17
View File
@@ -0,0 +1,17 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "echo 'CAVEMAN MODE ACTIVE. Rules: Drop articles/filler/pleasantries/hedging. Fragments OK. Short synonyms. Pattern: [thing] [action] [reason]. [next step]. Not: Sure! I would be happy to help you with that. Yes: Bug in auth middleware. Fix: Code/commits/security: write normal. User says stop caveman or normal mode to deactivate.'",
"timeout": 5,
"statusMessage": "Loading caveman mode"
}
]
}
]
}
}
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: JuliusBrussee
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+46
View File
@@ -0,0 +1,46 @@
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
node:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Installer test suite
run: npm test
- name: Standalone hook/tool tests
run: |
for f in tests/test_*.js; do
echo "== $f"
node "$f"
done
python:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# Several python tests shell out to node for hook checks — don't rely
# on the runner image happening to ship it.
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Python test suite
run: python -m unittest discover -s tests -v
+35 -8
View File
@@ -1,10 +1,14 @@
name: Sync SKILL.md
name: Sync SKILL.md and rules
on:
push:
branches: [main]
paths:
- skills/caveman/SKILL.md
- skills/cavecrew/SKILL.md
- agents/cavecrew-*.md
- skills/caveman-compress/SKILL.md
- skills/caveman-compress/scripts/**
concurrency:
group: sync-skill
@@ -26,18 +30,41 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
git pull --rebase origin main
- name: Copy to duplicate locations
- name: Sync SKILL.md copies
run: |
cp skills/caveman/SKILL.md caveman/SKILL.md
cp skills/caveman/SKILL.md plugins/caveman/skills/caveman/SKILL.md
- name: Rebuild caveman.skill ZIP
- name: Sync caveman-compress skill to plugin
run: |
cd skills && zip -r ../caveman.skill caveman/
# Plugin distribution mirrors source verbatim — no rename, no sed.
mkdir -p plugins/caveman/skills/caveman-compress
cp skills/caveman-compress/SKILL.md plugins/caveman/skills/caveman-compress/SKILL.md
rm -rf plugins/caveman/skills/caveman-compress/scripts
cp -r skills/caveman-compress/scripts plugins/caveman/skills/caveman-compress/scripts
rm -rf plugins/caveman/skills/caveman-compress/scripts/__pycache__
- name: Sync cavecrew skill + agents to plugin
run: |
mkdir -p plugins/caveman/skills/cavecrew plugins/caveman/agents
cp skills/cavecrew/SKILL.md plugins/caveman/skills/cavecrew/SKILL.md
cp agents/cavecrew-investigator.md plugins/caveman/agents/cavecrew-investigator.md
cp agents/cavecrew-builder.md plugins/caveman/agents/cavecrew-builder.md
cp agents/cavecrew-reviewer.md plugins/caveman/agents/cavecrew-reviewer.md
- name: Rebuild caveman.skill ZIP
run: mkdir -p dist && cd skills && zip -r ../dist/caveman.skill caveman/
- name: Commit and push if changed
run: |
git diff --quiet && exit 0
git add caveman/SKILL.md plugins/caveman/skills/caveman/SKILL.md caveman.skill
git commit -m "chore: sync SKILL.md copies and caveman.skill [skip ci]"
git add \
skills/caveman-compress/ \
plugins/caveman/skills/caveman-compress/ \
plugins/caveman/skills/caveman/SKILL.md \
plugins/caveman/skills/cavecrew/SKILL.md \
plugins/caveman/agents/cavecrew-investigator.md \
plugins/caveman/agents/cavecrew-builder.md \
plugins/caveman/agents/cavecrew-reviewer.md \
dist/caveman.skill
git diff --staged --quiet && exit 0
git commit -m "chore: sync SKILL.md copies [skip ci]"
git push
+11 -1
View File
@@ -3,5 +3,15 @@ __pycache__/
*.pyc
.venv/
.env.local
caveman-compress.md
**/.DS_Store
.claude/worktrees/
evals/snapshots/*.html
evals/snapshots/*.png
context/refs/research-brief-caveman-code-efficiency.md
# Build artifacts
dist/*
!dist/caveman.skill
# Local star-history chart scratch (not part of the product)
tmp-starcharts/
+4
View File
@@ -0,0 +1,4 @@
@./skills/caveman/SKILL.md
@./skills/caveman-commit/SKILL.md
@./skills/caveman-review/SKILL.md
@./skills/caveman-compress/SKILL.md
+306
View File
@@ -0,0 +1,306 @@
# CLAUDE.md — caveman
## README is a product artifact
README = product front door. Non-technical people read it to decide if caveman worth install. Treat like UI copy.
**Rules for any README change:**
- Readable by non-AI-agent users. If you write "SessionStart hook injects system context," invisible to most — translate it.
- Keep Before/After examples first. That the pitch.
- Install table always complete + accurate. One broken install command costs real user.
- What You Get table must sync with actual code. Feature ships or removed → update table.
- Preserve voice. Caveman speak in README on purpose. "Brain still big." "Cost go down forever." "One rock. That it." — intentional brand. Don't normalize.
- Benchmark numbers from real runs in `benchmarks/` and `evals/`. Never invent or round. Re-run if doubt.
- Adding new agent to install table → add detail block in `<details>` section below.
- Readability check before any README commit: would non-programmer understand + install within 60 seconds?
---
## Project overview
Caveman makes AI coding agents respond in compressed caveman-style prose — cuts 65% output tokens (measured), full technical accuracy. Ships as Claude Code plugin, Codex plugin, Gemini CLI extension, agent rule files for Cursor, Windsurf, Cline, Copilot, 40+ others via `npx skills`.
---
## What lives where
Post-cleanup layout. Sources of truth at the top, distribution mirrors below, build outputs in `dist/`, human docs alongside each skill.
```
caveman/
├── README.md # Front door (product pitch)
├── INSTALL.md # Per-agent install commands
├── CONTRIBUTING.md # Dev guide
├── CLAUDE.md # This file (maintainer instructions)
├── AGENTS.md / GEMINI.md # Autodiscovery files (must stay at root)
├── install.sh / install.ps1 # 30-line shims → cli/install.js
├── cli/ # Unified installer
│ ├── install.js # Single source for all 30+ agents (PROVIDERS array)
│ └── lib/settings.js # JSONC-tolerant settings.json reader/writer
├── skills/ # ALL skills, single source of truth
│ ├── caveman/{SKILL.md, README.md}
│ ├── caveman-commit/{SKILL.md, README.md}
│ ├── caveman-review/{SKILL.md, README.md}
│ ├── caveman-help/{SKILL.md, README.md}
│ ├── caveman-stats/{SKILL.md, README.md}
│ ├── caveman-compress/{SKILL.md, README.md, scripts/}
│ └── cavecrew/{SKILL.md, README.md}
├── agents/ # cavecrew subagents (single source — kept at root for plugin auto-discovery)
├── commands/ # Codex/Gemini TOML command stubs (root for plugin auto-discovery)
├── src/ # Internal source — not auto-discovered by plugin
│ ├── hooks/ # Claude Code hooks (installer reads here)
│ ├── rules/ # Auto-activation rule body (single source)
│ ├── tools/ # caveman-init.js (per-repo rule writer)
│ └── mcp-servers/ # caveman-shrink npm-published MCP middleware
├── .claude-plugin/ # Claude Code plugin manifest (REQUIRED at root)
├── plugins/caveman/ # Claude Code plugin distribution (CI-mirrored)
│ ├── skills/ # ← from skills/
│ └── agents/ # ← from agents/
├── dist/ # Build artifacts (gitignored)
│ └── caveman.skill # ZIP of skills/caveman/, rebuilt by CI
├── tests/ # All tests (Node + Python)
├── benchmarks/ # Real token measurements through Claude API
├── evals/ # Three-arm eval harness
├── docs/ # User-facing docs site
└── .github/workflows/ # CI sync
```
---
## File structure and what owns what
### Single source of truth files — edit only these
| File | What it controls |
|------|-----------------|
| `skills/caveman/SKILL.md` | Caveman behavior: intensity levels, rules, wenyan mode, auto-clarity, persistence. Only file to edit for behavior changes. |
| `src/rules/caveman-activate.md` | Always-on auto-activation rule body. Consumed by `src/tools/caveman-init.js` when a user runs `npx caveman --with-init` (per-repo IDE rule files). Edit here, not in any per-agent rule copy. |
| `src/rules/caveman-openclaw-bootstrap.md` | Marker-fenced bootstrap snippet appended to `~/.openclaw/workspace/SOUL.md` by `cli/lib/openclaw.js`. Drives always-on caveman through the OpenClaw gateway. Must include the SENTINEL `Respond terse like smart caveman` and stay well under OpenClaw's 12K-per-bootstrap-file cap. |
| `cli/lib/openclaw.js` | OpenClaw install/uninstall helper. Frontmatter merge (`version`, `always: true`), SOUL.md marker append/strip, idempotent. Shared by `cli/install.js` and `src/tools/caveman-init.js`. |
| `skills/caveman-commit/SKILL.md` | Caveman commit message behavior. Fully independent skill. |
| `skills/caveman-review/SKILL.md` | Caveman code review behavior. Fully independent skill. |
| `skills/caveman-help/SKILL.md` | Quick-reference card. One-shot display, not a persistent mode. |
| `skills/caveman-compress/SKILL.md` | Compress sub-skill behavior. |
| `skills/cavecrew/SKILL.md` | Cavecrew decision guide — when to delegate to caveman subagents vs vanilla. Edit only here. |
| `agents/cavecrew-investigator.md` | Read-only locator subagent (haiku). Output contract: `path:line — symbol — note`. |
| `agents/cavecrew-builder.md` | Surgical 1-2 file editor subagent. Refuses 3+ file scope. |
| `agents/cavecrew-reviewer.md` | Diff/file reviewer subagent (haiku). One-line findings with severity emoji. |
| `src/plugins/opencode/plugin.js` | opencode native plugin. ESM Bun module — `session.created` writes flag, `tui.prompt.append` parses slash/natural-language activation and appends per-prompt reinforcement. Reuses `caveman-config.js` via `createRequire`. |
| `src/plugins/opencode/commands/*.md` | Six opencode slash-command prompt templates (`/caveman`, `/caveman-{commit,review,compress,stats,help}`). |
### Auto-generated / auto-synced — do not edit directly
We removed the agent-specific dotdir mirrors at the repo root (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, root `caveman/SKILL.md`). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run `npx caveman --with-init` once (writes per-repo rule files from `src/rules/caveman-activate.md` via `src/tools/caveman-init.js`). For per-user installs through the upstream skills CLI, `npx caveman --only <agent>` runs `npx skills add ... -a <profile>`.
A handful of dotdir leftovers (`.junie/`, `.kiro/`, `.roo/`, `.agents/`) still hold a stale `cavecrew/SKILL.md` mirror from before the cleanup. They aren't read by anything in the current install path; remove on sight, no migration needed.
What's left is the Claude Code plugin distribution (required by the plugin loader) and the release ZIP.
| File | Synced from |
|------|-------------|
| `plugins/caveman/skills/caveman/SKILL.md` | `skills/caveman/SKILL.md` |
| `plugins/caveman/skills/caveman-compress/SKILL.md` (+ `scripts/`) | `skills/caveman-compress/SKILL.md` (+ `scripts/`) |
| `plugins/caveman/skills/cavecrew/SKILL.md` | `skills/cavecrew/SKILL.md` |
| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |
| `dist/caveman.skill` | ZIP of `skills/caveman/` directory (gitignored; rebuilt by CI on release) |
Skills not in this table (`caveman-commit`, `caveman-review`, `caveman-help`, `caveman-stats`) are not mirrored into the Claude Code plugin distribution by CI. They reach Claude Code through the standalone hook + skill install path, and reach other agents via `npx skills add`. A `plugins/caveman/skills/caveman-stats/` directory is currently checked in as a hand-committed copy; the sync workflow does not touch it, so don't rely on edits there to propagate.
---
## CI sync workflow
`.github/workflows/sync-skill.yml` triggers on main push when `skills/**/SKILL.md` or `agents/cavecrew-*.md` changes.
What it does:
1. Copies `skills/caveman/SKILL.md` and `skills/cavecrew/SKILL.md` into their `plugins/caveman/skills/<name>/` mirrors so the Claude Code plugin loader sees the latest behavior.
2. Copies `skills/caveman-compress/SKILL.md` and its `scripts/` into `plugins/caveman/skills/caveman-compress/`.
3. Copies `agents/cavecrew-*.md` into `plugins/caveman/agents/`.
4. Rebuilds `dist/caveman.skill` (ZIP of `skills/caveman/`) for the release artifact.
5. Commits and pushes with `[skip ci]` to avoid loops.
CI bot commits as `github-actions[bot]`. After PR merge, wait for workflow before declaring release complete.
The old steps that mirrored SKILL.md and rules into root dotdirs (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`) are gone — those mirrors no longer exist. The old `caveman-compress/``skills/compress/` rename-on-sync is also gone now that compress lives at `skills/caveman-compress/`.
---
## Hook system (Claude Code)
Three hooks in `src/hooks/` plus a `caveman-config.js` shared module and a `package.json` CommonJS marker. Communicate via flag file at `$CLAUDE_CONFIG_DIR/.caveman-active` (falls back to `~/.claude/.caveman-active`).
```
SessionStart hook ──writes "full"──▶ $CLAUDE_CONFIG_DIR/.caveman-active ◀──writes mode── UserPromptSubmit hook
reads
caveman-statusline.sh
[CAVEMAN] / [CAVEMAN:ULTRA] / ...
```
`src/hooks/package.json` pins the directory to `{"type": "commonjs"}` so the `.js` hooks resolve as CJS even when an ancestor `package.json` (e.g. `~/.claude/package.json` from another plugin) declares `"type": "module"`. Without this, `require()` blows up with `ReferenceError: require is not defined in ES module scope`.
All hooks honor `CLAUDE_CONFIG_DIR` for non-default Claude Code config locations.
### `src/hooks/caveman-config.js` — shared module
Exports:
- `getDefaultMode()` — resolves default mode in order: `CAVEMAN_DEFAULT_MODE` env var → repo-local config (`<cwd>/.caveman/config.json` or `<cwd>/.caveman.json`, walking up to the filesystem root) → user config (`$XDG_CONFIG_HOME/caveman/config.json` / `~/.config/caveman/config.json` / `%APPDATA%\caveman\config.json`) → `'full'`. The env var short-circuits before any cwd walk. Repo-local config lets a team check in a per-project default without polluting every contributor's env or user config.
- `findRepoConfigPath(start)` — walks up from `start` (default `process.cwd()`) looking for the first `.caveman/config.json` or `.caveman.json`. Bounded to 64 ancestors. Refuses symlinked files (symmetric with `safeWriteFlag` / `readFlag`).
- `safeWriteFlag(flagPath, content)` — symlink-safe flag write. Refuses if flag target or its immediate parent is a symlink. Opens with `O_NOFOLLOW` where supported. Atomic temp + rename. Creates with `0600`. Protects against local attackers replacing the predictable flag path with a symlink to clobber files writable by the user. Used by both write hooks. Silent-fails on all filesystem errors.
### `src/hooks/caveman-activate.js` — SessionStart hook
Runs once per Claude Code session start. Three things:
1. Writes the active mode to `$CLAUDE_CONFIG_DIR/.caveman-active` via `safeWriteFlag` (creates if missing). Branches on the hook payload's `source` field (#691): `startup` resets to the configured default; `resume`/`clear`/`compact` re-fires preserve a valid existing flag so mid-session `/caveman <level>` switches survive.
2. Emits caveman ruleset as hidden stdout — Claude Code injects SessionStart hook stdout as system context, invisible to user
3. Checks `settings.json` for statusline config; if missing, appends nudge to offer setup — once per install, gated by a `.caveman-nudge-shown` marker file (#661)
Silent-fails on all filesystem errors — never blocks session start.
### `src/hooks/caveman-mode-tracker.js` — UserPromptSubmit hook
Reads JSON from stdin. Three responsibilities:
**1. Slash-command activation.** If prompt starts with `/caveman`, writes mode to flag file via `safeWriteFlag`:
- `/caveman` → configured default (see `caveman-config.js`, defaults to `full`)
- `/caveman lite``lite`
- `/caveman ultra``ultra`
- `/caveman wenyan` or `/caveman wenyan-full``wenyan` (alias) / `wenyan-full`
- `/caveman wenyan-lite``wenyan-lite`
- `/caveman wenyan-ultra``wenyan-ultra`
- `/caveman-commit``commit`
- `/caveman-review``review`
- `/caveman-compress``compress`
**2. Natural-language activation/deactivation.** Matches phrases like "activate caveman", "turn on caveman mode", "talk like caveman" and writes the configured default mode. Matches "stop caveman", "disable caveman", "normal mode", "deactivate caveman" etc. and deletes the flag file. README promises these triggers, the hook enforces them.
**3. Per-turn reinforcement.** When flag is set to a non-independent mode (i.e. not `commit`/`review`/`compress`), emits a small `hookSpecificOutput` JSON reminder so the model keeps caveman style after other plugins inject competing instructions mid-conversation. The full ruleset still comes from SessionStart — this is just an attention anchor.
### `src/hooks/caveman-statusline.sh` — Statusline badge
Reads flag file at `$CLAUDE_CONFIG_DIR/.caveman-active`. Outputs colored badge string for Claude Code statusline:
- `full` or empty → `[CAVEMAN]` (orange)
- anything else → `[CAVEMAN:<MODE_UPPERCASED>]` (orange)
Then appends the lifetime-savings suffix (`⛏ 12.4k`) read from `$CLAUDE_CONFIG_DIR/.caveman-statusline-suffix` — written by `caveman-stats.js` on every `/caveman-stats` run. **Default on**; users opt out with `CAVEMAN_STATUSLINE_SAVINGS=0`. The suffix file is absent until `/caveman-stats` runs at least once, so fresh installs render no fake number.
Configured in `settings.json` under `statusLine.command`. PowerShell counterpart at `src/hooks/caveman-statusline.ps1` for Windows. Both scripts symlink-refuse and whitelist-validate the flag/suffix file contents — never echo arbitrary bytes.
### Hook installation
**Plugin install** — hooks wired automatically by plugin system.
**Standalone install**`cli/install.js` (the unified Node installer) copies hook files into `$CLAUDE_CONFIG_DIR/hooks/` and merges SessionStart + UserPromptSubmit + statusline into `settings.json`. Uses the JSONC-tolerant helpers in `cli/lib/settings.js` so a commented `settings.json` no longer crashes the merge. Defensive `validateHookFields` runs before every write to prevent a single malformed hook from poisoning the entire file (Claude Code Zod silently discards the whole `settings.json` on schema mismatch).
The `install.sh` / `install.ps1` shims at the repo root delegate to `cli/install.js` via `node` (local clone) or `npx -y github:JuliusBrussee/caveman` (curl|bash). No legacy fallback path remains — earlier `install.sh.legacy` / `install.ps1.legacy` files were removed.
**Uninstall**`npx -y github:JuliusBrussee/caveman -- --uninstall` (or `node cli/install.js --uninstall` from a clone). Strips caveman hook entries from `settings.json` via substring marker `caveman`, deletes hook files, and removes the Claude plugin / Gemini extension. Also removes state files from `$CLAUDE_CONFIG_DIR` (`.caveman-active`, `.caveman-active.prev`, `.caveman-mode-log.jsonl`, `.caveman-statusline-suffix`, `.caveman-nudge-shown`); keeps `.caveman-history.jsonl` (lifetime savings data) with a printed note (#635). Skill installs done via `npx skills add` must be removed via the IDE's skill manager (we don't track them).
---
## Skill system
Skills = Markdown files with YAML frontmatter consumed by Claude Code's skill/plugin system and by `npx skills` for other agents.
Each skill has a human-facing `README.md` alongside the LLM-facing `SKILL.md`. The README explains what the skill does for users browsing GitHub; the SKILL.md is the prompt body the agent loads. Don't merge them — different audiences, different formats.
### Intensity levels
Defined in `skills/caveman/SKILL.md`. Six levels: `lite`, `full` (default), `ultra`, `wenyan-lite`, `wenyan-full`, `wenyan-ultra`. Persists until changed or session ends.
### Auto-clarity rule
Caveman drops to normal prose for: security warnings, irreversible action confirmations, multi-step sequences where fragment ambiguity risks misread, user confused or repeating question. Resumes after. Defined in skill — preserve in any SKILL.md edit.
### caveman-compress
Sub-skill in `skills/caveman-compress/SKILL.md`. Takes file path, compresses prose to caveman style, writes to original path, saves backup at `<filename>.original.md`. Validates headings, code blocks, URLs, file paths, commands preserved. Retries up to 2 times on failure with targeted patches only. Requires Python 3.10+.
The slash command is `/caveman-compress` everywhere — same name in plugin and standalone install. CI no longer renames the directory on sync (the old `caveman-compress/``skills/compress/` sed rename is gone now that the source lives at `skills/caveman-compress/`).
### caveman-commit / caveman-review
Independent skills in `skills/caveman-commit/SKILL.md` and `skills/caveman-review/SKILL.md`. Both have own `description` and `name` frontmatter so they load independently. caveman-commit: Conventional Commits, ≤50 char subject. caveman-review: one-line comments in `L<line>: <severity> <problem>. <fix>.` format.
---
## Agent distribution
How caveman reaches each agent type:
| Agent | Mechanism | Auto-activates? |
|-------|-----------|----------------|
| Claude Code | Plugin (hooks + skills) or standalone hooks | Yes — SessionStart hook injects rules |
| Codex | Plugin in `plugins/caveman/` plus repo `.codex/hooks.json` and `.codex/config.toml` | Yes on macOS/Linux — SessionStart hook |
| Gemini CLI | Extension with `GEMINI.md` context file | Yes — context file loads every session |
| opencode | Native plugin (`src/plugins/opencode/`) copied into `~/.config/opencode/plugins/caveman/` + `AGENTS.md` ruleset + skills/agents/commands directories. Plugin uses `session.created` and `tui.prompt.append` lifecycle hooks. No statusline (opencode TUI exposes no plugin-writable badge). | Yes — `session.created` writes flag, `AGENTS.md` carries always-on ruleset |
| OpenClaw | Workspace skill at `~/.openclaw/workspace/skills/caveman/SKILL.md` (frontmatter merged with `version` + `always: true`) plus a marker-fenced bootstrap block in `~/.openclaw/workspace/SOUL.md`. Both writes go through `cli/lib/openclaw.js`; workspace path is overridable via `OPENCLAW_WORKSPACE`. | Yes — SOUL.md is auto-injected each turn under "Project Context" (subject to OpenClaw's 12K-per-file / 60K-total bootstrap caps) |
| Cursor | `npx skills add ... -a cursor` (default via `--only cursor`) writes the upstream skill profile; per-repo `.cursor/rules/caveman.mdc` via `--with-init` (calls `src/tools/caveman-init.js`) | Yes — always-on rule |
| Windsurf | `npx skills add ... -a windsurf` (default via `--only windsurf`); per-repo `.windsurf/rules/caveman.md` via `--with-init` | Yes — always-on rule |
| Cline | `npx skills add ... -a cline` (default via `--only cline`); per-repo `.clinerules/caveman.md` via `--with-init` | Yes — Cline auto-discovers `.clinerules/` |
| Copilot | `npx skills add ... -a github-copilot` (soft probe — pass `--only copilot`); per-repo `.github/copilot-instructions.md` + `AGENTS.md` via `--with-init` | Yes — repo-wide instructions |
| Others (Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, ForgeCode, Bob, Crush, iFlow, OpenHands, Qoder, Rovo Dev, Replit, Antigravity, …) | `npx skills add JuliusBrussee/caveman -a <profile>` | No — user must say `/caveman` each session |
opencode reaches Tier 1 minus the statusline (opencode's TUI has no plugin-writable badge). Mode flag lives at `~/.config/opencode/.caveman-active` for any external tooling that wants to surface it.
For agents without hook systems, the always-on snippet lives in `INSTALL.md`'s "Want it always on?" section — keep current with `src/rules/caveman-activate.md`.
**Adding a new agent.** Edit the `PROVIDERS` array in `cli/install.js` — single source of truth, no more bash/PS1 dual-source drift. Each entry has `id`, `label`, `mech`, `detect` (clause spec like `command:foo||dir:$HOME/x`), optional `profile` (vercel-labs/skills slug), optional `soft: true` (config-dir-only detection).
1. The profile slug must exist in upstream [vercel-labs/skills](https://github.com/vercel-labs/skills). Verify against the README before merging — wrong slugs cause `npx skills add` to fail at runtime, not at install-script load.
2. Run `node cli/install.js --list` to confirm the new row renders correctly.
3. Soft probes (config-dir-only) are fine but tag them with `soft: true`. They render with `(soft)` in `--list` so users know detection is best-effort.
---
## Evals
`evals/` has three-arm harness:
- `__baseline__` — no system prompt
- `__terse__``Answer concisely.`
- `<skill>``Answer concisely.\n\n{SKILL.md}`
Honest delta = **skill vs terse**, not skill vs baseline. Baseline comparison conflates skill with generic terseness — that cheating. Harness designed to prevent this.
`llm_run.py` calls `claude -p --system-prompt ...` per (prompt, arm), saves to `evals/snapshots/results.json`. `measure.py` reads snapshot offline with tiktoken (OpenAI BPE — approximates Claude tokenizer, ratios meaningful, absolute numbers approximate).
Add skill: drop `skills/<name>/SKILL.md`. Harness auto-discovers. Add prompt: append line to `evals/prompts/en.txt`.
Snapshots committed to git. CI reads without API calls. Only regenerate when SKILL.md or prompts change.
---
## Benchmarks
`benchmarks/` runs real prompts through Claude API (not Claude Code CLI), records raw token counts. Results committed as JSON in `benchmarks/results/`. Benchmark table in README generated from results — update when regenerating.
To reproduce: `uv run python benchmarks/run.py` (needs `ANTHROPIC_API_KEY` in `.env.local`).
---
## Key rules for agents working here
- Edit `skills/<name>/SKILL.md` for behavior changes. Never edit synced copies under `plugins/caveman/skills/`.
- Edit `src/rules/caveman-activate.md` for auto-activation rule changes. Never edit any per-agent rule copy a user has on their machine.
- Edit `src/rules/caveman-openclaw-bootstrap.md` for the OpenClaw SOUL.md bootstrap snippet. Keep the `<!-- caveman-begin -->` / `<!-- caveman-end -->` markers and the `Respond terse like smart caveman` sentinel — `cli/lib/openclaw.js` keys idempotency off both. If you change the embedded fallback in `cli/lib/openclaw.js`, keep it byte-equivalent to the file.
- Per-skill human docs live in `skills/<name>/README.md`. The LLM-facing body is in `SKILL.md`. Don't merge them — different audiences.
- Build artifacts go in `dist/`. Never check files into `dist/` manually — CI rebuilds them on push, and `dist/` is gitignored.
- README most important file for user-facing impact. Optimize for non-technical readers. Preserve caveman voice.
- `INSTALL.md` is the per-agent install reference. Keep the install table in `README.md` short and link out to `INSTALL.md` for the full matrix.
- Benchmark and eval numbers must be real. Never fabricate or estimate.
- CI workflow commits back to main after merge. Account for when checking branch state.
- Hook files must silent-fail on all filesystem errors. Never let hook crash block session start.
- Any new flag file write must go through `safeWriteFlag()` in `caveman-config.js`. Direct `fs.writeFileSync` on predictable user-owned paths reopens the symlink-clobber attack surface.
- Hooks must respect `CLAUDE_CONFIG_DIR` env var, not hardcode `~/.claude`. Same for `cli/install.js` / statusline scripts.
- `cli/install.js` is the only installer source. `install.sh` / `install.ps1` at repo root are 30-line shims that delegate to it. Never re-add per-OS install logic to the shims — that's how we got the Windows quoting bug (#249).
- Any settings.json read in installer or hooks must go through `cli/lib/settings.js` `readSettings()` so JSONC comments don't crash the merge. Any settings.json write must run through `validateHookFields()` first.
+55
View File
@@ -0,0 +1,55 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best for the overall community, not just for us as
individuals
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and unwelcome sexual attention or
advances
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
## Reporting & Contact
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the repository owners. All complaints will be reviewed and investigated
promptly and fairly.
+184 -12
View File
@@ -1,20 +1,192 @@
# Contributing
# Contributing to caveman
Improvements to the SKILL.md prompt are welcome — open a PR with before/after examples showing the change.
Thanks for considering a contribution. Caveman is a multi-agent skill that
makes 30+ AI coding agents talk in compressed caveman-style prose. Most
contributions fall into one of three buckets:
## How
1. **Editing skill prose** — change how caveman speaks, what intensity levels do, what slash commands trigger.
2. **Adding a new agent** — wire a fresh editor/CLI/IDE into the unified installer.
3. **Fixing the hooks or installer** — Claude Code hooks, the Node installer, the per-repo init script.
1. Fork repo
2. Edit `skills/caveman/SKILL.md` — this is the only copy you need to touch
3. Open PR with:
- **Before:** what caveman say now
- **After:** what caveman say with change
- One sentence why change better
Caveman like simple. Small focused PR > big rewrite.
> **Note:** `caveman/SKILL.md`, `plugins/caveman/skills/caveman/SKILL.md`, and `caveman.skill` are auto-synced by CI after merge. Do not edit them directly.
---
Small focused change > big rewrite. Caveman like simple.
## Quick orientation
The repo distributes one skill (caveman) plus a handful of sub-skills
(caveman-commit, caveman-review, caveman-compress, cavecrew-*) to many
agents through different distribution mechanisms (Claude Code plugin, Codex
plugin, Gemini extension, Cursor/Windsurf/Cline rule files, `npx skills` for
the long tail). A single Node installer at `cli/install.js` detects which
agents are on the user's machine and installs the right thing for each.
Sources of truth live at the **top level** of the repo. Agent-specific
copies live under `plugins/caveman/` and similar mirror dirs — those are
**rebuilt by CI** and edits there are reverted.
---
## What to edit (sources of truth)
| I want to change... | Edit this file |
|---|---|
| Caveman behavior (intensity levels, voice, rules) | `skills/caveman/SKILL.md` |
| Caveman commit-message format | `skills/caveman-commit/SKILL.md` |
| Caveman code-review format | `skills/caveman-review/SKILL.md` |
| Caveman compress logic | `skills/caveman-compress/SKILL.md` and `skills/caveman-compress/scripts/` |
| Caveman quick-reference card | `skills/caveman-help/SKILL.md` |
| Cavecrew decision guide (when to delegate to subagents) | `skills/cavecrew/SKILL.md` |
| cavecrew subagent definitions | `agents/cavecrew-investigator.md`, `agents/cavecrew-builder.md`, `agents/cavecrew-reviewer.md` |
| Auto-activation rule body (Cursor/Windsurf/Cline/Copilot) | `src/rules/caveman-activate.md` |
| Add support for a new agent | `cli/install.js` (PROVIDERS array) |
| Per-repo init script (drops rule files into a user's repo) | `src/tools/caveman-init.js` |
| Claude Code hooks | `src/hooks/caveman-activate.js`, `src/hooks/caveman-mode-tracker.js`, `src/hooks/caveman-config.js`, `src/hooks/caveman-statusline.sh`, `src/hooks/caveman-statusline.ps1` |
| Settings.json read/write helpers | `cli/lib/settings.js` |
| MCP shrink server | `src/mcp-servers/caveman-shrink/` |
That's it. Every other markdown file with `SKILL.md` in the path is a copy.
---
## What NOT to edit (CI-generated mirrors)
Edits to these files are wiped by the next CI run. The
`.github/workflows/sync-skill.yml` job rebuilds them from the sources above
on every push to `main`.
| Path | Rebuilt from |
|------|--------------|
| `plugins/caveman/skills/caveman/SKILL.md` | `skills/caveman/SKILL.md` |
| `plugins/caveman/skills/caveman-compress/{SKILL.md, scripts/}` | `skills/caveman-compress/{SKILL.md, scripts/}` |
| `plugins/caveman/skills/cavecrew/SKILL.md` | `skills/cavecrew/SKILL.md` |
| `plugins/caveman/agents/cavecrew-*.md` | `agents/cavecrew-*.md` |
| `dist/caveman.skill` | ZIP of `skills/caveman/` (gitignored; rebuilt by CI on each push to `main`) |
`caveman-commit`, `caveman-review`, `caveman-help`, and `caveman-stats` are **not** mirrored under `plugins/caveman/skills/` by CI. Claude Code reaches them through the standalone hook + skill install path and `npx skills` carries them to other agents. If you see `plugins/caveman/skills/caveman-stats/` checked in, treat it as a legacy hand-committed copy — the workflow in `.github/workflows/sync-skill.yml` does not touch it.
When in doubt: if the file lives under `plugins/`, `dist/`, or any agent
dotdir mirror, it's a build artifact. Edit the top-level source instead.
---
## Adding a new agent
The unified Node installer at `cli/install.js` is the **single source of
truth** for the supported-agent list. The README and `INSTALL.md` install
tables mirror it by hand — bash and PowerShell shims at the repo root just
delegate to it.
1. Confirm the agent has a distribution path. Either:
- it has a profile slug in upstream [vercel-labs/skills](https://github.com/vercel-labs/skills) (most common), or
- it has a native plugin / extension / rule-file mechanism we can target.
2. Append a row to the `PROVIDERS` array in `cli/install.js`. Each row needs:
- `id` — short kebab-case identifier (e.g. `windsurf`)
- `label` — human display name (e.g. `Windsurf`)
- `mech` — distribution mechanism (`plugin`, `extension`, `rules-file`, `skills-cli`, …)
- `detect` — clause spec like `command:foo||dir:$HOME/x` describing how to detect the agent
- `profile` — the vercel-labs/skills slug, if applicable
- `soft: true` — set when detection is config-dir-only (best-effort)
3. Run `node cli/install.js --list` and confirm the new row renders correctly. Soft probes should show as `(soft)`.
4. Add a row to the install tables in `README.md` and `INSTALL.md`.
5. No CI changes needed — the workflow re-reads `cli/install.js` automatically.
Bad slug? `npx skills add` fails at install **runtime**, not at install-script
load. Always verify the slug against the vercel-labs/skills README before
merging.
---
## Adding a new skill
1. Create `skills/<name>/SKILL.md` with frontmatter:
```yaml
---
name: <name>
description: <one sentence, present tense>
---
```
2. Create `skills/<name>/README.md` — human-facing summary, install hint, example.
3. Add `skills/<name>/scripts/` if the skill ships helpers (Python or Node).
4. If the skill should be in the Claude Code plugin, add a sync step to `.github/workflows/sync-skill.yml` so CI mirrors it into `plugins/caveman/skills/<name>/`.
5. If it's user-invocable as a slash command, add a row to the slash-command table in `README.md` and `INSTALL.md`.
6. Add an eval prompt to `evals/prompts/en.txt` if you want the eval harness to score it.
---
## Running tests
```bash
# Installer unit + e2e tests (Node)
npm test
# Compress-skill safety tests (Python)
python3 -m unittest tests.test_compress_safety
# Per-repo init tests
node tests/test_caveman_init.js
# Flag-file symlink-safety tests
node tests/test_symlink_flag.js
```
CI runs all of the above on every PR. If any test depends on a network or
external SDK, it must skip cleanly when the dependency is missing — never
gate the whole suite on optional creds.
---
## Running benchmarks and evals
Benchmarks hit the real Claude API and record raw token counts:
```bash
uv run python benchmarks/run.py # needs ANTHROPIC_API_KEY in .env.local
```
Evals are a three-arm offline harness (`__baseline__`, `__terse__`, each skill):
```bash
python evals/llm_run.py # regenerates evals/snapshots/results.json
python evals/measure.py # reads snapshot, prints token deltas
```
Snapshots are committed to git. Only regenerate when a `SKILL.md` or
`evals/prompts/en.txt` changes. Numbers in `README.md` and any docs come from
real runs — never invent or round.
---
## Pull-request guidelines
- **Conventional Commits** for the commit subject. See `skills/caveman-commit/SKILL.md` for the format we use here.
- **One concern per PR.** A README copy-edit and an installer fix go in separate PRs.
- **Update `package.json` `files`** if you add a new top-level directory the installer needs to ship to npm. Files outside that array don't get published.
- **Show before/after** for prose changes to any `SKILL.md`. One sentence on why the new wording is better.
- **Mention the CI sync.** If you edited a source-of-truth file, note it: "CI will resync `plugins/caveman/skills/...` on merge."
PR descriptions don't need to be long. Caveman style fine. Just say what change, why.
---
## Code style
A handful of invariants that have bitten us before. Keep them.
- **Hooks must silent-fail on filesystem errors.** A `try/catch` that swallows the error is correct here. A hook that throws blocks Claude Code session start — that's user-facing breakage. See existing patterns in `src/hooks/caveman-activate.js`.
- **Settings.json reads and writes go through `cli/lib/settings.js`.** It tolerates JSONC comments. Direct `JSON.parse` on a user's `settings.json` will crash on a single `// comment`.
- **Validate hook entries before writing.** Use `validateHookFields()` in `cli/lib/settings.js`. Claude Code's Zod schema silently discards the **entire** `settings.json` on a single bad hook entry — one malformed write poisons the user's whole config.
- **Symlink-safe flag writes via `safeWriteFlag()`** in `src/hooks/caveman-config.js`. The flag file lives at a predictable path under `$CLAUDE_CONFIG_DIR/`; without `O_NOFOLLOW` and a parent-symlink check, a local attacker can clobber any file the user can write.
- **Honor `CLAUDE_CONFIG_DIR`.** Hooks, the installer, and the statusline scripts must respect it — never hardcode `~/.claude`.
- **`install.sh` and `install.ps1` at the repo root are 30-line shims** that delegate to `cli/install.js`. Don't re-add per-OS install logic to them. Quoting bugs that way lie.
---
## Ideas
See [issues labeled `good first issue`](../../issues?q=label%3A%22good+first+issue%22) for starter tasks.
See [issues labeled `good first issue`](../../issues?q=label%3A%22good+first+issue%22)
for starter tasks. Or grep `TODO` / `FIXME` in `src/hooks/`, `cli/`, `src/tools/` —
each one is a real lead.
Caveman like contribution. You bring rock, caveman put rock in pile. Pile
get bigger. Brain still big.
+4
View File
@@ -0,0 +1,4 @@
@./skills/caveman/SKILL.md
@./skills/caveman-commit/SKILL.md
@./skills/caveman-review/SKILL.md
@./skills/caveman-compress/SKILL.md
+261
View File
@@ -0,0 +1,261 @@
# Install caveman
One install. Works for every AI coding agent on your machine.
If just want it to work, run the one-liner. If want to know what gets touched, scroll down.
## One-liner
**macOS / Linux / WSL / Git Bash**
```bash
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash
```
**Windows (PowerShell 5.1+)**
```powershell
irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex
```
> Piping a script straight into a shell runs it sight-unseen. If you'd rather read it first, download then run: `curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh -o install.sh` (review it) `&& bash install.sh`. The installer downloads hook files from a pinned release tag and verifies them against a committed SHA-256 manifest before writing.
What it does:
- Auto-detects every supported agent installed on your machine (Claude Code, Cursor, Codex, etc.).
- For each one, runs that agent's native install path (plugin / extension / rule file / `npx skills add`).
- Wires Claude Code hooks and statusline badge on top. (`caveman-shrink` MCP middleware is opt-in via `--with-mcp-shrink` — see flag table below.)
- Skips anything you don't have. Safe to re-run. ~30 seconds end-to-end.
Want to preview before installing? Use `--dry-run`:
```bash
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash -s -- --dry-run
```
## Per-agent install
If you want to install for one agent (or want to know exactly what command runs under the hood), use the table below. Every row also works as `--only <id>` to the unified installer.
| Agent | Install command | Auto-activates? |
|---|---|:-:|
| **Claude Code** | `claude plugin marketplace add JuliusBrussee/caveman && claude plugin install caveman@caveman` | Yes |
| **Gemini CLI** | `gemini extensions install https://github.com/JuliusBrussee/caveman --consent` | Yes |
| **opencode** | `node cli/install.js --only opencode` *(or `npx -y github:JuliusBrussee/caveman -- --only opencode`)* | Yes (plugin + AGENTS.md) |
| **OpenClaw** | `npx -y github:JuliusBrussee/caveman -- --only openclaw` | Yes (workspace skill + SOUL.md) |
| **Hermes Agent** | `npx -y github:JuliusBrussee/caveman -- --only hermes` *(or `node cli/install.js --only hermes` from a clone)* | Yes (native skills, enabled on load) |
| **Codex CLI** | `npx skills add JuliusBrussee/caveman -a codex` | Per-session: `/caveman` |
| **Cursor** | `npx skills add JuliusBrussee/caveman -a cursor` | Per-session by default; `--with-init` for an always-on rule file |
| **Windsurf** | `npx skills add JuliusBrussee/caveman -a windsurf` | Per-session by default; `--with-init` for an always-on rule file |
| **Cline** | `npx skills add JuliusBrussee/caveman -a cline` | Per-session by default; `--with-init` for an always-on rule file |
| **GitHub Copilot** *(soft probe)* | `npx -y github:JuliusBrussee/caveman -- --only copilot --with-init` | Repo-wide instructions via `--with-init` |
| **Continue** | `npx skills add JuliusBrussee/caveman -a continue` | No — say `/caveman` |
| **Kilo Code** | `npx skills add JuliusBrussee/caveman -a kilo` | No |
| **Roo Code** | `npx skills add JuliusBrussee/caveman -a roo` | No |
| **Augment Code** | `npx skills add JuliusBrussee/caveman -a augment` | No |
| **Aider Desk** | `npx skills add JuliusBrussee/caveman -a aider-desk` | No |
| **Sourcegraph Amp** | `npx skills add JuliusBrussee/caveman -a amp` | No |
| **IBM Bob** | `npx skills add JuliusBrussee/caveman -a bob` | No |
| **Crush** | `npx skills add JuliusBrussee/caveman -a crush` | No |
| **Devin (terminal)** | `npx skills add JuliusBrussee/caveman -a devin` | No |
| **Droid (Factory)** | `npx skills add JuliusBrussee/caveman -a droid` | No |
| **ForgeCode** | `npx skills add JuliusBrussee/caveman -a forgecode` | No |
| **Block Goose** | `npx skills add JuliusBrussee/caveman -a goose` | No |
| **iFlow CLI** | `npx skills add JuliusBrussee/caveman -a iflow-cli` | No |
| **Kiro CLI** | `npx skills add JuliusBrussee/caveman -a kiro-cli` | No |
| **Mistral Vibe** | `npx skills add JuliusBrussee/caveman -a mistral-vibe` | No |
| **OpenHands** | `npx skills add JuliusBrussee/caveman -a openhands` | No |
| **Qwen Code** | `npx skills add JuliusBrussee/caveman -a qwen-code` | No |
| **Atlassian Rovo Dev** | `npx skills add JuliusBrussee/caveman -a rovodev` | No |
| **Tabnine CLI** | `npx skills add JuliusBrussee/caveman -a tabnine-cli` | No |
| **Trae** | `npx skills add JuliusBrussee/caveman -a trae` | No |
| **Warp** | `npx skills add JuliusBrussee/caveman -a warp` | No |
| **Replit Agent** | `npx skills add JuliusBrussee/caveman -a replit` | No |
| **JetBrains Junie** *(soft probe)* | `npx skills add JuliusBrussee/caveman -a junie` | No |
| **Qoder** *(soft probe)* | `npx skills add JuliusBrussee/caveman -a qoder` | No |
| **Google Antigravity** *(soft probe)* | `npx skills add JuliusBrussee/caveman -a antigravity` | No |
"Soft probe" = installer won't auto-detect these without `--only <id>` because there's no reliable always-on signal (Copilot subscription state is auth-gated; the others have no CLI / config-dir-only). Pass the flag when you want them.
For "auto-activates? No" agents, type `/caveman` once per session (or use natural-language triggers like "talk like caveman", "caveman mode").
**Finding a profile slug for `npx skills add ... -a <profile>`?** Either read the table above, or print the live matrix from the installer:
```bash
# Either of these works (install.sh / install.ps1 are thin shims that
# forward all flags to cli/install.js):
bash install.sh --list # macOS / Linux / WSL, from a local clone
pwsh install.ps1 --list # Windows / PowerShell, from a local clone
node cli/install.js --list # any platform, from a local clone
npx -y github:JuliusBrussee/caveman -- --list # no clone needed
```
Each row prints the agent id, profile slug (where applicable), and whether it was auto-detected on your machine. Full agent matrix (with detection rules) is also defined in `cli/install.js` under the `PROVIDERS` array.
## Manual install (no `curl | bash`)
If you'd rather see exactly what runs:
```bash
# Clone the repo
git clone https://github.com/JuliusBrussee/caveman.git
cd caveman
# Preview every command the installer would run
node cli/install.js --dry-run --all
# Inspect the agent matrix
node cli/install.js --list
# Install for everything detected
node cli/install.js --all
```
Useful flags:
| Flag | What |
|---|---|
| `--all` | Plugin + hooks + statusline + per-repo rule files in `$PWD`. (MCP shrink is opt-in — see `--with-mcp-shrink` below.) |
| `--minimal` | Plugin / extension only. No hooks, no MCP shrink, no per-repo rules. |
| `--only <id>` | One agent only. Repeatable: `--only claude --only cursor`. |
| `--dry-run` | Print every command. Write nothing. |
| `--with-init` | Drop always-on rule files into the current repo (`.cursor/`, `.windsurf/`, `.clinerules/`, `.github/copilot-instructions.md`, `.opencode/AGENTS.md`, `AGENTS.md`) and, if OpenClaw is on the box, append the bootstrap block to `~/.openclaw/workspace/SOUL.md`. |
| `--with-mcp-shrink="<upstream cmd>"` | Register `caveman-shrink` MCP proxy wrapping the given upstream MCP server. **Off by default.** A value is required — caveman-shrink is a proxy and exits immediately without one. Example: `--with-mcp-shrink="npx @modelcontextprotocol/server-filesystem /tmp"`. The value is split on whitespace; for paths-with-spaces, install via `node cli/install.js` from a clone or edit `~/.claude.json` after a stub install. |
| `--no-mcp-shrink` | Skip MCP-shrink registration. (Default.) |
| `--with-hooks` / `--no-hooks` | Force-on or force-off the Claude Code hook installer. (Default: on.) |
| `--skip-skills` | Don't run the npx-skills auto-detect fallback when nothing else matched. |
| `--config-dir <path>` | Claude Code config dir for hook files + `settings.json`. **Does NOT scope** `claude plugin install`, `gemini extensions install`, opencode (`XDG_CONFIG_HOME`), or openclaw (`OPENCLAW_WORKSPACE`) — those use their own paths. Default: `$CLAUDE_CONFIG_DIR` or `~/.claude`. `~` is expanded. |
| `--non-interactive` | Never prompt; use defaults. (Auto when stdin is not a TTY.) |
| `--no-color` | Disable ANSI colors. |
| `--list` | Print full agent matrix and exit. |
| `--force` | Re-run even if already installed. |
| `--uninstall` | Remove everything. See below. |
## Always-on rules
For agents without a hook system (Cursor, Windsurf, Cline, Copilot, and friends), the always-on path is a static rule file. Two ways:
```bash
# Drop rule files into the current repo
node cli/install.js --with-init
# Or pull the rule body straight in (manual)
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md \
> .cursor/rules/caveman.mdc # or .windsurf/rules/caveman.md, .clinerules/caveman.md, .github/copilot-instructions.md
```
`--with-init` writes the rule into every supported per-agent location it can detect (`.cursor/rules/`, `.windsurf/rules/`, `.clinerules/`, `.github/copilot-instructions.md`, `.opencode/AGENTS.md`, `AGENTS.md`). It also installs the OpenClaw workspace bootstrap (skill folder + SOUL.md marker block) when `~/.openclaw/workspace/` exists. Single source: [`src/rules/caveman-activate.md`](src/rules/caveman-activate.md).
## Verify
After install, three quick checks:
**1. See what got installed.**
```bash
node cli/install.js --list
```
You should see ~30 rows. Detected agents are marked. Anything you wanted but isn't marked → not detected (likely the binary isn't on `PATH`).
**2. Talk to Claude Code.**
Open Claude Code, type `/caveman`. Response should be terse fragments — "Got it. Caveman mode on." or similar. Try a real question: "What is closures in JS?" — answer should drop articles and read like grunts.
**3. Check the flag file.**
```bash
cat "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.caveman-active"
# expected output: full
```
If it's missing or empty, the SessionStart hook didn't fire. See troubleshooting below.
Statusline should show `[CAVEMAN]` (orange) at the bottom of Claude Code. After your first `/caveman-stats` run it appends a savings counter like `[CAVEMAN] ⛏ 12.4k`.
## Uninstall
```bash
npx -y github:JuliusBrussee/caveman -- --uninstall
```
What it removes:
- Caveman hook entries from `$CLAUDE_CONFIG_DIR/settings.json` (default `~/.claude/`; matched by the substring `caveman`).
- Hook files in `$CLAUDE_CONFIG_DIR/hooks/` (`caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js`, `caveman-config.js`, `caveman-statusline.{sh,ps1}`, plus the dir's `package.json` marker).
- The Claude Code plugin and the Gemini CLI extension (if installed).
- The opencode native plugin (`~/.config/opencode/plugins/caveman/`, the `plugin` and `mcp.caveman-shrink` entries from `opencode.json`, our skill/agent/command files, the caveman block from `AGENTS.md`, and the opencode flag file).
- The OpenClaw workspace skill folder and the marker-fenced block from `~/.openclaw/workspace/SOUL.md` (when present).
- The `.caveman-active` flag file.
What it does **not** remove:
- Skills installed via `npx skills add` — the `skills` CLI manages those. Run `npx skills remove caveman` (or use your IDE's skill manager).
- Per-repo rule files written by `--with-init` (`.cursor/rules/`, `.windsurf/rules/`, `.clinerules/`, `.github/copilot-instructions.md`, `.opencode/AGENTS.md`, `AGENTS.md`). Delete by hand if you want.
## Troubleshooting
**"Install script broke. What now?"**
Open your agent in this repo and say:
> "Read CLAUDE.md and INSTALL.md. Install caveman for me."
Agent read repo. Agent run install. Caveman make agent talk less — agent first job is install caveman to talk less. Snake eat tail.
Still broken? [Open an issue](https://github.com/JuliusBrussee/caveman/issues).
**"I ran the installer but Claude Code isn't talking caveman."**
1. Run `node cli/install.js --list` — confirm `claude` is on the detected list. If not, `claude` isn't on `PATH`. Fix that first.
2. Open `$CLAUDE_CONFIG_DIR/settings.json` (default `~/.claude/settings.json`) and look for `"hooks"` containing `caveman-activate.js` and `caveman-mode-tracker.js`. If missing, re-run with `--force`.
3. Check `$CLAUDE_CONFIG_DIR/.caveman-active` exists with content `full`. If not, the SessionStart hook silent-failed — check `$CLAUDE_CONFIG_DIR/hooks/` for the JS files and try `node $CLAUDE_CONFIG_DIR/hooks/caveman-activate.js < /dev/null` to see if it errors.
4. Restart Claude Code. The SessionStart hook only fires on session start, not mid-session.
**"Hooks failing on Windows."**
- Use `install.ps1`, not `install.sh`. Git Bash works for the shell version, but the hook side wires PowerShell counterparts (`caveman-statusline.ps1`).
- PowerShell 5.1 minimum. Check with `$PSVersionTable.PSVersion`.
- If `irm | iex` blocks on execution policy: `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` for the install session, then re-run.
- Long-running issues: see `docs/install-windows.md` in the repo for manual fallback.
**"My `settings.json` got mangled."**
The installer uses a JSONC-tolerant parser (`cli/lib/settings.js`) so comments and trailing commas don't crash the merge. It also runs `validateHookFields()` before every write so a malformed hook can't poison the file. If something still went wrong:
1. Check for a backup at `$CLAUDE_CONFIG_DIR/settings.json.bak` (installer writes one before any merge).
2. If no backup, restore from your shell history or version control.
3. File an issue with the broken `settings.json` content (redacted) — that file passing validation but breaking Claude Code is a bug we want to fix.
**"I'm in a managed env where I can't install hooks."**
Use the rule-file-only path. Hooks are Claude Code-specific; everything else works via static rule files:
```bash
# Just install for one agent, no Claude hooks
node cli/install.js --only cursor
# Or write rule files into the current repo only (no global state)
node cli/install.js --with-init --only cursor --only windsurf
```
This drops `.cursor/rules/caveman.mdc` (and friends) into your repo. No hooks, no global config, nothing outside the repo.
**"`npx skills add` errored on a profile slug."**
The profile slug must exist in [vercel-labs/skills](https://github.com/vercel-labs/skills). If a row in the table above 404s, the upstream profile was renamed or removed — open an issue, we'll update.
## Privacy
The installer doesn't phone home. It writes to:
- `$CLAUDE_CONFIG_DIR` (default `~/.claude/`) — hooks, flag file, `settings.json` merge.
- Each agent's own config location — Cursor's `.cursor/rules/`, Windsurf's `.windsurf/rules/`, opencode's `~/.config/opencode/`, etc.
- Your current working directory (only with `--with-init`) — repo-local rule files.
- `~/.openclaw/workspace/` (only with `--only openclaw` or `--with-init` when OpenClaw is detected) — the one `--with-init` side-effect outside the cwd.
No telemetry. No analytics. Run from a clone or via npx, the installer's own code makes no network calls — files are copied locally. One exception: run detached from any checkout (the rare curl-fallback path), it downloads the hook files from raw.githubusercontent.com pinned to an immutable release tag and verifies each against a SHA-256 manifest before wiring anything. Network requests also happen indirectly through the per-agent CLIs it shells out to — `claude plugin marketplace add`, `claude plugin install`, `gemini extensions install`, `npm view caveman-shrink`, and `npx -y skills add`. Each fetches from its own registry (Anthropic / GitHub / npm). Source: [`cli/install.js`](cli/install.js). After install: zero network calls, ever — full statement in [SECURITY.md](./SECURITY.md#privacy--telemetry).
---
Stuck? Open an issue: <https://github.com/JuliusBrussee/caveman/issues>
+256 -197
View File
@@ -1,110 +1,164 @@
<p align="center">
<img src="https://em-content.zobj.net/source/apple/391/rock_1faa8.png" width="120" />
<img src="docs/assets/caveman-logo-banner.png" alt="Caveman" width="720">
</p>
<h1 align="center">caveman</h1>
<p align="center">
<strong>why use many token when few do trick</strong>
</p>
<p align="center">
<a href="https://github.com/JuliusBrussee/caveman/stargazers"><img src="https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow" alt="Stars"></a>
<a href="https://github.com/JuliusBrussee/caveman/commits/main"><img src="https://img.shields.io/github/last-commit/JuliusBrussee/caveman?style=flat" alt="Last Commit"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/JuliusBrussee/caveman?style=flat" alt="License"></a>
Make your AI coding agent talk like a caveman.<br>
Same answers. <strong>65% fewer output tokens</strong> on prose,<br>
<strong>8.5%</strong> on <a href="#independently-measured-jetbrains-86-tasks">long-horizon agentic coding runs</a>. Brain still big. Mouth small.
</p>
<p align="center">
<a href="#install">Install</a>
<a href="#benchmarks">Benchmarks</a> •
<a href="#before--after">Before/After</a> •
<a href="#intensity-levels">Intensity Levels</a> •
<a href="#caveman-compress">Compress</a>
<a href="#why">Why</a>
<a href="https://trendshift.io/repositories/25391?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-25391" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/25391" alt="JuliusBrussee%2Fcaveman | Trendshift" width="250" height="55"/></a>
</p>
<p align="center">
<a href="https://github.com/JuliusBrussee/caveman/stargazers"><img src="https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow" alt="Stars"></a>
<a href="./INSTALL.md"><img src="https://img.shields.io/badge/works_with-30%2B_agents-orange?style=flat" alt="30+ agents"></a>
<a href="https://github.com/JuliusBrussee/caveman/commits/main"><img src="https://img.shields.io/github/last-commit/JuliusBrussee/caveman?style=flat" alt="Last commit"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/JuliusBrussee/caveman?style=flat" alt="License"></a>
<a href="https://skills.sh/JuliusBrussee/caveman"><img src="https://skills.sh/b/JuliusBrussee/caveman"></a>
</p>
<p align="center">
<a href="#before--after">See it</a> ·
<a href="#install">Install</a> ·
<a href="#pick-your-grunt">Levels</a> ·
<a href="#what-you-get">What you get</a> ·
<a href="#benchmarks">Benchmarks</a> ·
<a href="#the-whole-cave">Ecosystem</a> ·
<a href="#caveman-2">Caveman 2</a>
</p>
---
A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill/plugin and Codex plugin that makes agent talk like caveman — cutting **~75% of output tokens** while keeping full technical accuracy. Plus a companion tool that compresses your memory files to cut **~45% of input tokens** every session.
Based on the viral observation that caveman-speak dramatically reduces LLM token usage without losing technical substance. So we made it a one-line install.
Caveman is a skill/plugin for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Codex, Gemini, Cursor, Windsurf, Cline, Copilot, and 30+ other agents. Install once. Agent drops the filler and answers in tight caveman-speak, keeping code, commands, and errors byte-for-byte exact. You save output tokens on every reply, forever.
## Before / After
<table>
<tr>
<td width="50%">
<th width="50%">🗣️ Normal agent — 69 tokens</th>
<th width="50%"><img src="docs/assets/dancing-rock.svg" width="18" height="18" alt=""> Caveman agent — 19 tokens</th>
</tr>
<tr>
<td valign="top">
### 🗣️ Normal Claude (69 tokens)
> "The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I'd recommend using useMemo to memoize the object."
> The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I'd recommend using useMemo to memoize the object.
</td>
<td width="50%">
<td valign="top">
### 🪨 Caveman Claude (19 tokens)
> "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
</td>
</tr>
<tr>
<td>
<td valign="top">
### 🗣️ Normal Claude
> "Sure! I'd be happy to help you with that. The issue you're experiencing is most likely caused by your authentication middleware not properly validating the token expiry. Let me take a look and suggest a fix."
> Sure! I'd be happy to help you with that. The issue you're experiencing is most likely caused by your authentication middleware not properly validating the token expiry. Let me take a look and suggest a fix.
</td>
<td>
<td valign="top">
### 🪨 Caveman Claude
> "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
> Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:
</td>
</tr>
</table>
**Same fix. 75% less word. Brain still big.**
Same fix. Third of the words. Nothing technical lost.
**Sometimes too much caveman. Sometimes not enough:**
```
┌────────────────────────────────────────────┐
│ output tokens saved █████████ 65% │
│ input tokens saved ░░░░░░░░░ 0% │
│ technical accuracy █████████ 100% │
│ vibes █████████ OOG │
└────────────────────────────────────────────┘
```
<table>
<tr>
<td width="33%">
Caveman no make brain smaller. Caveman make *mouth* smaller. Shrinks what the agent **says**, not what it knows.
#### 🪶 Lite
That 65% is the prose number, measured on replies like the ones above. On a full agentic coding run, where most of the output is code and tool calls, it's [8.5%](#independently-measured-jetbrains-86-tasks). Same skill, different workload — mechanism below.
> "Your component re-renders because you create a new object reference each render. Inline object props fail shallow comparison every time. Wrap it in `useMemo`."
## Install
</td>
<td width="33%">
**One command. Finds every agent on your machine. Installs for each.**
#### 🪨 Full
```bash
# macOS · Linux · WSL · Git Bash
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash
```
> "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
```powershell
# Windows · PowerShell 5.1+
irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex
```
</td>
<td width="33%">
~30 seconds. Needs Node ≥18. Skips agents you no have. Safe to re-run.
#### 🔥 Ultra
Prefer one agent at a time? Each has its own path:
> "Inline obj prop → new ref → re-render. `useMemo`."
```bash
# Claude Code plugin
claude plugin marketplace add JuliusBrussee/caveman && claude plugin install caveman@caveman
</td>
</tr>
</table>
# Gemini CLI extension
gemini extensions install https://github.com/JuliusBrussee/caveman --consent
**Same answer. You pick how many word.**
# Cursor / Windsurf / Cline / Codex / 30+ more, via the skills registry
npx skills add JuliusBrussee/caveman -a cursor
```
The full per-agent matrix, all flags, dry-run, and uninstall live in **[INSTALL.md](./INSTALL.md)**.
> [!TIP]
> **Turn it on:** type `/caveman` or say *"talk like caveman"*. **Turn it off:** say *"normal mode"*. On Claude Code, Codex, and Gemini it's already on from message one. No command needed.
**Install broke?** Open your agent in this repo and say: *"Read CLAUDE.md and INSTALL.md, install caveman for me."* Agent read repo, agent fix own brain. Snake eat tail.
## Pick your grunt
Six levels. Switch anytime with `/caveman <level>`. Level sticks until you change it or the session ends.
| Level | Same sentence, shrunk |
|---|---|
| *normal agent* | You should wrap the object in `useMemo`, since a new reference is created on every render. |
| `lite` | Wrap object in `useMemo`. New ref created every render. |
| `full` *(default)* | New ref each render. Wrap object in `useMemo`. |
| `ultra` | New ref/render. `useMemo` it. |
| `wenyan` | New ref every render, so wrap in `useMemo` — rendered in classical Chinese, shorter still. |
> [!NOTE]
> **Speak your tongue.** Caveman keeps your language. Write Portuguese, caveman grunt Portuguese. Spanish, French, same. It compresses the *style*, never translates. `wenyan` mode is the exception on purpose: classical Chinese packs the most meaning per token.
## What you get
| Command | What it does |
|---|---|
| `/caveman [lite\|full\|ultra\|wenyan]` | Compress every reply. Level sticks for the session. |
| `/caveman-commit` | Conventional Commit messages, ≤50-char subject. Why over what. |
| `/caveman-review` | One-line PR comments: `L42: 🔴 bug: user null. Add guard.` |
| `/caveman-stats` | Real session token usage, lifetime savings, USD. Tweetable line with `--share`. |
| `/caveman-compress <file>` | Rewrite a memory file (like `CLAUDE.md`) into caveman-speak. Cuts ~46% input tokens **every session after**. Code, URLs, paths byte-preserved. |
| `caveman-shrink` | MCP middleware. Wraps any MCP server, compresses its tool descriptions. [npm](https://www.npmjs.com/package/caveman-shrink). |
| `cavecrew-*` | Caveman subagents (investigator, builder, reviewer). ~60% fewer tokens than vanilla, so main context lasts longer. |
> [!TIP]
> On Claude Code the statusline shows `[CAVEMAN] ⛏ 12.4k` — that's your lifetime tokens saved, updated on every `/caveman-stats`. Silence it with `CAVEMAN_STATUSLINE_SAVINGS=0`.
## Benchmarks
Real token counts from the Claude API ([reproduce it yourself](benchmarks/)):
Real token counts from the Claude API. Average **65% output reduction** across 10 **chat-style prompts** (range 2287%), measured against default verbose replies. Output tokens only, committed and reproducible in [`benchmarks/`](./benchmarks/) and [`evals/`](./evals/). This is one-question-one-answer, not a full agentic coding run — for that number, see [JetBrains](#independently-measured-jetbrains-86-tasks) below.
<!-- BENCHMARK-TABLE-START -->
| Task | Normal (tokens) | Caveman (tokens) | Saved |
|------|---------------:|----------------:|------:|
| Task | Normal | Caveman | Saved |
|------|-------:|--------:|------:|
| Explain React re-render bug | 1180 | 159 | 87% |
| Fix auth middleware token expiry | 704 | 121 | 83% |
| Set up PostgreSQL connection pool | 2347 | 380 | 84% |
@@ -116,178 +170,183 @@ Real token counts from the Claude API ([reproduce it yourself](benchmarks/)):
| Debug PostgreSQL race condition | 1200 | 232 | 81% |
| Implement React error boundary | 3454 | 456 | 87% |
| **Average** | **1214** | **294** | **65%** |
*Range: 22%87% savings across prompts.*
<!-- BENCHMARK-TABLE-END -->
> [!IMPORTANT]
> Caveman only affects output tokens — thinking/reasoning tokens are untouched. Caveman no make brain smaller. Caveman make *mouth* smaller. Biggest win is **readability and speed**, cost savings are a bonus.
> **Honest number warning.** Caveman only shrinks **output** tokens. Input and reasoning tokens are untouched, and the skill itself adds ~11.5k input tokens per turn. So whole-session savings run smaller than the output number, and on already-terse workloads they can go net-negative. The real win is **readability and speed**. Cost savings are the bonus. When caveman wins, when it loses, and how to measure it yourself: **[docs/HONEST-NUMBERS.md](./docs/HONEST-NUMBERS.md)**.
### Science back caveman up
### Independently measured: JetBrains, 86 tasks
A March 2026 paper ["Brevity Constraints Reverse Performance Hierarchies in Language Models"](https://arxiv.org/abs/2604.00025) found that constraining large models to brief responses **improved accuracy by 26 percentage points** on certain benchmarks and completely reversed performance hierarchies. Verbose not always better. Sometimes less word = more correct.
JetBrains ran the skill against [86 tasks from SkillsBench](https://blog.jetbrains.com/ai/2026/07/speak-to-ai-agents-like-cavemen-tosave-tokens/) in July 2026 — real coding work, auto-graded by each task's own tests, Claude Code on `claude-sonnet-5`, skill forced on for every reply.
## Install
| Workload | Output tokens saved | Measured by |
|---|---:|---|
| Chat-style prose | **65%** | us, table above |
| Agentic coding run | **8.5%** | JetBrains, 86 tasks |
```bash
npx skills add JuliusBrussee/caveman
```
Both numbers are real. They measure different workloads, and the gap is mechanical: caveman compresses narration and leaves code, diffs, tool calls, and error strings byte-exact. In a chat answer, narration is the whole reply. In an agentic run it's the thin layer between tool calls, so that's all there is to squeeze. An output-only skill has a low ceiling on work that is mostly not prose.
`npx skills` supports 40+ agents — Claude Code, GitHub Copilot, Cursor, Windsurf, Cline, and more. To install for a specific agent:
Pick the number that matches your workload:
```bash
npx skills add JuliusBrussee/caveman -a cursor
npx skills add JuliusBrussee/caveman -a copilot
npx skills add JuliusBrussee/caveman -a cline
npx skills add JuliusBrussee/caveman -a windsurf
```
- **Agent writes you prose** — explanations, review, docs, debugging walkthroughs → 65% territory.
- **Agent works a repo unattended** → single digits. Not zero, not 65%.
Or with Claude Code plugin system:
Quality was unaffected: across 86 auto-graded tasks the two arms were statistically indistinguishable. Small mouth, same brain — checked by someone who didn't ship it.
```bash
claude plugin marketplace add JuliusBrussee/caveman
claude plugin install caveman@caveman
```
Two things follow:
Codex:
- **Agentic bills are mostly input tokens**, which an output-only skill cannot touch by construction. `/caveman-compress` and `caveman-shrink` chip at that side; the skill alone never will.
- **The right number is your number.** JetBrains had to run a full paid benchmark to find out what caveman does on their stack. That's the job [Caveman 2](#caveman-2) exists to do — for yours, continuously.
1. Clone repo
2. Open Codex in repo
3. Run `/plugins`
4. Search `Caveman`
5. Install plugin
Turns out short isn't just cheaper. A March 2026 paper, [*Brevity Constraints Reverse Performance Hierarchies in Language Models*](https://arxiv.org/abs/2604.00025), tested 31 models and found that constraining large models to brief answers **improved accuracy by ~26 points** on some benchmarks. Sometimes less word = more correct.
Install once. Use in all sessions after that.
<details>
<summary><strong>caveman-compress receipts</strong> — real memory files, cutting input tokens forever</summary>
One rock. That it.
## Usage
Trigger with:
- `/caveman` or Codex `$caveman`
- "talk like caveman"
- "caveman mode"
- "less tokens please"
Stop with: "stop caveman" or "normal mode"
### Intensity Levels
Sometimes full caveman too much. Sometimes not enough. Now you pick:
| Level | Trigger | What it do |
|-------|---------|------------|
| **Lite** | `/caveman lite` or `$caveman lite` | Drop filler, keep grammar. Professional but no fluff |
| **Full** | `/caveman full` or `$caveman full` | Default caveman. Drop articles, fragments, full grunt |
| **Ultra** | `/caveman ultra` or `$caveman ultra` | Maximum compression. Telegraphic. Abbreviate everything |
Level stick until you change it or session end.
## What Caveman Do
| Thing | Caveman Do? |
|-------|------------|
| English explanation | 🪨 Caveman smash filler words |
| Code blocks | ✍️ Write normal (caveman not stupid) |
| Technical terms | 🧠 Keep exact (polymorphism stay polymorphism) |
| Error messages | 📋 Quote exact |
| Git commits & PRs | ✍️ Write normal |
| Articles (a, an, the) | 💀 Gone |
| Pleasantries | 💀 "Sure I'd be happy to" is dead |
| Hedging | 💀 "It might be worth considering" extinct |
## Why
```
┌─────────────────────────────────────┐
│ TOKENS SAVED ████████ 75% │
│ TECHNICAL ACCURACY ████████ 100%│
│ SPEED INCREASE ████████ ~3x │
│ VIBES ████████ OOG │
└─────────────────────────────────────┘
```
- **Faster response** — less token to generate = speed go brrr
- **Easier to read** — no wall of text, just the answer
- **Same accuracy** — all technical info kept, only fluff removed ([science say so](https://arxiv.org/abs/2604.00025))
- **Save money** — ~71% less output token = less cost
- **Fun** — every code review become comedy
## How It Work
Caveman not dumb. Caveman **efficient**.
Normal LLM waste token on:
- "I'd be happy to help you with that" (8 wasted tokens)
- "The reason this is happening is because" (7 wasted tokens)
- "I would recommend that you consider" (7 wasted tokens)
- "Sure, let me take a look at that for you" (10 wasted tokens)
Caveman say what need saying. Then stop.
## Caveman Compress
Caveman makes Claude *speak* with fewer tokens. **Caveman Compress** makes Claude *read* fewer tokens.
Your `CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs you tokens every single time you open a project. Caveman Compress rewrites those files into caveman-speak so Claude reads less — without you losing the human-readable original.
```
/caveman-compress CLAUDE.md
```
```
CLAUDE.md ← compressed (Claude reads this every session — fewer tokens)
CLAUDE.original.md ← human-readable backup (you read and edit this)
```
### How it works
A Python pipeline that shells out to `claude --print` for the actual compression, then validates the result locally — no tokens wasted on checking.
```
detect file type (local) → compress with Claude (1 call) → validate (local)
if errors: targeted fix (1 call, cherry-pick only)
retry up to 2×, restore original on failure
```
### What's preserved exactly
Code blocks, inline code, URLs, file paths, commands, headings, table structure, dates, version numbers — anything technical passes through untouched. Only natural language prose gets compressed.
### Compress benchmarks
<br>
| File | Original | Compressed | Saved |
|------|----------:|----------:|------:|
|---|---:|---:|---:|
| `claude-md-preferences.md` | 706 | 285 | **59.6%** |
| `project-notes.md` | 1145 | 535 | **53.3%** |
| `claude-md-project.md` | 1122 | 687 | **38.8%** |
| `claude-md-project.md` | 1122 | 636 | **43.3%** |
| `todo-list.md` | 627 | 388 | **38.1%** |
| `mixed-with-code.md` | 888 | 574 | **35.4%** |
| **Average** | **898** | **494** | **45%** |
| `mixed-with-code.md` | 888 | 560 | **36.9%** |
| **Average** | **898** | **481** | **46%** |
### Full-circle token savings
Every session after, that file loads ~46% smaller. Input tokens saved forever, not just one reply.
| Tool | What it cuts | Savings |
|------|-------------|---------|
| **caveman** | Output tokens (Claude's responses) | ~65% |
| **caveman-compress** | Input tokens (memory files loaded per session) | ~45% |
| **Both together** | The whole conversation | Output + input both shrunk |
</details>
See the full [caveman-compress README](caveman-compress/README.md) for install, usage, and validation details.
## The whole cave
## Star This Repo
<table>
<tr><td>
If caveman save you mass token, mass money — leave mass star. ⭐
### <img src="docs/assets/dancing-rock.svg" width="20" height="20" alt=""> Want the whole agent, not just its mouth? → caveman-code
[![Star History Chart](https://api.star-history.com/svg?repos=JuliusBrussee/caveman&type=Date)](https://star-history.com/#JuliusBrussee/caveman&Date)
This skill shrinks what an agent **says**. **[caveman-code](https://github.com/JuliusBrussee/caveman-code)** shrinks **everything** — a full terminal coding agent, caveman top to bottom. **~2× fewer tokens than Codex** on identical tasks. 20+ providers, plan mode, autopilot goal loop, MIT.
## Also by Julius Brussee
```bash
npm install -g @juliusbrussee/caveman-code
```
- **[Blueprint](https://github.com/JuliusBrussee/blueprint)** — specification-driven development for Claude Code. Natural language → blueprints → parallel builds → working software.
- **[Revu](https://github.com/JuliusBrussee/revu-swift)** — local-first macOS study app with FSRS spaced repetition, decks, exams, and study guides. [revu.cards](https://revu.cards)
[**▶ Try caveman-code →**](https://github.com/JuliusBrussee/caveman-code)
## License
</td></tr>
</table>
Five tools, one idea: **agent do more with less.**
| Repo | What it shrinks |
|------|------|
| [**caveman**](https://github.com/JuliusBrussee/caveman) *(you here)* | What the agent **says** |
| [**caveman-code**](https://github.com/JuliusBrussee/caveman-code) | The **whole agent**, end to end |
| [**cavemem**](https://github.com/JuliusBrussee/cavemem) | What the agent **remembers**, across sessions |
| [**cavekit**](https://github.com/JuliusBrussee/cavekit) | The **build loop** — spec-driven, no guessing |
| [**cavegemma**](https://github.com/JuliusBrussee/finetune-caveman) | The compression **baked into weights** (Gemma fine-tune) |
<details>
<summary><strong>Also: five sibling skills, one install</strong></summary>
<br>
[**JuliusBrussee/skills**](https://github.com/JuliusBrussee/skills) — works in Claude Code, Cursor, Gemini, Cline, Copilot, 40+ agents:
| Skill | What |
|------|------|
| [**caveman**](https://github.com/JuliusBrussee/skills/tree/main/skills/caveman) | This one. Speak less, say more. |
| [**grill-me**](https://github.com/JuliusBrussee/skills/tree/main/skills/grill-me) | Agent grills your plan *before* you build the wrong thing. |
| [**interface-kit**](https://github.com/JuliusBrussee/skills/tree/main/skills/interface-kit) | Build UI that looks good, loads fast, works for everyone. |
| [**junior-to-senior**](https://github.com/JuliusBrussee/skills/tree/main/skills/junior-to-senior) | Adversarial review pass. Junior output in, senior output out. |
| [**loop-factory**](https://github.com/JuliusBrussee/skills/tree/main/skills/loop-factory) | Spec-driven task loop — inbox → active → archive. |
```bash
npx skills@latest add JuliusBrussee/skills
```
</details>
<details>
<summary><strong>🦞 Teach the lobster brevity — OpenClaw integration</strong></summary>
<br>
[**OpenClaw**](https://openclaw.ai) is a self-host gateway: one box, many agents inside, wired to Slack / Discord / iMessage / Telegram. Lobster strong. Lobster smart. Lobster also talk a lot.
Same installer, scoped to one agent:
```bash
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash -s -- --only openclaw
```
Two things happen, no more: a caveman skill lands in the workspace, and a tiny marker-fenced block is appended to `SOUL.md` (OpenClaw injects it every turn, so the lobster is terse from message one — no `/caveman` per session). Custom path? `OPENCLAW_WORKSPACE=/your/path`. Uninstall with the same line plus `--uninstall`; your other workspace content stays untouched. Lobster claw still sharp. Lobster mouth now small.
</details>
## Caveman 2
**Caveman make token small. Caveman 2 make it _provable_.**
Today's savings numbers (including `/caveman-stats`) are local estimates. Caveman 2 measures and verifies them across a whole team — real receipts, real dashboard, real proof the tokens went down. Building it now.
[The JetBrains result](#independently-measured-jetbrains-86-tasks) is the argument for it. 65% and 8.5% are both correct, and neither one is *your* number — one harness, one model, one task set, and your stack is none of those. The fix is not a better README claim, ours or anyone's. It's a baseline on your own traffic and a receipt at the end of the month.
[**Join the waitlist → caveman.so**](https://caveman.so)
## How it works
1. Install drops a skill file into your agent.
2. Skill tells agent: drop filler, keep substance, use fragments — but never touch code, commands, or errors.
3. On Claude Code, a hook writes a tiny flag file each session, so the agent talks caveman from message one without `/caveman`.
4. `/caveman-stats` reads your session log, counts tokens saved, writes the number to your statusline.
5. `/caveman-compress` rewrites memory files (like `CLAUDE.md`) so every future session starts with a smaller context. Save tokens forever, not just once.
Hook architecture, file ownership, and CI sync are documented for maintainers in [CLAUDE.md](./CLAUDE.md).
## Privacy
Caveman no phone home. No telemetry, no analytics, no accounts, no backend. After install, zero network calls — the skill is a prompt, the hooks are local scripts, and `/caveman-stats` reads a log already on your disk. Install-time fetches (GitHub plus your agents' own registries) are spelled out in [SECURITY.md](./SECURITY.md#privacy--telemetry).
## Sponsors
Caveman free forever. Sponsors keep the rock sharp.
<p align="center">
<a href="https://www.atlascloud.ai">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/atlas-cloud-dark.svg">
<img src="docs/assets/atlas-cloud.svg" alt="Atlas Cloud" height="32">
</picture>
</a>
</p>
<p align="center">
<a href="https://www.atlascloud.ai"><strong>Atlas Cloud</strong></a> — full-modal AI inference platform, one API.
</p>
<p align="center">
<a href="https://github.com/sponsors/JuliusBrussee"><strong>Want your rock here? → Sponsor caveman</strong></a>
</p>
## Star this repo
Caveman save you token, save you money. Star cost zero. Fair trade. ⭐
[![Star History Chart](./docs/assets/star-history.png)](https://star-history.com/#JuliusBrussee/caveman&Date)
---
<sub>
<strong>Docs:</strong>
<a href="./INSTALL.md">Install matrix</a> ·
<a href="./docs/HONEST-NUMBERS.md">Honest numbers</a> ·
<a href="./CONTRIBUTING.md">Contributing</a> ·
<a href="./CLAUDE.md">Maintainer guide</a> ·
<a href="https://github.com/JuliusBrussee/caveman/issues">Issues</a>
<br>
<strong>Also by Julius Brussee:</strong>
<a href="https://github.com/JuliusBrussee/revu-swift">Revu</a> — local-first macOS study app with FSRS spaced repetition (<a href="https://revu.cards">revu.cards</a>)
<br><br>
MIT — free like mass mammoth on open plain.
</sub>
+46
View File
@@ -0,0 +1,46 @@
# Security Policy
## Supported Versions
Only the latest stable release builds are supported with security patches.
## Reporting a Vulnerability
If you identify a security vulnerability in caveman (such as arbitrary shell execution, workspace folder escapes, token/credentials hijack via prompts, or malicious JSON parsing flaws in extension settings), please do **not** open a public issue.
Please report vulnerabilities privately by emailing the maintainers or using [GitHub's private vulnerability reporting](https://github.com/JuliusBrussee/caveman/security/advisories/new).
## Privacy & Telemetry
**Caveman has no telemetry. Zero.** No analytics, no crash reporting, no phone-home, no accounts, no API keys collected. There is no caveman backend — nothing to send data to.
### After install: zero network calls
Once installed, nothing in caveman touches the network. Verified against the code (audit it yourself — every file is in this repo):
- **The skill itself** (`skills/caveman/SKILL.md`) is a markdown prompt. It contains no code.
- **The hooks** (`src/hooks/*.js`, statusline scripts) are local Node/shell scripts. They read and write local files only (flag file, session log, statusline savings file). No `http`/`https`/`fetch` anywhere in them.
- **`/caveman-stats`** reads Claude Code's session JSONL from your local disk and prints counts. USD figures come from pricing constants hardcoded in the script. Nothing leaves your machine.
- **`caveman-shrink`** (MCP middleware) spawns the MCP server *you* configure, locally, and compresses its output in-process. It makes no network calls of its own; any network activity belongs to the server you wrapped.
- **`/caveman-compress`** rewrites a local file you name and saves a `.original.md` backup next to it. Local file I/O only.
### At install time: exactly these network requests, nothing else
- `curl … install.sh | bash` (or `irm … install.ps1 | iex`) fetches the shim from raw.githubusercontent.com, which delegates to `npx -y github:JuliusBrussee/caveman` — npm fetches this repo from GitHub.
- The installer shells out to per-agent CLIs which fetch from their own registries: `claude plugin marketplace add` / `claude plugin install` (Anthropic/GitHub), `gemini extensions install`, `npm view caveman-shrink`, `npx -y skills add` (npm).
- **Rare fallback:** if the installer runs detached from a repo checkout, it downloads the hook files from raw.githubusercontent.com **pinned to an immutable release tag** and verifies each against a published SHA-256 manifest before wiring anything (a mismatch aborts). From a normal clone or npx run, files are copied locally — offline installs work.
Nothing is uploaded in any of these steps. Details and the full list of paths written: [INSTALL.md → Privacy](./INSTALL.md#privacy).
### What stays on your machine
Everything. Skill/rule files in your agents' config dirs, the mode flag file and merged `settings.json` under `~/.claude/` (or `$CLAUDE_CONFIG_DIR`), the lifetime-savings statusline file, and `.original.md` backups from `/caveman-compress`. Uninstall removes what the installer wrote: `npx -y github:JuliusBrussee/caveman -- --uninstall`.
### Enterprise / air-gapped use
Caveman is self-contained after install and fully functional offline. There is no license server, no external backend, and no data flow to audit beyond the install-time fetches above. For air-gapped environments, clone the repo internally and run the installer from the clone — no network needed.
## About scanner warnings
- **Windows Defender / SmartScreen on `install.ps1` (#383):** piping a script from the internet into `iex` and writing into agent config directories matches generic dropper heuristics, so AV tools may warn. The script is short and readable in this repo; the hook files it installs are SHA-256-verified against the pinned release manifest. If you'd rather not pipe-to-shell, clone the repo and run `node cli/install.js` — same result, fully inspectable first.
- **Snyk "High Risk" on `caveman-compress` (#28):** the compress skill instructs the agent to read a file you name, rewrite it in place, and save a backup. In-place file rewriting is exactly what generic risk scoring flags. It is a real capability, not hidden — but there is no network access, no shell execution beyond what's documented in [`skills/caveman-compress/`](./skills/caveman-compress/), and it never touches files you didn't name.
+47
View File
@@ -0,0 +1,47 @@
---
name: cavecrew-builder
description: >
Surgical 1-2 file edit. Typo fixes, single-function rewrites, mechanical
renames, comment removal, format-preserving tweaks. Hard refuses 3+ file
scope. Returns caveman diff receipt. Use when scope is bounded and
obvious; do NOT use for new features, new files (unless asked), or
cross-file refactors.
tools: [Read, Edit, Write, Grep, Glob]
---
Caveman-ultra. Drop articles/filler. Code/paths exact, backticked. No narration.
## Scope
1 file ideal. 2 OK. 3+ → refuse.
Edit existing only (new file iff user asked).
No new abstractions. No drive-by refactors. No comment additions.
No `Bash` available — cannot shell out, cannot push, cannot delete.
## Workflow
1. `Read` target(s). Never edit blind.
2. `Edit` smallest diff that work.
3. Re-`Read` to verify.
4. Return receipt.
## Output (receipt)
```
<path:line-range> — <change ≤10 words>.
<path:line-range> — <change ≤10 words>.
verified: <re-read OK | mismatch @ path:line>.
```
Diff is the artifact. Receipt is the proof. No exploration story.
## Refusals (terminal lines)
3+ files → `too-big. split: <n one-line tasks>.`
Destructive needed → `needs-confirm. op: <command>.`
Spec ambiguous → `ambiguous. ask: <one question>.`
Tests fail post-edit, can't fix in scope → `regressed. revert path:line. cause: <fragment>.`
## Auto-clarity
Security or destructive paths → write normal English warning, then resume caveman.
+57
View File
@@ -0,0 +1,57 @@
---
name: cavecrew-investigator
description: >
Read-only code locator. Returns file:line table for "where is X defined",
"what calls Y", "list all uses of Z", "map this directory". Output is
caveman-compressed so the main thread eats ~60% fewer tokens than
vanilla Explore. Refuses to suggest fixes.
tools: [Read, Grep, Glob, Bash]
model: haiku
---
Caveman-ultra. Drop articles/filler/hedging. Code/symbols/paths exact, backticked. Lead with answer.
## Job
Locate. Report. Stop. Never edit, never propose fix.
## Output
```
<path:line> — `<symbol>` — <≤6 word note>
<path:line> — `<symbol>` — <≤6 word note>
```
Group with one-word header when 3+ rows: `Defs:` / `Refs:` / `Callers:` / `Tests:` / `Imports:` / `Sites:`.
Single hit → one line, no header.
Zero hits → `No match.`
Last line → totals: `2 defs, 5 refs.` (omit if 0 or 1).
## Tools
`Grep` for symbols/strings. `Glob` for paths. `Read` only specific ranges. `Bash` for `git log -S`/`git grep`/`find` when faster.
## Refusals
Asked to fix → `Read-only. Spawn cavecrew-builder.`
Asked to design → `Read-only. Spawn cavecrew-builder or use main thread.`
## Auto-clarity
Security warnings, destructive ops → write normal English. Resume after.
## Example
Q: "where symlink-safe flag write?"
```
Defs:
- hooks/caveman-config.js:81 — `safeWriteFlag` — atomic write w/ O_NOFOLLOW
- hooks/caveman-config.js:160 — `readFlag` — paired reader
Callers:
- hooks/caveman-mode-tracker.js:33,87
- hooks/caveman-activate.js:40
Tests:
- tests/test_symlink_flag.js — 12 cases
2 defs, 3 callers, 1 test file.
```
+48
View File
@@ -0,0 +1,48 @@
---
name: cavecrew-reviewer
description: >
Diff/branch/file reviewer. One line per finding, severity-tagged, no praise,
no scope creep. Output format `path:line: <emoji> <severity>: <problem>. <fix>.`
Use for "review this PR", "review my diff", "audit this file". Skips
formatting nits unless they change meaning.
tools: [Read, Grep, Bash]
model: haiku
---
Caveman-ultra. Findings only. No "looks good", no "I'd suggest", no preamble.
## Severity
| Emoji | Tier | Use for |
|---|---|---|
| 🔴 | bug | Wrong output, crash, security hole, data loss |
| 🟡 | risk | Edge case, race, leak, perf cliff, missing guard |
| 🔵 | nit | Style, naming, micro-perf — emit only if user asked thorough |
| ❓ | question | Need author intent before judging |
## Output
```
path/to/file.ts:42: 🔴 bug: token expiry uses `<` not `<=`. Off-by-one allows expired tokens 1 tick.
path/to/file.ts:118: 🟡 risk: pool not closed on error path. Add `try/finally`.
src/utils.ts:7: ❓ question: why duplicate `.trim()` here?
totals: 1🔴 1🟡 1❓
```
Zero findings → `No issues.`
File order, ascending line numbers within file.
## Boundaries
- Review only what's in front of you. No "while we're here".
- No big-refactor proposals.
- Need more context → append `(see L<n> in <file>)`. Don't guess.
- Formatting nits skipped unless they change meaning.
## Tools
`Bash` only for `git diff`/`git log -p`/`git show`. No mutating commands.
## Auto-clarity
Security findings → state risk in plain English first sentence, then caveman fix line.
+14 -5
View File
@@ -13,14 +13,23 @@ from pathlib import Path
import anthropic
# Load .env.local from repo root if it exists
# The only env var this benchmark needs: the anthropic SDK reads it in
# anthropic.Anthropic(). Read it — and ONLY it — from repo-root .env.local.
# Deliberately narrow (issue #528): the old loader setdefault'ed EVERY key in
# .env.local into os.environ, which security scanners rightly flag as an
# exfiltration surface. Nothing else from the file is ever read or exported.
_API_KEY_VAR = "ANTHROPIC_API_KEY"
_env_file = Path(__file__).parent.parent / ".env.local"
if _env_file.exists():
if _API_KEY_VAR not in os.environ and _env_file.exists():
for line in _env_file.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
if line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
if key.strip() == _API_KEY_VAR:
os.environ.setdefault(_API_KEY_VAR, value.strip())
break
SCRIPT_VERSION = "1.0.0"
SCRIPT_DIR = Path(__file__).parent
-158
View File
@@ -1,158 +0,0 @@
#!/usr/bin/env python3
"""
Caveman Memory Orchestrator
Usage:
python memory/compress.py <filepath>
"""
import subprocess
import sys
from pathlib import Path
from typing import List
from .detect import should_compress
from .validate import validate
MAX_RETRIES = 2
# ---------- Claude Calls ----------
def call_claude(prompt: str) -> str:
try:
result = subprocess.run(
["claude", "--print"],
input=prompt,
text=True,
capture_output=True,
check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
def build_compress_prompt(original: str) -> str:
return f"""
Compress this markdown into caveman format.
STRICT RULES:
- Do NOT modify anything inside ``` code blocks
- Do NOT modify anything inside inline backticks
- Preserve ALL URLs exactly
- Preserve ALL headings exactly
- Preserve file paths and commands
Only compress natural language.
TEXT:
{original}
"""
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
errors_str = "\n".join(f"- {e}" for e in errors)
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
CRITICAL RULES:
- DO NOT recompress or rephrase the file
- ONLY fix the listed errors — leave everything else exactly as-is
- The ORIGINAL is provided as reference only (to restore missing content)
- Preserve caveman style in all untouched sections
ERRORS TO FIX:
{errors_str}
HOW TO FIX:
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
- Do not touch any section not mentioned in the errors
ORIGINAL (reference only):
{original}
COMPRESSED (fix this):
{compressed}
Return ONLY the fixed compressed file. No explanation.
"""
# ---------- Core Logic ----------
def compress_file(filepath: Path) -> bool:
print(f"📄 Processing: {filepath}")
if not should_compress(filepath):
print("⚠️ Skipping (not natural language)")
return False
original_text = filepath.read_text(errors="ignore")
backup_path = filepath.with_name(filepath.stem + ".original.md")
# Step 1: Compress
print("🧠 Compressing with Claude...")
compressed = call_claude(build_compress_prompt(original_text))
# Save original as backup, write compressed to original path
backup_path.write_text(original_text)
filepath.write_text(compressed)
# Step 2: Validate + Retry
for attempt in range(MAX_RETRIES):
print(f"\n🔍 Validation attempt {attempt + 1}")
result = validate(backup_path, filepath)
if result.is_valid:
print("✅ Validation passed")
break
print("❌ Validation failed:")
for err in result.errors:
print(f" - {err}")
if attempt == MAX_RETRIES - 1:
# Restore original on failure
filepath.write_text(original_text)
backup_path.unlink(missing_ok=True)
print("❌ Failed after retries — original restored")
return False
print("🛠 Fixing with Claude...")
compressed = call_claude(
build_fix_prompt(original_text, compressed, result.errors)
)
filepath.write_text(compressed)
return True
# ---------- Main ----------
def main():
if len(sys.argv) != 2:
print("Usage: python memory/compress.py <filepath>")
sys.exit(1)
filepath = Path(sys.argv[1])
if not filepath.exists():
print(f"❌ File not found: {filepath}")
sys.exit(1)
success = compress_file(filepath)
if success:
sys.exit(0)
else:
sys.exit(2)
if __name__ == "__main__":
main()
BIN
View File
Binary file not shown.
-72
View File
@@ -1,72 +0,0 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Slash token usage ~75% by speaking like caveman
while keeping full technical accuracy. Use when user says "caveman mode", "talk like caveman",
"use caveman", "less tokens", "be brief", or invokes /caveman. Also auto-triggers
when token efficiency is requested.
---
# Caveman Mode
## Core Rule
Respond like smart caveman. Cut articles, filler, pleasantries. Keep all technical substance.
## Grammar
- Drop articles (a, an, the)
- Drop filler (just, really, basically, actually, simply)
- Drop pleasantries (sure, certainly, of course, happy to)
- Short synonyms (big not extensive, fix not "implement a solution for")
- No hedging (skip "it might be worth considering")
- Fragments fine. No need full sentence
- Technical terms stay exact. "Polymorphism" stays "polymorphism"
- Code blocks unchanged. Caveman speak around code, not in code
- Error messages quoted exact. Caveman only for explanation
## Pattern
```
[thing] [action] [reason]. [next step].
```
Not:
> Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by...
Yes:
> Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:
## Examples
**User:** Why is my React component re-rendering?
**Normal (69 tokens):** "The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I'd recommend using useMemo to memoize the object."
**Caveman (19 tokens):** "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
---
**User:** How do I set up a PostgreSQL connection pool?
**Caveman:**
```
Use `pg` pool:
```
```js
const pool = new Pool({
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
})
```
```
max = concurrent connections. Keep under DB limit. idleTimeout kill stale conn.
```
## Boundaries
- Code: write normal. Caveman English only
- Git commits: normal
- PR descriptions: normal
- User say "stop caveman" or "normal mode": revert immediately
Executable
+1600
View File
File diff suppressed because it is too large Load Diff
+318
View File
@@ -0,0 +1,318 @@
// caveman → OpenClaw install / uninstall helper.
//
// OpenClaw is a self-hosted gateway that orchestrates Claude Code, Codex,
// Pi, OpenCode, and others. It has its own workspace + skills system at
// ~/.openclaw/workspace/. Skills there appear in a compact list and are
// loaded on-demand by the model — they are NOT injected as system prompt
// each turn. The bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, MEMORY.md)
// ARE injected each turn under "Project Context", subject to a 12K-per-file
// and 60K-total cap.
//
// To make caveman always-on through OpenClaw, we do two writes:
// 1. Drop a copy of skills/caveman/SKILL.md into <workspace>/skills/caveman/
// with OpenClaw-required frontmatter (`version`, `always: true`) merged
// in. Makes the skill discoverable via `openclaw skills list` and lets
// the orchestrated agent `read` it on demand.
// 2. Append a tiny marker-fenced bootstrap snippet to <workspace>/SOUL.md
// pointing the agent at the skill. SOUL.md is auto-injected each turn,
// so this is what actually drives always-on behavior.
//
// Idempotent on both writes. Uninstall removes the skill folder and strips
// the marker block from SOUL.md while preserving any user-authored content.
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const SKILL_NAME = 'caveman';
const SKILL_VERSION = '1.0.0';
const MARK_BEGIN = '<!-- caveman-begin -->';
const MARK_END = '<!-- caveman-end -->';
const SOUL_FILE = 'SOUL.md';
function resolveWorkspace(env = process.env) {
if (env.OPENCLAW_WORKSPACE) return path.resolve(env.OPENCLAW_WORKSPACE);
return path.join(os.homedir(), '.openclaw', 'workspace');
}
function readIfExists(p) {
try { return fs.readFileSync(p, 'utf8'); } catch (_) { return null; }
}
// ── Frontmatter helpers ───────────────────────────────────────────────────
// Lightweight YAML merge — we only need to insert `version` and `always` if
// they're absent. Avoids pulling in a YAML dep for a job this small. The
// caveman SKILL.md uses block-scalar `description: >`, which a naive split
// would mangle — but since we're only ever appending top-level keys (never
// editing existing ones), a string-prepend after the leading `---\n` is safe.
function splitFrontmatter(src) {
if (!src.startsWith('---\n') && !src.startsWith('---\r\n')) {
return { frontmatter: '', body: src };
}
const after = src.slice(src.indexOf('\n') + 1);
const endRe = /(^|\n)---\s*(\r?\n|$)/;
const m = endRe.exec(after);
if (!m) return { frontmatter: '', body: src };
const fmEnd = m.index + (m[1] ? 1 : 0);
const fm = after.slice(0, fmEnd);
const rest = after.slice(m.index + m[0].length);
return { frontmatter: fm, body: rest };
}
function frontmatterHasKey(fm, key) {
const re = new RegExp('(^|\\n)' + key + '\\s*:', 'i');
return re.test(fm);
}
// `opts.version` defaults to SKILL_VERSION (the '1.0.0' fallback) when the
// caller doesn't have a better one on hand — cli/install.js threads through
// PINNED_REF (its release-tag source of truth) instead so the two never
// drift. `opts.always` defaults to true (existing behavior); pass `false`
// (from --no-always) to omit the `always: true` key entirely — the skill
// then loads on demand instead of always-on.
function mergeOpenclawFrontmatter(src, opts = {}) {
const version = opts.version || SKILL_VERSION;
const always = opts.always !== false;
const { frontmatter, body } = splitFrontmatter(src);
const additions = [];
if (!frontmatterHasKey(frontmatter, 'name')) additions.push(`name: ${SKILL_NAME}`);
if (!frontmatterHasKey(frontmatter, 'version')) additions.push(`version: ${version}`);
if (always && !frontmatterHasKey(frontmatter, 'always')) additions.push('always: true');
if (additions.length === 0 && frontmatter) return src;
const fmBody = (frontmatter ? frontmatter.trimEnd() + '\n' : '') + additions.join('\n') + (additions.length ? '\n' : '');
return '---\n' + fmBody + '---\n' + body;
}
// ── Bootstrap snippet load ────────────────────────────────────────────────
function loadBootstrapSnippet(repoRoot) {
if (repoRoot) {
const p = path.join(repoRoot, 'src', 'rules', 'caveman-openclaw-bootstrap.md');
const body = readIfExists(p);
if (body) return body.endsWith('\n') ? body : body + '\n';
}
// Standalone fallback (curl|node case where there's no repo on disk).
// Keep this in sync with src/rules/caveman-openclaw-bootstrap.md.
return [
MARK_BEGIN,
'## Caveman mode (always on)',
'',
'Respond terse like smart caveman. All technical substance stay. Only fluff die.',
'',
"The full ruleset and intensity levels live in this workspace's caveman skill:",
'',
' skills/caveman/SKILL.md',
'',
'Default intensity: `full`. Switch with `/caveman lite|full|ultra|wenyan`.',
'Stop with: "stop caveman" / "normal mode" / "deactivate caveman".',
'',
'Auto-Clarity: drop caveman for security warnings, irreversible action',
'confirmations, multi-step sequences where fragments risk misread, or when',
'user is confused or repeating. Resume after.',
'',
'Boundaries: code, commit messages, and PR descriptions stay normal prose.',
MARK_END,
'',
].join('\n');
}
function loadSkillBody(repoRoot) {
if (!repoRoot) return null;
return readIfExists(path.join(repoRoot, 'skills', 'caveman', 'SKILL.md'));
}
// ── SOUL.md marker-block append/strip ─────────────────────────────────────
//
// Damage tolerance (#596): a stray or truncated marker (interrupted write,
// partial user edit) used to chain into data loss — append saw "no complete
// block" and added a SECOND block; strip then cut from the FIRST begin to the
// FIRST end, which spanned all user content between the stray marker and the
// appended block. The scan below pairs each begin with the nearest end BEFORE
// the next begin; an unpaired marker is removed as just the marker itself,
// never as a span over user content.
function stripAllBootstrapBlocks(text) {
let result = '';
let found = false;
let i = 0;
while (i < text.length) {
const b = text.indexOf(MARK_BEGIN, i);
if (b === -1) { result += text.slice(i); break; }
result += text.slice(i, b);
found = true;
const nextB = text.indexOf(MARK_BEGIN, b + MARK_BEGIN.length);
const e = text.indexOf(MARK_END, b + MARK_BEGIN.length);
if (e !== -1 && (nextB === -1 || e < nextB)) {
i = e + MARK_END.length; // well-formed block — drop begin..end inclusive
} else {
i = b + MARK_BEGIN.length; // orphan begin — drop only the marker itself
}
// Collapse the blank-line scar around the cut (same cosmetic rule the
// old single-cut code applied): keep at most one newline on each side.
result = result.replace(/\n+$/, '\n');
const lead = /^\n+/.exec(text.slice(i));
if (lead) i += lead[0].length - (result ? 1 : 0);
}
// Orphan end markers (begin already gone or never written) — drop marker only.
while (result.includes(MARK_END)) { found = true; result = result.replace(MARK_END, ''); }
return { next: result, found };
}
function appendBootstrapToSoul(soulPath, snippet) {
const existing = readIfExists(soulPath);
const count = (s, sub) => s.split(sub).length - 1;
let base = existing;
let repaired = false;
if (existing) {
const nb = count(existing, MARK_BEGIN);
const ne = count(existing, MARK_END);
if (nb === 1 && ne === 1 && existing.indexOf(MARK_END) > existing.indexOf(MARK_BEGIN)) {
return { changed: false, reason: 'already present' };
}
if (nb > 0 || ne > 0) {
// Damaged markers — strip them safely first, then append one clean block.
base = stripAllBootstrapBlocks(existing).next;
repaired = true;
}
}
let next;
if (base && base.length) {
const sep = base.endsWith('\n\n') ? '' : (base.endsWith('\n') ? '\n' : '\n\n');
next = base + sep + snippet;
} else {
next = snippet;
}
fs.writeFileSync(soulPath, next, { mode: 0o644 });
return repaired ? { changed: true, repaired: true } : { changed: true };
}
function stripBootstrapFromSoul(soulPath) {
const existing = readIfExists(soulPath);
if (!existing) return { changed: false, reason: 'no SOUL.md' };
const { next: stripped, found } = stripAllBootstrapBlocks(existing);
if (!found) return { changed: false, reason: 'no marker block' };
let next = stripped.trimEnd();
next = next ? next + '\n' : '';
if (next === '') {
// SOUL.md only contained our block — remove the file so OpenClaw doesn't
// bootstrap an empty section every turn.
try { fs.unlinkSync(soulPath); } catch (_) {}
return { changed: true, removed: true };
}
fs.writeFileSync(soulPath, next, { mode: 0o644 });
return { changed: true };
}
// ── Public API ────────────────────────────────────────────────────────────
// `version` — bare semver stamped into the skill frontmatter; defaults to
// SKILL_VERSION if the caller doesn't pass one (see mergeOpenclawFrontmatter).
// `always` — default true (existing behavior). Pass false (--no-always) to
// skip the `always: true` frontmatter key AND the SOUL.md bootstrap append,
// so the skill installs load-on-demand instead of always-on.
function installOpenclaw({ workspace, repoRoot, dryRun = false, force = false, log = noopLog(), version, always = true } = {}) {
const ws = workspace || resolveWorkspace();
const skillBody = loadSkillBody(repoRoot);
if (!skillBody) {
log.warn(' openclaw install requires the caveman repo on disk (skills/caveman/SKILL.md missing).');
log.note(' Re-run from a clone or via `npx -y github:JuliusBrussee/caveman -- --only openclaw`.');
return { ok: false, reason: 'repo not available' };
}
const snippet = loadBootstrapSnippet(repoRoot);
if (!fs.existsSync(ws)) {
if (!force) {
log.warn(` openclaw workspace not found at ${ws}.`);
log.note(' Either install OpenClaw (https://openclaw.ai) and re-run, or pass --force to mkdir.');
return { ok: false, reason: 'workspace missing' };
}
if (!dryRun) fs.mkdirSync(ws, { recursive: true });
}
const skillDir = path.join(ws, 'skills', SKILL_NAME);
const skillFile = path.join(skillDir, 'SKILL.md');
const soulFile = path.join(ws, SOUL_FILE);
if (dryRun) {
log.note(` would write ${skillFile} (with version${always ? '/always' : ''} frontmatter)`);
if (always) {
log.note(` would ${fs.existsSync(soulFile) ? 'append to' : 'create'} ${soulFile} (caveman bootstrap block)`);
} else {
log.note(' --no-always: would skip SOUL.md bootstrap append (skill loads on demand)');
}
return { ok: true, dryRun: true };
}
fs.mkdirSync(skillDir, { recursive: true });
const merged = mergeOpenclawFrontmatter(skillBody, { version, always });
fs.writeFileSync(skillFile, merged, { mode: 0o644 });
log.write(` installed: ${skillFile}\n`);
if (always) {
const soul = appendBootstrapToSoul(soulFile, snippet);
if (soul.changed) log.write(` wrote bootstrap block to ${soulFile}\n`);
else log.note(` ${soulFile} already contains caveman bootstrap`);
} else {
log.note(' --no-always: skipped SOUL.md bootstrap append (skill loads on demand via `openclaw skills list`)');
}
return { ok: true };
}
function uninstallOpenclaw({ workspace, dryRun = false, log = noopLog() } = {}) {
const ws = workspace || resolveWorkspace();
const skillDir = path.join(ws, 'skills', SKILL_NAME);
const soulFile = path.join(ws, SOUL_FILE);
let touched = false;
if (fs.existsSync(skillDir)) {
if (dryRun) {
log.note(` would remove ${skillDir}/`);
} else {
try { fs.rmSync(skillDir, { recursive: true, force: true }); } catch (_) {}
log.note(` removed ${skillDir}`);
}
touched = true;
}
if (fs.existsSync(soulFile)) {
if (dryRun) {
log.note(` would strip caveman block from ${soulFile}`);
touched = true;
} else {
const r = stripBootstrapFromSoul(soulFile);
if (r.changed) {
log.note(r.removed ? ` removed ${soulFile}` : ` stripped caveman block from ${soulFile}`);
touched = true;
}
}
}
return { ok: true, touched };
}
function noopLog() {
return {
write: (_) => {},
note: (_) => {},
warn: (_) => {},
};
}
module.exports = {
installOpenclaw,
uninstallOpenclaw,
resolveWorkspace,
// exported for tests
mergeOpenclawFrontmatter,
splitFrontmatter,
appendBootstrapToSoul,
stripBootstrapFromSoul,
loadBootstrapSnippet,
MARK_BEGIN,
MARK_END,
SKILL_NAME,
SKILL_VERSION,
};
+42
View File
@@ -0,0 +1,42 @@
'use strict';
// Strip the `tools:` field from a Claude-Code-style subagent frontmatter so
// the file is valid for opencode, whose schema rejects the YAML array form
// (`tools: [Read, Grep, Bash]`) with:
//
// Configuration is invalid at .../agents/cavecrew-reviewer.md
// ↳ Expected object | undefined, got ["Read","Grep","Bash"] tools
//
// opencode allows `tools` to be a map (`{read: true, grep: true}`) or
// omitted entirely. Omitting falls back to opencode's default tool set,
// which is what the cavecrew subagent prompts already self-restrict against
// in their body ("Read-only locator", "No `Bash` available", etc.), so
// dropping the array form is safe.
const TOOLS_FIELD_RE = /^tools[ \t]*:/;
const CONTINUATION_RE = /^[ \t]/;
const FRONTMATTER_FENCE = '---\n';
function stripOpencodeAgentTools(content) {
if (typeof content !== 'string' || !content.startsWith(FRONTMATTER_FENCE)) return content;
const fmEnd = content.indexOf('\n---', FRONTMATTER_FENCE.length);
if (fmEnd < 0) return content;
const fm = content.slice(FRONTMATTER_FENCE.length, fmEnd);
const rest = content.slice(fmEnd);
const out = [];
let dropping = false;
for (const line of fm.split('\n')) {
if (dropping) {
if (CONTINUATION_RE.test(line)) continue;
dropping = false;
}
if (TOOLS_FIELD_RE.test(line)) { dropping = true; continue; }
out.push(line);
}
return FRONTMATTER_FENCE + out.join('\n') + rest;
}
module.exports = { stripOpencodeAgentTools };
+368
View File
@@ -0,0 +1,368 @@
// caveman — JSONC-tolerant settings.json read/write + defensive hook validation.
//
// Lifted in spirit from gsd-build/get-shit-done's stripJsonComments + readSettings.
// Reused by cli/install.js and (optionally) by hooks/caveman-activate.js so a
// commented settings.json no longer crashes the installer or the runtime hooks.
//
// Public API:
// readSettings(path) → object, {}, or null on hard parse failure
// writeSettings(path, obj) → atomic write with newline
// stripJsonComments(src) → string with // and /* */ stripped (string-aware)
// validateHookFields(settings) → mutates: drops malformed hook entries
// hasCavemanHook(settings, ev) → idempotency probe
// addCommandHook(settings, ev, opts) → no-op if substring marker already present
// removeCavemanHooks(settings) → uninstall helper
//
// Pure stdlib, CommonJS, Node ≥14.
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const crypto = require('crypto');
// ── stripJsonComments ──────────────────────────────────────────────────────
// Hand-rolled state machine. Tracks string state + backslash escape so a
// comment-looking sequence inside a quoted string is left alone. Removes
// trailing commas in a final pass — JSONC tolerates those, JSON.parse does not.
function stripJsonComments(src) {
if (typeof src !== 'string') return src;
let out = '';
let i = 0;
const n = src.length;
let inString = false;
let stringChar = '';
let inLine = false;
let inBlock = false;
while (i < n) {
const c = src[i];
const next = i + 1 < n ? src[i + 1] : '';
if (inLine) {
if (c === '\n') { inLine = false; out += c; }
i++; continue;
}
if (inBlock) {
if (c === '*' && next === '/') { inBlock = false; i += 2; continue; }
i++; continue;
}
if (inString) {
out += c;
if (c === '\\') { if (i + 1 < n) { out += src[i + 1]; i += 2; continue; } }
if (c === stringChar) { inString = false; }
i++; continue;
}
if (c === '"' || c === "'") { inString = true; stringChar = c; out += c; i++; continue; }
if (c === '/' && next === '/') { inLine = true; i += 2; continue; }
if (c === '/' && next === '*') { inBlock = true; i += 2; continue; }
out += c; i++;
}
return stripTrailingCommas(out);
}
// ── stripTrailingCommas ────────────────────────────────────────────────────
// Remove `,` when the next non-whitespace char is `}` or `]` — but only
// OUTSIDE strings. The old global regex ran over string contents too and
// silently corrupted values like `"echo ,}"` → `"echo }"` (issue #595);
// comment-stripping does not sanitize string bodies, so a string-aware scan
// is required here as well.
function stripTrailingCommas(src) {
let out = '';
let i = 0;
const n = src.length;
let inString = false;
let stringChar = '';
while (i < n) {
const c = src[i];
if (inString) {
out += c;
if (c === '\\') { if (i + 1 < n) { out += src[i + 1]; i += 2; continue; } }
if (c === stringChar) inString = false;
i++; continue;
}
if (c === '"' || c === "'") { inString = true; stringChar = c; out += c; i++; continue; }
if (c === ',') {
let j = i + 1;
while (j < n && /\s/.test(src[j])) j++;
if (j < n && (src[j] === '}' || src[j] === ']')) { i++; continue; } // drop the comma
}
out += c; i++;
}
return out;
}
// ── readSettings ───────────────────────────────────────────────────────────
// Try strict JSON first (fast path). On failure, strip comments and retry.
// On total failure return `null` and warn — never silently overwrite a
// malformed-but-recoverable file with `{}`.
function readSettings(p) {
if (!fs.existsSync(p)) return {};
let raw;
try { raw = fs.readFileSync(p, 'utf8'); }
catch (e) {
process.stderr.write(`caveman: cannot read ${p}: ${e.message}\n`);
return null;
}
if (!raw.trim()) return {};
try { return JSON.parse(raw); } catch (_) { /* fall through to JSONC */ }
try { return JSON.parse(stripJsonComments(raw)); }
catch (e) {
process.stderr.write(`caveman: warning — ${p} is not valid JSON or JSONC: ${e.message}\n`);
return null;
}
}
// ── writeSettings ──────────────────────────────────────────────────────────
// Atomic write: temp file + rename. mode 0600 (settings often contains tokens).
function writeSettings(p, obj) {
const dir = path.dirname(p);
fs.mkdirSync(dir, { recursive: true });
const tmp = path.join(dir, `.${path.basename(p)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
fs.renameSync(tmp, p);
}
// ── validateHookFields ────────────────────────────────────────────────────
// Claude Code uses strict Zod on settings.json — a single malformed hook
// silently discards the entire file. Mutate-to-valid before write.
//
// Required shape (per Claude Code docs):
// settings.hooks[event] = [{ hooks: [{ type:'command', command:'…', timeout?:n }, ...] }, ...]
// settings.hooks[event] = [{ matcher?:'…', hooks: [...] }, ...] // also valid
function validateHookFields(settings) {
if (!settings || typeof settings !== 'object') return settings;
if (!settings.hooks || typeof settings.hooks !== 'object') return settings;
for (const ev of Object.keys(settings.hooks)) {
const arr = settings.hooks[ev];
if (!Array.isArray(arr)) { delete settings.hooks[ev]; continue; }
settings.hooks[ev] = arr.filter(entry => {
if (!entry || typeof entry !== 'object') return false;
if (!Array.isArray(entry.hooks)) return false;
entry.hooks = entry.hooks.filter(h => {
if (!h || typeof h !== 'object') return false;
if (h.type === 'command') return typeof h.command === 'string' && h.command.length > 0;
if (h.type === 'agent') return typeof h.prompt === 'string' && h.prompt.length > 0;
return false;
});
return entry.hooks.length > 0;
});
if (settings.hooks[ev].length === 0) delete settings.hooks[ev];
}
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
return settings;
}
// ── Idempotency probe ──────────────────────────────────────────────────────
function hasCavemanHook(settings, event, marker = 'caveman') {
const arr = settings && settings.hooks && settings.hooks[event];
if (!Array.isArray(arr)) return false;
return arr.some(e =>
e && Array.isArray(e.hooks) &&
e.hooks.some(h => h && typeof h.command === 'string' && h.command.includes(marker))
);
}
// ── addCommandHook ────────────────────────────────────────────────────────
// Idempotent push. `marker` defaults to opts.command — pass an explicit
// shorter substring (e.g. the script basename) when the full command path
// might rotate across reinstalls.
function addCommandHook(settings, event, opts) {
if (!settings.hooks) settings.hooks = {};
if (!Array.isArray(settings.hooks[event])) settings.hooks[event] = [];
const marker = opts.marker || opts.command;
if (hasCavemanHook(settings, event, marker)) return false;
const hook = { type: 'command', command: opts.command };
if (typeof opts.timeout === 'number') hook.timeout = opts.timeout;
if (typeof opts.statusMessage === 'string') hook.statusMessage = opts.statusMessage;
settings.hooks[event].push({ hooks: [hook] });
return true;
}
// ── Managed hook scripts ──────────────────────────────────────────────────
// The exact script basenames this installer wires into settings.json. Every
// helper that decides "is this hook ours?" must match against these — never
// against a bare "caveman" substring, which also matches user-authored hooks
// that merely mention the word in a path (issue #593).
const MANAGED_HOOK_BASENAMES = new Set([
'caveman-activate.js',
'caveman-mode-tracker.js',
'caveman-stats.js',
'caveman-statusline.sh',
'caveman-statusline.ps1',
]);
// Split a command into shell-ish tokens, honoring single/double quotes so a
// path containing spaces survives intact. Good enough for hook commands we
// generate (`node "/a/x.js"`, `"/abs/node" "/a/x.js"`, `bash /a/x.sh`); not
// a full shell parser.
function tokenizeCommand(command) {
const out = [];
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
let m;
while ((m = re.exec(command)) !== null) out.push(m[1] ?? m[2] ?? m[3]);
return out;
}
// True iff some token's BASENAME exactly equals a managed script name. Exact
// match — not substring — so `mycaveman-activate.js` or a user hook living
// under a `caveman-notes/` directory is never treated as ours. win32.basename
// splits on both / and \ so a settings.json written on Windows still matches
// when processed elsewhere.
function referencesManagedScript(command) {
try {
for (const tok of tokenizeCommand(command)) {
if (tok && typeof tok === 'string' && MANAGED_HOOK_BASENAMES.has(path.win32.basename(tok))) return true;
}
} catch (_) { /* malformed command — treat as not ours */ }
return false;
}
// ── removeCavemanHooks ────────────────────────────────────────────────────
// Strip every entry whose any hook command targets one of our managed hook
// scripts (exact basename match, see above). Empties events. Tolerates
// malformed pre-existing settings (non-array hook lists, foreign shapes) —
// those get dropped by validateHookFields first so we never call .length /
// .filter on a non-array.
function removeCavemanHooks(settings) {
if (!settings || !settings.hooks) return 0;
validateHookFields(settings);
if (!settings.hooks) return 0; // validate may have deleted the whole tree
let removed = 0;
for (const ev of Object.keys(settings.hooks)) {
if (!Array.isArray(settings.hooks[ev])) { delete settings.hooks[ev]; continue; }
const before = settings.hooks[ev].length;
settings.hooks[ev] = settings.hooks[ev].filter(entry => {
if (!entry || !Array.isArray(entry.hooks)) return true;
return !entry.hooks.some(h => h && typeof h.command === 'string' && referencesManagedScript(h.command));
});
removed += before - settings.hooks[ev].length;
if (settings.hooks[ev].length === 0) delete settings.hooks[ev];
}
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
return removed;
}
// ── rewriteLegacyManagedHookCommands ──────────────────────────────────────
// Walk every hook command. If it's a bare `node /path/to/<managed>.js` (no
// absolute node path) and the basename is one of ours, rewrite to use
// `absoluteNode` so GUI launchers with minimal PATH still find Node. Only
// touches commands matching the exact bare-node shape — won't false-positive
// on user-authored hooks that just happen to mention "caveman".
function rewriteLegacyManagedHookCommands(settings, absoluteNode) {
if (!settings || !settings.hooks || !absoluteNode) return 0;
let rewritten = 0;
const reBare = /^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/;
for (const ev of Object.keys(settings.hooks)) {
// A hook event value that is an object/string (not an array) survives
// JSONC parse untouched — this runs BEFORE validateHookFields in
// installHooks, so it must tolerate malformed input itself rather than
// assume the array shape. Pre-fix, `for...of` on a non-iterable object
// threw a TypeError here and killed the installer mid-run (mirrors the
// guard removeCavemanHooks already has).
if (!Array.isArray(settings.hooks[ev])) continue;
for (const entry of settings.hooks[ev]) {
if (!entry || !Array.isArray(entry.hooks)) continue;
for (const h of entry.hooks) {
if (!h || typeof h.command !== 'string') continue;
const m = reBare.exec(h.command);
if (!m) continue;
const scriptPath = m[2] || m[3] || m[4];
const basename = path.basename(scriptPath);
if (!MANAGED_HOOK_BASENAMES.has(basename)) continue;
h.command = `"${absoluteNode}" "${scriptPath}"`;
rewritten++;
}
}
}
return rewritten;
}
// ── pruneOrphanedManagedHooks ─────────────────────────────────────────────
// Remove managed hook entries whose target script no longer exists on disk.
//
// Migrating an old manual install (settings.json hooks → ~/.claude/hooks/
// caveman-*.js) to the Claude Code plugin disables/renames those local
// scripts but leaves the settings.json entries pointing at the now-missing
// file. Claude Code then runs `node <missing>` every SessionStart /
// UserPromptSubmit and crashes with `node:…/loader:1478 — Cannot find module
// …caveman-activate.js` (issue #471). rewriteLegacyManagedHookCommands can't
// help — it only matches the bare-node shape and these orphans are usually
// absolute-node — and removeCavemanHooks runs only on uninstall.
//
// We extract the script path from any managed-looking command (bare- or
// absolute-node, quoted or not), resolve it relative to dir if not absolute,
// and drop the hook only when its target is genuinely absent. A managed hook
// whose script still exists is left untouched, so this is safe to run on
// every install.
function pruneOrphanedManagedHooks(settings, configDir) {
if (!settings || typeof settings !== 'object') return 0;
const baseDir = configDir || claudeConfigDir();
let removed = 0;
// A command is a missing managed target iff some token's BASENAME exactly
// equals a managed script (exact match — not substring — so a user hook like
// `mycaveman-activate.js` is never touched) and that resolved path is absent.
// Relative paths resolve against configDir; honors CLAUDE_CONFIG_DIR. Wrapped
// so a malformed command or fs error never throws out of the prune pass.
const targetMissing = (command) => {
try {
for (const tok of tokenizeCommand(command)) {
if (!tok || typeof tok !== 'string') continue;
if (!MANAGED_HOOK_BASENAMES.has(path.basename(tok))) continue;
const scriptPath = path.isAbsolute(tok) ? tok : path.join(baseDir, tok);
return !fs.existsSync(scriptPath);
}
} catch (_) { /* silent-fail: never block install on a parse/fs hiccup */ }
return false;
};
if (settings.hooks && typeof settings.hooks === 'object') {
// Normalize malformed shapes first so the filter below only sees valid
// entries (and a poisoned settings.json can't survive the rewrite).
validateHookFields(settings);
}
if (settings.hooks && typeof settings.hooks === 'object') {
for (const ev of Object.keys(settings.hooks)) {
if (!Array.isArray(settings.hooks[ev])) { delete settings.hooks[ev]; continue; }
const before = settings.hooks[ev].length;
settings.hooks[ev] = settings.hooks[ev].filter(entry => {
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.hooks)) return true;
return !entry.hooks.some(h => h && typeof h.command === 'string' && targetMissing(h.command));
});
removed += before - settings.hooks[ev].length;
if (settings.hooks[ev].length === 0) delete settings.hooks[ev];
}
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
}
// statusLine lives outside settings.hooks. A managed statusline command
// pointing at a missing script leaves a blank statusline (cosmetic, exits
// clean) but is still stale — drop it so Claude Code falls back to default.
if (settings.statusLine && typeof settings.statusLine.command === 'string'
&& targetMissing(settings.statusLine.command)) {
delete settings.statusLine;
removed++;
}
return removed;
}
// ── claudeConfigDir ───────────────────────────────────────────────────────
function claudeConfigDir() {
if (process.env.CLAUDE_CONFIG_DIR) return process.env.CLAUDE_CONFIG_DIR;
return path.join(os.homedir(), '.claude');
}
module.exports = {
stripJsonComments,
readSettings,
writeSettings,
validateHookFields,
hasCavemanHook,
addCommandHook,
removeCavemanHooks,
rewriteLegacyManagedHookCommands,
pruneOrphanedManagedHooks,
claudeConfigDir,
MANAGED_HOOK_BASENAMES,
};
+5
View File
@@ -0,0 +1,5 @@
---
description: Generate terse caveman-style commit message
---
Generate a terse commit message for the current staged changes. Conventional Commits format. Subject: ≤50 chars, imperative, lowercase after type. Body: only when 'why' isn't obvious from subject. Why over what. No period on subject.
+2
View File
@@ -0,0 +1,2 @@
description = "Generate terse caveman-style commit message"
prompt = "Generate a terse commit message for the current staged changes. Conventional Commits format. Subject: ≤50 chars, imperative, lowercase after type. Body: only when 'why' isn't obvious from subject. Why over what. No period on subject."
+13
View File
@@ -0,0 +1,13 @@
---
description: Drop the always-on caveman activation rule into the current repo for every IDE agent
argument-hint: "[--dry-run|--force] [--only <agent>]"
---
Write the per-repo caveman rule files (Cursor, Windsurf, Cline, Copilot, AGENTS.md) into the current repo, then report the result.
How to run the init script — pick the first that applies:
1. If `src/tools/caveman-init.js` exists in the current repo (you are inside a caveman checkout), run: `node src/tools/caveman-init.js $ARGUMENTS`
2. Otherwise download and run the standalone script (it is self-contained and supports stdin execution): `curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/tools/caveman-init.js | node - $ARGUMENTS`
Use `--dry-run` first if the user did not pass `--force`, so we never silently overwrite an existing rule file.
+2
View File
@@ -0,0 +1,2 @@
description = "Drop the always-on caveman activation rule into the current repo for every IDE agent"
prompt = "Write the per-repo caveman rule files into the current repo and report the result. If `src/tools/caveman-init.js` exists in the current repo (a caveman checkout), run `node src/tools/caveman-init.js {{args}}`. Otherwise run the standalone script (self-contained, supports stdin execution): `curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/tools/caveman-init.js | node - {{args}}`. Use --dry-run first if the user did not pass --force, so we never silently overwrite an existing rule file."
+5
View File
@@ -0,0 +1,5 @@
---
description: One-line code review comments
---
Review the current code changes. One-line per finding. Format: L<line>: <severity> <problem>. <fix>. Severity: bug, risk, nit, q. Skip praise. Skip obvious. If code look good, say 'LGTM' and stop.
+2
View File
@@ -0,0 +1,2 @@
description = "One-line code review comments"
prompt = "Review the current code changes. One-line per finding. Format: L<line>: <severity> <problem>. <fix>. Severity: bug, risk, nit, q. Skip praise. Skip obvious. If code look good, say 'LGTM' and stop."
+6
View File
@@ -0,0 +1,6 @@
---
description: Real session token usage + lifetime savings + USD. Tweetable line via --share.
argument-hint: "[--share|--all|--since 7d]"
---
/caveman-stats $ARGUMENTS
+2
View File
@@ -0,0 +1,2 @@
description = "Real session token usage + lifetime savings + USD. Tweetable line via --share."
prompt = "/caveman-stats {{args}}"
+6
View File
@@ -0,0 +1,6 @@
---
description: Switch caveman intensity level (lite/full/ultra/wenyan)
argument-hint: "[lite|full|ultra|wenyan]"
---
Switch to caveman $ARGUMENTS mode. If no level specified, use full. Respond terse like smart caveman — drop articles, filler, pleasantries. Fragments OK. Technical terms exact. Code unchanged. Pattern: [thing] [action] [reason]. [next step].
+2
View File
@@ -0,0 +1,2 @@
description = "Switch caveman intensity level (lite/full/ultra/wenyan)"
prompt = "Switch to caveman {{args}} mode. If no level specified, use full. Respond terse like smart caveman — drop articles, filler, pleasantries. Fragments OK. Technical terms exact. Code unchanged. Pattern: [thing] [action] [reason]. [next step]."
BIN
View File
Binary file not shown.
+47
View File
@@ -0,0 +1,47 @@
# Honest Numbers
Caveman save tokens sometimes. Caveman cost tokens sometimes. This page say which is which, with the real numbers. No marketing. If caveman lose for your workload, this page tell you to turn it off.
## What caveman actually does
Caveman is a system-prompt skill. It makes the model **write shorter output**. That is the whole mechanism. It does not compress your input, your context, your files, or the model's thinking tokens.
## The measured numbers
| What | Number | How measured | Source |
|---|---|---|---|
| Output reduction vs default verbose replies | **65% average** (range 2287%) | Real Claude API token counts, 10 prompts | [`benchmarks/`](../benchmarks/) |
| Input reduction from the skill | **0%** | It's an output-style instruction | — |
| Input cost the skill *adds* | **~11.5k tokens per turn** | SKILL.md rules (~5 KB) injected into context, plus skill-list entries | [`skills/caveman/SKILL.md`](../skills/caveman/SKILL.md) |
| `/caveman-compress` on memory files | ~46% average input reduction, per session, for those files only | Real files, token counts in README table | [README](../README.md#benchmarks) |
These figures are output tokens only — the skill does not compress your input, your context, your files, or the model's thinking tokens. The full eval harness and its correction history are documented in [`evals/README.md`](../evals/README.md).
## When caveman wins
- **Long chatty outputs.** Explanations, architecture discussions, code review, docs, debugging walkthroughs — anywhere the model would write 1k+ output tokens per reply. This is where the 5087% cuts happen.
- **Long sessions with verbose agents.** The per-reply savings compound; the fixed ~11.5k/turn rule cost stays flat.
- **Reading speed.** Shorter replies finish sooner and you read them faster. For many users this, not cost, is the real win.
## When caveman loses (net-negative)
Plainly: **the skill costs ~11.5k input tokens every turn. If it saves less output than that, you are paying to use it.**
- **Terse coding Q&A** ([#145](https://github.com/JuliusBrussee/caveman/issues/145)). If your normal replies are ~150 output tokens, caveman saves maybe 70100 of them and costs ~1k+ of input overhead per turn. Net loss. The user in #145 measured exactly this. They were right.
- **Agents that bill by request or credit, not tokens** ([#506](https://github.com/JuliusBrussee/caveman/issues/506)). GitHub Copilot charges premium *requests*. A shorter answer is the same request. Caveman cannot lower your Copilot credit use. Same logic for any per-message pricing.
- **Session-level totals** are always smaller than the output-reduction headline, because input tokens (your prompts, your context, your files, the injected rules) dwarf output tokens in agentic coding. Independent session-level measurements land around **1421% total savings** on output-heavy workloads — and below zero on terse ones.
- **Some tool-side counters go the wrong way** ([#550](https://github.com/JuliusBrussee/caveman/issues/550)). One Cursor A/B showed 4.3M tokens with caveman vs 1M without, and double the wall-clock time. We could not reproduce the exact run, but the honest reading is: rule re-injection, retries, and cache/context accounting can swamp output savings in some agents. If your A/B looks like that, caveman is net-negative for you. Turn it off. Wanting the rock to work does not make the rock work.
## Measure it yourself
1. **`/caveman-stats`** (Claude Code) reads your real session log and prints actual input/output token counts. The "saved" line is an **estimate**: it extrapolates what the output would have been without caveman using the benchmark ratio. Real usage, estimated baseline — the output labels it `est.` for exactly that reason.
2. **The only fully honest test is an A/B**: run the same task with and without caveman and compare your provider's own usage/billing page. That number outranks anything this repo prints.
3. **Reproduce our numbers**: `benchmarks/run.py` (needs an Anthropic key) and `evals/measure.py` (offline, reads the committed snapshot).
## Rule of thumb
> Normal reply longer than ~1.52k output tokens → caveman probably saves you money.
> Normal reply shorter than that, or you pay per request → caveman probably costs you money.
> Either way, caveman replies faster to read. That part is free.
Found a workload where our numbers are wrong? [Open an issue](https://github.com/JuliusBrussee/caveman/issues) with the A/B. We will put it on this page.
+12
View File
@@ -0,0 +1,12 @@
<svg width="163" height="26" viewBox="0 0 163 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.9475 25.7973C30.9995 25.7973 29.4793 25.2568 28.3984 24.1871C27.3174 23.1174 26.7769 21.6085 26.7769 19.6492V12.0148H23.8154V8.28766H24.1307C24.9752 8.28766 25.6283 8.06245 26.09 7.6233C26.5404 7.17289 26.7769 6.53106 26.7769 5.68654V4.34657H30.9769V8.28766H34.9518V12.0148H30.9769V19.4353C30.9769 20.0096 31.0783 20.4938 31.281 20.8991C31.4837 21.3045 31.7989 21.6085 32.2381 21.8225C32.6772 22.0364 33.229 22.1378 33.9046 22.1378C34.051 22.1378 34.2312 22.1378 34.4338 22.104C34.6365 22.0814 34.828 22.0589 35.0194 22.0364V25.6059C34.7266 25.6509 34.3775 25.696 34.006 25.7298C33.6231 25.7748 33.274 25.7973 32.9588 25.7973H32.9475Z" fill="#FFFFFF"/>
<path d="M36.5732 25.6059V1.52028H40.7733V25.6059H36.5732Z" fill="#FFFFFF"/>
<path d="M48.2839 25.9888C47.079 25.9888 46.0206 25.7861 45.1197 25.3807C44.2189 24.9753 43.5208 24.4011 43.0366 23.6466C42.5524 22.8922 42.3047 22.0139 42.3047 21.023C42.3047 20.0321 42.5186 19.2101 42.9578 18.4557C43.3969 17.7012 44.05 17.0706 44.9508 16.5639C45.8404 16.0572 46.9664 15.6969 48.3289 15.483L53.959 14.5596V17.7463L49.1171 18.602C48.2951 18.7484 47.6758 19.0074 47.2705 19.379C46.8651 19.7506 46.6624 20.246 46.6624 20.8541C46.6624 21.4621 46.8876 21.9238 47.3493 22.2729C47.7997 22.6219 48.374 22.8021 49.0496 22.8021C49.9166 22.8021 50.6936 22.6219 51.3579 22.2504C52.0223 21.8788 52.5403 21.3608 52.9006 20.7077C53.2609 20.0546 53.4411 19.334 53.4411 18.5795V14.0867C53.4411 13.3435 53.1596 12.7242 52.5853 12.2287C52.011 11.7333 51.2453 11.4856 50.2995 11.4856C49.4099 11.4856 48.6217 11.7333 47.9236 12.2175C47.2367 12.7017 46.73 13.3322 46.4147 14.0979L43.0141 12.4427C43.3519 11.5306 43.8924 10.7424 44.6243 10.0668C45.3562 9.39116 46.2233 8.87319 47.2142 8.49034C48.2163 8.10749 49.2973 7.91607 50.4571 7.91607C51.8759 7.91607 53.1258 8.17505 54.2068 8.69303C55.2878 9.211 56.1323 9.94291 56.7403 10.8775C57.3484 11.8121 57.6524 12.8818 57.6524 14.0867V25.6059H53.7113V22.6445L54.6009 22.6107C54.1505 23.3313 53.6212 23.9507 52.9907 24.4574C52.3601 24.9641 51.662 25.3469 50.885 25.6059C50.108 25.8649 49.241 25.9888 48.2951 25.9888H48.2839Z" fill="#FFFFFF"/>
<path d="M66.4802 25.9888C64.6336 25.9888 63.0233 25.5496 61.6609 24.6713C60.2871 23.793 59.3412 22.5994 58.812 21.0906L61.9649 19.5929C62.4153 20.5726 63.0346 21.3383 63.8228 21.89C64.6223 22.4418 65.5006 22.712 66.4802 22.712C67.2234 22.712 67.8202 22.5431 68.2594 22.2053C68.7098 21.8675 68.9237 21.4171 68.9237 20.8653C68.9237 20.5275 68.8336 20.246 68.6535 20.0208C68.4733 19.7956 68.2368 19.6042 67.9328 19.4466C67.64 19.2889 67.291 19.1538 66.9194 19.0524L64.0818 18.253C62.6405 17.8476 61.537 17.2058 60.7826 16.3275C60.0281 15.4492 59.6565 14.4132 59.6565 13.2196C59.6565 12.1612 59.9268 11.2266 60.4673 10.4384C61.0078 9.65015 61.7622 9.01957 62.7306 8.58042C63.699 8.13001 64.8025 7.91607 66.0524 7.91607C67.6851 7.91607 69.1264 8.31018 70.3763 9.09839C71.6262 9.88661 72.5157 10.9901 73.045 12.4089L69.8583 13.9065C69.5656 13.1183 69.0588 12.499 68.3607 12.0486C67.6626 11.5982 66.8743 11.3617 66.0073 11.3617C65.3092 11.3617 64.7574 11.5193 64.3521 11.8234C63.9467 12.1274 63.744 12.5553 63.744 13.0845C63.744 13.3773 63.8341 13.6475 64.003 13.884C64.1719 14.1205 64.4084 14.3119 64.7236 14.4583C65.0277 14.6047 65.388 14.7398 65.7934 14.8749L68.5634 15.6969C69.9822 16.1248 71.0857 16.7554 71.8626 17.6111C72.6396 18.4557 73.0224 19.5029 73.0224 20.7302C73.0224 21.7662 72.7409 22.6895 72.2005 23.4777C71.6487 24.2772 70.883 24.8965 69.9146 25.3357C68.935 25.7861 67.7977 26 66.4802 26V25.9888Z" fill="#FFFFFF"/>
<path d="M90.2282 25.9889C88.5279 25.9889 86.9627 25.6849 85.5214 25.0656C84.0801 24.4463 82.8302 23.5905 81.7717 22.487C80.7133 21.3835 79.88 20.0886 79.2719 18.6022C78.6639 17.1159 78.3599 15.4944 78.3599 13.7378C78.3599 11.9812 78.6526 10.3485 79.2494 8.85085C79.8462 7.35324 80.6795 6.05831 81.7492 4.96606C82.8189 3.87382 84.0688 3.0293 85.4989 2.42125C86.9289 1.8132 88.5053 1.50917 90.2282 1.50917C91.951 1.50917 93.4486 1.79068 94.7998 2.36495C96.1511 2.93922 97.2884 3.69366 98.223 4.63952C99.1576 5.58538 99.8219 6.62132 100.227 7.74735L96.3425 9.59403C95.8921 8.38918 95.1489 7.39828 94.0792 6.62132C93.0207 5.84436 91.737 5.46152 90.2282 5.46152C88.7193 5.46152 87.4356 5.81058 86.2983 6.50872C85.1611 7.20685 84.2828 8.17523 83.6522 9.4026C83.0216 10.63 82.7176 12.0713 82.7176 13.7265C82.7176 15.3818 83.0329 16.8344 83.6522 18.073C84.2828 19.3116 85.1611 20.28 86.2983 20.9894C87.4356 21.6875 88.7418 22.0366 90.2282 22.0366C91.7145 22.0366 93.0207 21.6537 94.0792 20.8768C95.1376 20.0998 95.8921 19.1202 96.3425 17.9379L100.227 19.7508C99.8219 20.8768 99.1576 21.9127 98.223 22.8586C97.2884 23.8045 96.1511 24.5589 94.7998 25.1332C93.4486 25.7074 91.9285 25.9889 90.2282 25.9889Z" fill="#FFFFFF"/>
<path d="M101.748 25.6059V1.52028H105.948V25.6059H101.748Z" fill="#FFFFFF"/>
<path d="M116.645 25.9884C114.968 25.9884 113.436 25.5943 112.051 24.8061C110.666 24.0178 109.551 22.9481 108.729 21.5969C107.907 20.2344 107.491 18.6917 107.491 16.9464C107.491 15.2011 107.907 13.6584 108.729 12.2959C109.551 10.9334 110.655 9.86371 112.04 9.08676C113.414 8.29854 114.956 7.90443 116.657 7.90443C118.357 7.90443 119.922 8.29854 121.307 9.08676C122.681 9.87497 123.784 10.9334 124.606 12.2847C125.417 13.6359 125.834 15.1898 125.834 16.9464C125.834 18.703 125.417 20.2344 124.595 21.5969C123.773 22.9594 122.67 24.0291 121.285 24.8061C119.911 25.5943 118.368 25.9884 116.668 25.9884H116.645ZM116.645 22.1712C117.602 22.1712 118.436 21.946 119.145 21.5068C119.854 21.0564 120.417 20.4371 120.834 19.6489C121.251 18.8494 121.453 17.9598 121.453 16.9577C121.453 15.9555 121.251 15.0659 120.834 14.289C120.417 13.5008 119.854 12.8927 119.145 12.4423C118.436 11.9919 117.602 11.778 116.645 11.778C115.688 11.778 114.889 12.0032 114.168 12.4423C113.447 12.8927 112.884 13.5008 112.468 14.289C112.051 15.0772 111.848 15.9668 111.848 16.9577C111.848 17.9486 112.051 18.8494 112.468 19.6489C112.884 20.4483 113.447 21.0677 114.168 21.5068C114.889 21.9572 115.722 22.1712 116.645 22.1712Z" fill="#FFFFFF"/>
<path d="M133.535 25.9884C132.172 25.9884 131.013 25.6956 130.033 25.0988C129.053 24.502 128.299 23.68 127.77 22.6215C127.24 21.5631 126.97 20.3245 126.97 18.8944V8.29852H131.17V18.5453C131.17 19.266 131.317 19.8966 131.598 20.4371C131.88 20.9775 132.296 21.4054 132.837 21.7095C133.377 22.0135 133.985 22.1711 134.672 22.1711C135.359 22.1711 135.956 22.0135 136.485 21.7095C137.014 21.4054 137.431 20.9775 137.724 20.4258C138.017 19.874 138.174 19.221 138.174 18.4553V8.29852H142.34V25.6055H138.399V22.2049L138.715 22.813C138.309 23.8714 137.656 24.6709 136.744 25.2001C135.832 25.7294 134.762 25.9996 133.535 25.9996V25.9884Z" fill="#FFFFFF"/>
<path d="M152.7 25.9888C151.022 25.9888 149.525 25.5947 148.196 24.7952C146.867 23.9957 145.82 22.9147 145.066 21.5297C144.3 20.156 143.917 18.6246 143.917 16.9468C143.917 15.269 144.3 13.7264 145.077 12.3639C145.854 11.0014 146.901 9.92042 148.207 9.12094C149.525 8.31021 151.011 7.9161 152.666 7.9161C153.984 7.9161 155.155 8.17508 156.179 8.69305C157.204 9.21103 158.015 9.94294 158.612 10.8775L157.97 11.7333V1.52028H162.136V25.6059H158.195V22.2616L158.645 23.0836C158.049 24.0408 157.227 24.7614 156.168 25.2456C155.11 25.7298 153.95 25.9775 152.7 25.9775V25.9888ZM153.139 22.1716C154.074 22.1716 154.907 21.9464 155.639 21.5072C156.371 21.0568 156.945 20.4487 157.362 19.6605C157.778 18.8723 157.981 17.9715 157.981 16.9581C157.981 15.9447 157.778 15.0664 157.362 14.2894C156.945 13.5012 156.371 12.8931 155.639 12.4427C154.907 11.9923 154.074 11.7784 153.139 11.7784C152.205 11.7784 151.371 12.0036 150.628 12.4427C149.885 12.8931 149.311 13.5012 148.894 14.2894C148.477 15.0776 148.275 15.9672 148.275 16.9581C148.275 17.949 148.477 18.8836 148.894 19.6605C149.311 20.4487 149.885 21.0568 150.628 21.5072C151.371 21.9576 152.205 22.1716 153.139 22.1716Z" fill="#FFFFFF"/>
<path d="M13.4447 1.71661e-05L0 25.9887C6.22692 23.5227 11.249 23.1623 15.7643 23.3763L13.7037 18.8159C12.8029 18.7258 10.1905 18.7258 8.9519 19.0523L13.4447 9.06451C13.4447 9.06451 20.2009 23.7366 20.2121 23.7366C21.5183 23.9393 24.9977 25.1104 26.8895 25.9887L13.4447 1.71661e-05Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg width="163" height="26" viewBox="0 0 163 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.9475 25.7973C30.9995 25.7973 29.4793 25.2568 28.3984 24.1871C27.3174 23.1174 26.7769 21.6085 26.7769 19.6492V12.0148H23.8154V8.28766H24.1307C24.9752 8.28766 25.6283 8.06245 26.09 7.6233C26.5404 7.17289 26.7769 6.53106 26.7769 5.68654V4.34657H30.9769V8.28766H34.9518V12.0148H30.9769V19.4353C30.9769 20.0096 31.0783 20.4938 31.281 20.8991C31.4837 21.3045 31.7989 21.6085 32.2381 21.8225C32.6772 22.0364 33.229 22.1378 33.9046 22.1378C34.051 22.1378 34.2312 22.1378 34.4338 22.104C34.6365 22.0814 34.828 22.0589 35.0194 22.0364V25.6059C34.7266 25.6509 34.3775 25.696 34.006 25.7298C33.6231 25.7748 33.274 25.7973 32.9588 25.7973H32.9475Z" fill="#0F1111"/>
<path d="M36.5732 25.6059V1.52028H40.7733V25.6059H36.5732Z" fill="#0F1111"/>
<path d="M48.2839 25.9888C47.079 25.9888 46.0206 25.7861 45.1197 25.3807C44.2189 24.9753 43.5208 24.4011 43.0366 23.6466C42.5524 22.8922 42.3047 22.0139 42.3047 21.023C42.3047 20.0321 42.5186 19.2101 42.9578 18.4557C43.3969 17.7012 44.05 17.0706 44.9508 16.5639C45.8404 16.0572 46.9664 15.6969 48.3289 15.483L53.959 14.5596V17.7463L49.1171 18.602C48.2951 18.7484 47.6758 19.0074 47.2705 19.379C46.8651 19.7506 46.6624 20.246 46.6624 20.8541C46.6624 21.4621 46.8876 21.9238 47.3493 22.2729C47.7997 22.6219 48.374 22.8021 49.0496 22.8021C49.9166 22.8021 50.6936 22.6219 51.3579 22.2504C52.0223 21.8788 52.5403 21.3608 52.9006 20.7077C53.2609 20.0546 53.4411 19.334 53.4411 18.5795V14.0867C53.4411 13.3435 53.1596 12.7242 52.5853 12.2287C52.011 11.7333 51.2453 11.4856 50.2995 11.4856C49.4099 11.4856 48.6217 11.7333 47.9236 12.2175C47.2367 12.7017 46.73 13.3322 46.4147 14.0979L43.0141 12.4427C43.3519 11.5306 43.8924 10.7424 44.6243 10.0668C45.3562 9.39116 46.2233 8.87319 47.2142 8.49034C48.2163 8.10749 49.2973 7.91607 50.4571 7.91607C51.8759 7.91607 53.1258 8.17505 54.2068 8.69303C55.2878 9.211 56.1323 9.94291 56.7403 10.8775C57.3484 11.8121 57.6524 12.8818 57.6524 14.0867V25.6059H53.7113V22.6445L54.6009 22.6107C54.1505 23.3313 53.6212 23.9507 52.9907 24.4574C52.3601 24.9641 51.662 25.3469 50.885 25.6059C50.108 25.8649 49.241 25.9888 48.2951 25.9888H48.2839Z" fill="#0F1111"/>
<path d="M66.4802 25.9888C64.6336 25.9888 63.0233 25.5496 61.6609 24.6713C60.2871 23.793 59.3412 22.5994 58.812 21.0906L61.9649 19.5929C62.4153 20.5726 63.0346 21.3383 63.8228 21.89C64.6223 22.4418 65.5006 22.712 66.4802 22.712C67.2234 22.712 67.8202 22.5431 68.2594 22.2053C68.7098 21.8675 68.9237 21.4171 68.9237 20.8653C68.9237 20.5275 68.8336 20.246 68.6535 20.0208C68.4733 19.7956 68.2368 19.6042 67.9328 19.4466C67.64 19.2889 67.291 19.1538 66.9194 19.0524L64.0818 18.253C62.6405 17.8476 61.537 17.2058 60.7826 16.3275C60.0281 15.4492 59.6565 14.4132 59.6565 13.2196C59.6565 12.1612 59.9268 11.2266 60.4673 10.4384C61.0078 9.65015 61.7622 9.01957 62.7306 8.58042C63.699 8.13001 64.8025 7.91607 66.0524 7.91607C67.6851 7.91607 69.1264 8.31018 70.3763 9.09839C71.6262 9.88661 72.5157 10.9901 73.045 12.4089L69.8583 13.9065C69.5656 13.1183 69.0588 12.499 68.3607 12.0486C67.6626 11.5982 66.8743 11.3617 66.0073 11.3617C65.3092 11.3617 64.7574 11.5193 64.3521 11.8234C63.9467 12.1274 63.744 12.5553 63.744 13.0845C63.744 13.3773 63.8341 13.6475 64.003 13.884C64.1719 14.1205 64.4084 14.3119 64.7236 14.4583C65.0277 14.6047 65.388 14.7398 65.7934 14.8749L68.5634 15.6969C69.9822 16.1248 71.0857 16.7554 71.8626 17.6111C72.6396 18.4557 73.0224 19.5029 73.0224 20.7302C73.0224 21.7662 72.7409 22.6895 72.2005 23.4777C71.6487 24.2772 70.883 24.8965 69.9146 25.3357C68.935 25.7861 67.7977 26 66.4802 26V25.9888Z" fill="#0F1111"/>
<path d="M90.2282 25.9889C88.5279 25.9889 86.9627 25.6849 85.5214 25.0656C84.0801 24.4463 82.8302 23.5905 81.7717 22.487C80.7133 21.3835 79.88 20.0886 79.2719 18.6022C78.6639 17.1159 78.3599 15.4944 78.3599 13.7378C78.3599 11.9812 78.6526 10.3485 79.2494 8.85085C79.8462 7.35324 80.6795 6.05831 81.7492 4.96606C82.8189 3.87382 84.0688 3.0293 85.4989 2.42125C86.9289 1.8132 88.5053 1.50917 90.2282 1.50917C91.951 1.50917 93.4486 1.79068 94.7998 2.36495C96.1511 2.93922 97.2884 3.69366 98.223 4.63952C99.1576 5.58538 99.8219 6.62132 100.227 7.74735L96.3425 9.59403C95.8921 8.38918 95.1489 7.39828 94.0792 6.62132C93.0207 5.84436 91.737 5.46152 90.2282 5.46152C88.7193 5.46152 87.4356 5.81058 86.2983 6.50872C85.1611 7.20685 84.2828 8.17523 83.6522 9.4026C83.0216 10.63 82.7176 12.0713 82.7176 13.7265C82.7176 15.3818 83.0329 16.8344 83.6522 18.073C84.2828 19.3116 85.1611 20.28 86.2983 20.9894C87.4356 21.6875 88.7418 22.0366 90.2282 22.0366C91.7145 22.0366 93.0207 21.6537 94.0792 20.8768C95.1376 20.0998 95.8921 19.1202 96.3425 17.9379L100.227 19.7508C99.8219 20.8768 99.1576 21.9127 98.223 22.8586C97.2884 23.8045 96.1511 24.5589 94.7998 25.1332C93.4486 25.7074 91.9285 25.9889 90.2282 25.9889Z" fill="#0F1111"/>
<path d="M101.748 25.6059V1.52028H105.948V25.6059H101.748Z" fill="#0F1111"/>
<path d="M116.645 25.9884C114.968 25.9884 113.436 25.5943 112.051 24.8061C110.666 24.0178 109.551 22.9481 108.729 21.5969C107.907 20.2344 107.491 18.6917 107.491 16.9464C107.491 15.2011 107.907 13.6584 108.729 12.2959C109.551 10.9334 110.655 9.86371 112.04 9.08676C113.414 8.29854 114.956 7.90443 116.657 7.90443C118.357 7.90443 119.922 8.29854 121.307 9.08676C122.681 9.87497 123.784 10.9334 124.606 12.2847C125.417 13.6359 125.834 15.1898 125.834 16.9464C125.834 18.703 125.417 20.2344 124.595 21.5969C123.773 22.9594 122.67 24.0291 121.285 24.8061C119.911 25.5943 118.368 25.9884 116.668 25.9884H116.645ZM116.645 22.1712C117.602 22.1712 118.436 21.946 119.145 21.5068C119.854 21.0564 120.417 20.4371 120.834 19.6489C121.251 18.8494 121.453 17.9598 121.453 16.9577C121.453 15.9555 121.251 15.0659 120.834 14.289C120.417 13.5008 119.854 12.8927 119.145 12.4423C118.436 11.9919 117.602 11.778 116.645 11.778C115.688 11.778 114.889 12.0032 114.168 12.4423C113.447 12.8927 112.884 13.5008 112.468 14.289C112.051 15.0772 111.848 15.9668 111.848 16.9577C111.848 17.9486 112.051 18.8494 112.468 19.6489C112.884 20.4483 113.447 21.0677 114.168 21.5068C114.889 21.9572 115.722 22.1712 116.645 22.1712Z" fill="#0F1111"/>
<path d="M133.535 25.9884C132.172 25.9884 131.013 25.6956 130.033 25.0988C129.053 24.502 128.299 23.68 127.77 22.6215C127.24 21.5631 126.97 20.3245 126.97 18.8944V8.29852H131.17V18.5453C131.17 19.266 131.317 19.8966 131.598 20.4371C131.88 20.9775 132.296 21.4054 132.837 21.7095C133.377 22.0135 133.985 22.1711 134.672 22.1711C135.359 22.1711 135.956 22.0135 136.485 21.7095C137.014 21.4054 137.431 20.9775 137.724 20.4258C138.017 19.874 138.174 19.221 138.174 18.4553V8.29852H142.34V25.6055H138.399V22.2049L138.715 22.813C138.309 23.8714 137.656 24.6709 136.744 25.2001C135.832 25.7294 134.762 25.9996 133.535 25.9996V25.9884Z" fill="#0F1111"/>
<path d="M152.7 25.9888C151.022 25.9888 149.525 25.5947 148.196 24.7952C146.867 23.9957 145.82 22.9147 145.066 21.5297C144.3 20.156 143.917 18.6246 143.917 16.9468C143.917 15.269 144.3 13.7264 145.077 12.3639C145.854 11.0014 146.901 9.92042 148.207 9.12094C149.525 8.31021 151.011 7.9161 152.666 7.9161C153.984 7.9161 155.155 8.17508 156.179 8.69305C157.204 9.21103 158.015 9.94294 158.612 10.8775L157.97 11.7333V1.52028H162.136V25.6059H158.195V22.2616L158.645 23.0836C158.049 24.0408 157.227 24.7614 156.168 25.2456C155.11 25.7298 153.95 25.9775 152.7 25.9775V25.9888ZM153.139 22.1716C154.074 22.1716 154.907 21.9464 155.639 21.5072C156.371 21.0568 156.945 20.4487 157.362 19.6605C157.778 18.8723 157.981 17.9715 157.981 16.9581C157.981 15.9447 157.778 15.0664 157.362 14.2894C156.945 13.5012 156.371 12.8931 155.639 12.4427C154.907 11.9923 154.074 11.7784 153.139 11.7784C152.205 11.7784 151.371 12.0036 150.628 12.4427C149.885 12.8931 149.311 13.5012 148.894 14.2894C148.477 15.0776 148.275 15.9672 148.275 16.9581C148.275 17.949 148.477 18.8836 148.894 19.6605C149.311 20.4487 149.885 21.0568 150.628 21.5072C151.371 21.9576 152.205 22.1716 153.139 22.1716Z" fill="#0F1111"/>
<path d="M13.4447 1.71661e-05L0 25.9887C6.22692 23.5227 11.249 23.1623 15.7643 23.3763L13.7037 18.8159C12.8029 18.7258 10.1905 18.7258 8.9519 19.0523L13.4447 9.06451C13.4447 9.06451 20.2009 23.7366 20.2121 23.7366C21.5183 23.9393 24.9977 25.1104 26.8895 25.9887L13.4447 1.71661e-05Z" fill="#0F1111"/>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

+14 -4
View File
@@ -33,7 +33,7 @@ body {
background-color: var(--bg); color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
overflow-x: hidden; -webkit-font-smoothing: antialiased; letter-spacing: -0.02em;
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ctext y='20' font-size='20'%3E🪨%3C/text%3E%3C/svg%3E"), auto;
cursor: url("assets/dancing-rock-32.png") 16 16, auto;
}
::selection { background: var(--text-primary); color: var(--bg); }
@@ -208,7 +208,7 @@ body {
<div class="telemetry-widget">
<div>STATUS: <span class="telemetry-val" style="color:#4ade80">OPTIMIZED</span></div>
<div>PHYSICAL_IMPACTS: <span class="telemetry-val" id="bonkCount">0</span></div>
<div>TOKENS_PURGED: <span class="telemetry-val">~75%</span></div>
<div>TOKENS_PURGED: <span class="telemetry-val">65%</span></div>
</div>
<section class="hero container">
@@ -363,7 +363,7 @@ document.addEventListener('mousemove', (e) => {
// --- Marquee Population ---
const items = [
{ k: "TOKENS_SAVED", v: "75%" }, { k: "ACCURACY", v: "100%" },
{ k: "TOKENS_SAVED", v: "65%" }, { k: "ACCURACY", v: "100%" },
{ k: "LATENCY_DROP", v: "3x" }, { k: "VIBES", v: "OOG" },
{ k: "PRICE", v: "$0.00" }, { k: "DEPENDENCIES", v: "NONE" }
];
@@ -417,7 +417,17 @@ cliInput.addEventListener('keydown', (e) => {
const val = cliInput.value.trim();
const row = document.createElement('div');
row.className = 'term-line';
row.innerHTML = `<span class="term-accent"></span> ${val}`;
// SECURITY FIX: Create DOM elements safely to prevent XSS
const prompt = document.createElement('span');
prompt.className = 'term-accent';
prompt.textContent = '';
const commandText = document.createElement('span');
commandText.textContent = ` ${val}`; // textContent automatically escapes HTML
row.appendChild(prompt);
row.appendChild(commandText);
cliInput.parentElement.before(row);
const res = document.createElement('div');
+59
View File
@@ -0,0 +1,59 @@
# Windows install fallback
If `irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex` fails on Windows (issues #249, #199, #72), set up plugin-skill activation by hand. This does **not** install the standalone hooks or the statusline — for those, run the unified Node installer afterwards: `npx -y github:JuliusBrussee/caveman -- --only claude` (or `node cli/install.js --only claude` from a clone).
```powershell
$ClaudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" }
$PluginSkillDir = Join-Path $ClaudeDir ".agents\plugins\caveman\skills\caveman"
$MarketplaceDir = Join-Path $ClaudeDir ".agents\plugins"
$MarketplaceFile = Join-Path $MarketplaceDir "marketplace.json"
# Copy SKILL.md into the plugin path (run from a clone of the repo)
New-Item -ItemType Directory -Path $PluginSkillDir -Force | Out-Null
Copy-Item ".\skills\caveman\SKILL.md" "$PluginSkillDir\SKILL.md" -Force
# Create or update marketplace.json with the caveman entry
New-Item -ItemType Directory -Path $MarketplaceDir -Force | Out-Null
if (Test-Path $MarketplaceFile) {
$marketplace = Get-Content $MarketplaceFile -Raw | ConvertFrom-Json
} else {
$marketplace = [pscustomobject]@{}
}
if (-not ($marketplace.PSObject.Properties.Name -contains "plugins")) {
$marketplace | Add-Member -NotePropertyName plugins -NotePropertyValue ([pscustomobject]@{})
}
$plugins = [ordered]@{}
foreach ($p in $marketplace.plugins.PSObject.Properties) { $plugins[$p.Name] = $p.Value }
$plugins["caveman"] = [ordered]@{ name = "caveman"; source = "JuliusBrussee/caveman"; version = "main" }
$marketplace.plugins = [pscustomobject]$plugins
$marketplace | ConvertTo-Json -Depth 10 | Set-Content -Path $MarketplaceFile -Encoding UTF8
```
Verify: `Test-Path "$PluginSkillDir\SKILL.md"` should print `True`. Restart Claude Code, then run `/caveman` to confirm the skill loads.
## Codex on Windows
1. Enable symlinks first: `git config --global core.symlinks true` (requires Developer Mode or admin).
2. Clone repo → Open VS Code → Codex Settings → Plugins → find "Caveman" under the local marketplace → Install → Reload Window.
3. Codex hooks are currently disabled on Windows, so use `$caveman` to start the mode manually each session.
## `npx skills` symlink fallback
`npx skills` uses symlinks by default. If symlinks fail, add `--copy`:
```powershell
npx skills add JuliusBrussee/caveman --copy
```
## Want it always on (any agent)?
Paste this into the agent's system prompt or rules file:
```
Terse like caveman. Technical substance exact. Only fluff die.
Drop: articles, filler (just/really/basically), pleasantries, hedging.
Fragments OK. Short synonyms. Code unchanged.
Pattern: [thing] [action] [reason]. [next step].
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift.
Code/commits/PRs: normal. Off: "stop caveman" / "normal mode".
```
+84
View File
@@ -0,0 +1,84 @@
# Evals
Measures real token compression of caveman skills by running the same
prompts through Claude Code under three conditions and comparing the
generated output token counts.
## The three arms
| Arm | System prompt |
|-----|--------------|
| `__baseline__` | none |
| `__terse__` | `Answer concisely.` |
| `<skill>` | `Answer concisely.\n\n{SKILL.md}` |
The honest delta for any skill is **`<skill>` vs `__terse__`** — i.e.
how much the skill itself adds on top of a plain "be terse" instruction.
Comparing a skill to the no-system-prompt baseline conflates the skill
with the generic terseness ask, which is what an earlier version of
this harness did and is why its numbers were inflated.
## Why this design
- **Real LLM output**, not hand-written examples (no circularity).
- **Same Claude Code** the skills target — no separate API key.
- **Snapshot committed to git** so CI runs are deterministic and free,
and so any change to the numbers is reviewable as a diff.
- **Control arm** isolates the skill's contribution from the generic
"be terse" effect.
## Files
- `prompts/en.txt` — fixed list of dev questions, one per line.
- `llm_run.py` — runs `claude -p --system-prompt …` per (prompt, arm),
captures real LLM output, writes `snapshots/results.json` along with
metadata (model, CLI version, generation timestamp).
- `measure.py` — reads the snapshot, counts tokens with tiktoken
`o200k_base`, prints a markdown table with median / mean / min / max /
stdev across prompts.
- `snapshots/results.json` — committed source of truth, regenerated only
when SKILL.md files or prompts change.
## Refresh the snapshot (requires `claude` CLI logged in)
```bash
uv run python evals/llm_run.py
```
This calls Claude once per prompt × (N skills + 2 control arms). Use
a small model to keep it cheap:
```bash
CAVEMAN_EVAL_MODEL=claude-haiku-4-5 uv run python evals/llm_run.py
```
## Read the snapshot (no LLM, no API key, runs in CI)
```bash
uv run --with tiktoken python evals/measure.py
```
## Adding a prompt
Append a line to `prompts/en.txt`, then refresh the snapshot.
## Adding a skill
Drop a `skills/<name>/SKILL.md`, then refresh the snapshot. `llm_run.py`
picks up every skill directory automatically.
## What this does NOT measure
- **Fidelity** — does the compressed answer preserve the technical
claims? A skill that replies `k` to everything would score 99% and
"win". A future v2 could add a judge-model rubric.
- **Latency or cost** — out of scope. Note that skills add input tokens
on every call, so output savings are not the full economic picture.
- **Cross-model behavior** — only the model used to generate the
snapshot is measured.
- **Exact Claude tokens** — `tiktoken o200k_base` is OpenAI's BPE and is
only an approximation of Claude's tokenizer. Ratios between arms are
meaningful; absolute numbers are approximate.
- **Statistical significance** — single run per (prompt, arm) at default
temperature. The min/max/stdev columns let you eyeball whether a
number is solid or noisy, but this is not a powered experiment.
+105
View File
@@ -0,0 +1,105 @@
"""
Run each prompt through Claude Code in three conditions and snapshot the
real LLM outputs:
1. baseline — no extra system prompt at all
2. terse — system prompt: "Answer concisely."
3. terse+skill — system prompt: "Answer concisely.\n\n{SKILL.md}"
The honest delta is (3) vs (2): how much does the SKILL itself add on top
of a plain "be terse" instruction? Comparing (3) vs (1) conflates the
skill with the generic terseness ask, which is what the previous version
of this harness did.
This is the source-of-truth generator. It calls a real LLM and produces
evals/snapshots/results.json. Run it locally when SKILL.md files change.
The CI-side `measure.py` only reads the snapshot and counts tokens.
Requires:
- `claude` CLI on PATH (Claude Code), authenticated
Run: uv run python evals/llm_run.py
Environment:
CAVEMAN_EVAL_MODEL optional --model flag value passed through to claude
"""
from __future__ import annotations
import datetime as dt
import json
import os
import subprocess
from pathlib import Path
EVALS = Path(__file__).parent
SKILLS = EVALS.parent / "skills"
PROMPTS = EVALS / "prompts" / "en.txt"
SNAPSHOT = EVALS / "snapshots" / "results.json"
TERSE_PREFIX = "Answer concisely."
def run_claude(prompt: str, system: str | None = None) -> str:
cmd = ["claude", "-p"]
if system:
cmd += ["--system-prompt", system]
if model := os.environ.get("CAVEMAN_EVAL_MODEL"):
cmd += ["--model", model]
cmd.append(prompt)
out = subprocess.run(cmd, capture_output=True, text=True, check=True)
return out.stdout.strip()
def claude_version() -> str:
try:
out = subprocess.run(
["claude", "--version"], capture_output=True, text=True, check=True
)
return out.stdout.strip()
except Exception:
return "unknown"
def main() -> None:
prompts = [p.strip() for p in PROMPTS.read_text().splitlines() if p.strip()]
skills = sorted(p.name for p in SKILLS.iterdir() if (p / "SKILL.md").exists())
print(
f"=== {len(prompts)} prompts × ({len(skills)} skills + 2 control arms) ===",
flush=True,
)
snapshot: dict = {
"metadata": {
"generated_at": dt.datetime.now(dt.timezone.utc).isoformat(),
"claude_cli_version": claude_version(),
"model": os.environ.get("CAVEMAN_EVAL_MODEL", "default"),
"n_prompts": len(prompts),
"terse_prefix": TERSE_PREFIX,
},
"prompts": prompts,
"arms": {},
}
print("baseline (no system prompt)", flush=True)
snapshot["arms"]["__baseline__"] = [run_claude(p) for p in prompts]
print("terse (control: terse instruction only, no skill)", flush=True)
snapshot["arms"]["__terse__"] = [
run_claude(p, system=TERSE_PREFIX) for p in prompts
]
for skill in skills:
skill_md = (SKILLS / skill / "SKILL.md").read_text()
system = f"{TERSE_PREFIX}\n\n{skill_md}"
print(f" {skill}", flush=True)
snapshot["arms"][skill] = [run_claude(p, system=system) for p in prompts]
SNAPSHOT.parent.mkdir(parents=True, exist_ok=True)
SNAPSHOT.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2))
print(f"\nWrote {SNAPSHOT}")
if __name__ == "__main__":
main()
+107
View File
@@ -0,0 +1,107 @@
"""
Read evals/snapshots/results.json (produced by llm_run.py) and report
real token compression per skill against the *terse control arm* — i.e.
how much the skill adds on top of a plain "Answer concisely." instruction.
Reports median, min, max and stdev across prompts, not just the mean,
so the reader can see whether a number is solid or noisy.
Tokenizer note: tiktoken o200k_base is OpenAI's tokenizer and is only an
approximation of Claude's BPE. The ratios are still meaningful for
comparing skills against each other, but the absolute numbers should be
read as "approximate output-length reduction", not "exact Claude tokens".
Run: uv run --with tiktoken python evals/measure.py
"""
from __future__ import annotations
import json
import statistics
from pathlib import Path
import tiktoken
ENCODING = tiktoken.get_encoding("o200k_base")
SNAPSHOT = Path(__file__).parent / "snapshots" / "results.json"
def count(text: str) -> int:
return len(ENCODING.encode(text))
def stats(savings: list[float]) -> tuple[float, float, float, float, float]:
return (
statistics.median(savings),
statistics.mean(savings),
min(savings),
max(savings),
statistics.stdev(savings) if len(savings) > 1 else 0.0,
)
def fmt_pct(x: float) -> str:
sign = "" if x < 0 else "+"
return f"{sign}{abs(x) * 100:.0f}%"
def main() -> None:
if not SNAPSHOT.exists():
print(f"No snapshot at {SNAPSHOT}. Run `python evals/llm_run.py` first.")
return
data = json.loads(SNAPSHOT.read_text())
arms = data["arms"]
meta = data.get("metadata", {})
baseline_tokens = [count(o) for o in arms["__baseline__"]]
terse_tokens = [count(o) for o in arms["__terse__"]]
print(f"_Generated: {meta.get('generated_at', '?')}_")
print(
f"_Model: {meta.get('model', '?')} · CLI: {meta.get('claude_cli_version', '?')}_"
)
print(f"_Tokenizer: tiktoken o200k_base (approximation of Claude's BPE)_")
print(
f"_n = {meta.get('n_prompts', len(baseline_tokens))} prompts, single run per arm_"
)
print()
print(f"**Reference arms (no skill):**")
print(f"- baseline (no system prompt): {sum(baseline_tokens)} tokens total")
print(
f"- terse control (`Answer concisely.`): {sum(terse_tokens)} tokens total "
f"({fmt_pct(1 - sum(terse_tokens) / sum(baseline_tokens))} vs baseline)"
)
print()
print("**Skills, measured as additional reduction on top of the terse control:**")
print()
print("| Skill | Median | Mean | Min | Max | Stdev | Tokens (skill / terse) |")
print("|-------|--------|------|-----|-----|-------|-------------------------|")
rows = []
for skill, outputs in arms.items():
if skill in ("__baseline__", "__terse__"):
continue
skill_tokens = [count(o) for o in outputs]
savings = [
1 - (s / t) if t else 0.0 for s, t in zip(skill_tokens, terse_tokens)
]
med, mean, lo, hi, sd = stats(savings)
rows.append(
(skill, med, mean, lo, hi, sd, sum(skill_tokens), sum(terse_tokens))
)
for row in sorted(rows, key=lambda r: -r[1]):
skill, med, mean, lo, hi, sd, st, tt = row
print(
f"| **{skill}** | {fmt_pct(med)} | {fmt_pct(mean)} | "
f"{fmt_pct(lo)} | {fmt_pct(hi)} | {sd * 100:.0f}% | {st} / {tt} |"
)
print()
print("_Savings = `1 - skill_tokens / terse_tokens` per prompt._")
print(f"_Source: {SNAPSHOT.name}. Refresh with `python evals/llm_run.py`._")
if __name__ == "__main__":
main()
+150
View File
@@ -0,0 +1,150 @@
"""
Generate a boxplot showing the distribution of token compression per
skill, compared against a plain "Answer concisely." control.
Reads evals/snapshots/results.json and writes:
- evals/snapshots/results.html (interactive plotly)
- evals/snapshots/results.png (static export for README/PR embed)
Run: uv run --with tiktoken --with plotly --with kaleido python evals/plot.py
"""
from __future__ import annotations
import json
import statistics
from pathlib import Path
import plotly.graph_objects as go
import tiktoken
ENCODING = tiktoken.get_encoding("o200k_base")
SNAPSHOT = Path(__file__).parent / "snapshots" / "results.json"
HTML_OUT = Path(__file__).parent / "snapshots" / "results.html"
PNG_OUT = Path(__file__).parent / "snapshots" / "results.png"
def count(text: str) -> int:
return len(ENCODING.encode(text))
def main() -> None:
data = json.loads(SNAPSHOT.read_text())
arms = data["arms"]
meta = data.get("metadata", {})
terse_tokens = [count(o) for o in arms["__terse__"]]
rows = []
for skill, outputs in arms.items():
if skill in ("__baseline__", "__terse__"):
continue
skill_tokens = [count(o) for o in outputs]
savings = [
(1 - (s / t)) * 100 if t else 0.0
for s, t in zip(skill_tokens, terse_tokens)
]
rows.append(
{"skill": skill, "savings": savings, "median": statistics.median(savings)}
)
rows.sort(key=lambda r: -r["median"]) # best first
fig = go.Figure()
for row in rows:
fig.add_trace(
go.Box(
y=row["savings"],
name=row["skill"],
boxpoints="all",
jitter=0.4,
pointpos=0,
marker=dict(color="#2ca02c", size=7, opacity=0.7),
line=dict(color="#2c3e50", width=2),
fillcolor="rgba(76, 120, 168, 0.25)",
boxmean=True,
hovertemplate="<b>%{x}</b><br>%{y:.1f}%<extra></extra>",
)
)
# zero line — "no effect"
fig.add_hline(
y=0,
line=dict(color="black", width=1.5, dash="dash"),
annotation_text="no effect (= same length as control)",
annotation_position="top right",
annotation_font=dict(size=11, color="black"),
)
# median labels above each box
for row in rows:
fig.add_annotation(
x=row["skill"],
y=max(row["savings"]),
text=f"<b>{row['median']:+.0f}%</b>",
showarrow=False,
yshift=22,
font=dict(size=16, color="#2c3e50"),
)
fig.update_layout(
title=dict(
text=f"<b>How much shorter does each skill make Claude's answers?</b><br>"
f"<sub>Distribution of per-prompt savings vs system prompt = "
f"<i>'Answer concisely.'</i><br>"
f"{meta.get('model', '?')} · n={meta.get('n_prompts', '?')} prompts · "
f"single run per arm</sub>",
x=0.5,
xanchor="center",
),
xaxis=dict(title="", automargin=True),
yaxis=dict(
title="↑ shorter · vs control · longer ↓",
ticksuffix="%",
zeroline=False,
gridcolor="rgba(0,0,0,0.08)",
range=[-30, 115],
),
plot_bgcolor="white",
height=560,
width=980,
margin=dict(l=140, r=80, t=120, b=120),
showlegend=False,
annotations=[
dict(
x=0.5,
y=-0.22,
xref="paper",
yref="paper",
showarrow=False,
font=dict(size=11, color="#555"),
text=(
"<b>box</b> = IQR (middle 50%) · "
"<b>line in box</b> = median · "
"<b>dashed line</b> = mean · "
"<b>green dots</b> = individual prompts"
),
)
],
)
# re-add labels after update_layout (which would otherwise wipe them)
for row in rows:
fig.add_annotation(
x=row["skill"],
y=max(row["savings"]),
text=f"<b>{row['median']:+.0f}%</b>",
showarrow=False,
yshift=22,
font=dict(size=16, color="#2c3e50"),
)
fig.write_html(HTML_OUT)
print(f"Wrote {HTML_OUT}")
fig.write_image(PNG_OUT, scale=2)
print(f"Wrote {PNG_OUT}")
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
Why does my React component re-render every time the parent updates?
Explain database connection pooling.
What's the difference between TCP and UDP?
How do I fix a memory leak in a long-running Node.js process?
What does the SQL EXPLAIN command tell me?
How does a hash table handle collisions?
Why am I getting CORS errors in my browser console?
What's the point of using a debouncer on a search input?
How does git rebase differ from git merge?
When should I use a queue vs a topic in messaging systems?
+95
View File
@@ -0,0 +1,95 @@
{
"metadata": {
"generated_at": "2026-04-08T22:01:24.027359+00:00",
"claude_cli_version": "2.1.97 (Claude Code)",
"model": "claude-opus-4-6",
"n_prompts": 10,
"terse_prefix": "Answer concisely."
},
"prompts": [
"Why does my React component re-render every time the parent updates?",
"Explain database connection pooling.",
"What's the difference between TCP and UDP?",
"How do I fix a memory leak in a long-running Node.js process?",
"What does the SQL EXPLAIN command tell me?",
"How does a hash table handle collisions?",
"Why am I getting CORS errors in my browser console?",
"What's the point of using a debouncer on a search input?",
"How does git rebase differ from git merge?",
"When should I use a queue vs a topic in messaging systems?"
],
"arms": {
"__baseline__": [
"Because by default, React re-renders all children when a parent re-renders, regardless of whether their props changed.\n\nTo prevent it, wrap the component in `React.memo`:\n\n```jsx\nconst Child = React.memo(function Child(props) { ... });\n```\n\nCaveats:\n- `memo` does a shallow prop comparison. New object/array/function props created inline in the parent (`<Child onClick={() => ...} data={{...}} />`) break it — wrap those in `useCallback`/`useMemo`.\n- Children passed via `children` or other JSX props are new elements each render, so `memo` won't help there.\n- Context updates still re-render consumers regardless of `memo`.\n\nIf you share the component + parent code, I can point to the exact cause.",
"Database connection pooling maintains a cache of reusable DB connections instead of opening/closing one per query.\n\n**Why:** Opening a TCP + auth handshake per query is expensive (often 10100ms). Pools amortize that cost.\n\n**How it works:**\n1. Pool opens N connections at startup (or lazily).\n2. App \"checks out\" a connection, runs queries, \"returns\" it.\n3. Idle connections stay open, ready for reuse.\n4. If all are busy, requests wait or the pool grows up to `max_size`.\n\n**Key knobs:**\n- `min_size` / `max_size` — floor and ceiling of connections.\n- `max_idle_time` — close connections idle too long.\n- `max_lifetime` — recycle old connections (avoids stale TCP state).\n- `timeout` — how long a checkout waits before erroring.\n\n**Gotchas:**\n- **Sizing**: `max_size` × app instances must stay below DB's `max_connections`. Postgres typically handles ~100200 well; beyond that use PgBouncer.\n- **Transaction leaks**: forgetting to release a connection starves the pool.\n- **Serverless**: short-lived functions defeat pooling — use an external pooler (PgBouncer, RDS Proxy, Neon pooler).\n- **Async frameworks** (FastAPI + asyncpg/SQLAlchemy async): pool is per-process; workers multiply it.\n\n**Common libraries:** HikariCP (Java), pgbouncer (external, Postgres), SQLAlchemy `QueuePool`, asyncpg `create_pool`, node-postgres `Pool`.",
"TCP is connection-oriented, reliable, and ordered: it establishes a handshake, retransmits lost packets, guarantees delivery and sequence, and handles flow/congestion control. Used for HTTP, SSH, email.\n\nUDP is connectionless and best-effort: no handshake, no retransmission, no ordering, minimal overhead. Faster and lighter, but packets can be lost or arrive out of order. Used for DNS, video/voice streaming, games, QUIC.\n\nKey tradeoff: TCP = reliability, UDP = low latency.",
"Common approaches:\n\n1. **Find the leak** — run with `--inspect`, take heap snapshots in Chrome DevTools at intervals, compare retained objects. Or use `clinic.js doctor` / `heapdump`.\n\n2. **Usual suspects**:\n - Unbounded caches/Maps — use `LRUCache` with a max size.\n - Event listeners not removed — check `emitter.listenerCount()`, use `once()` or `off()`.\n - Closures holding large scopes — especially in callbacks/timers.\n - Global arrays that only grow (logs, metrics buffers).\n - Timers (`setInterval`) keeping refs alive — `clearInterval` on shutdown, or `.unref()`.\n - Promises that never settle, holding their chain in memory.\n\n3. **Measure** — log `process.memoryUsage().heapUsed` over time; a steadily rising baseline across GCs confirms a leak (vs. normal sawtooth).\n\n4. **Bound it** — run with `--max-old-space-size=N` so it crashes loudly instead of thrashing, and use a supervisor (pm2, systemd) to restart.\n\nIf you share the code or a heap snapshot I can point at the specific culprit.",
"`EXPLAIN` shows the query planner's execution plan for a SQL statement — how the database intends to run it, without actually executing it (use `EXPLAIN ANALYZE` to run it and get real timings).\n\nKey things it reveals:\n- **Scan type**: Seq Scan (full table) vs Index Scan / Index Only Scan / Bitmap Scan — tells you if indexes are being used.\n- **Join strategy**: Nested Loop, Hash Join, Merge Join.\n- **Estimated cost**: `cost=startup..total`, `rows=`, `width=` (bytes/row). Costs are in arbitrary planner units, not ms.\n- **Order of operations**: read bottom-up, inside-out.\n- **Filters vs Index Conds**: a `Filter:` after a Seq Scan means rows were read then discarded — often a missing index.\n\nWith `EXPLAIN ANALYZE` you also get `actual time`, `rows`, and `loops`. Big gaps between estimated and actual rows usually mean stale stats (`ANALYZE` the table). Add `BUFFERS` to see cache vs disk reads.",
"Two main strategies:\n\n**1. Separate chaining** — each bucket stores a linked list (or tree) of entries. Colliding keys get appended. Lookup scans the chain. Used by Java's `HashMap`, Python's `dict` historically is different.\n\n**2. Open addressing** — on collision, probe for another empty slot in the array itself:\n- *Linear probing*: try `i+1, i+2, ...` (cache-friendly but clusters)\n- *Quadratic probing*: try `i+1, i+4, i+9, ...` (reduces clustering)\n- *Double hashing*: step size from a second hash function\n\nPython's `dict` and Ruby's `Hash` use open addressing; Go's `map` uses chaining with bucket arrays.\n\nBoth degrade as load factor rises, so tables resize (typically at 0.70.75) to keep average lookup O(1).",
"CORS errors happen when your frontend (origin A) makes a request to a backend (origin B) and the server doesn't return the `Access-Control-Allow-Origin` header permitting origin A. The browser enforces this — the request often reaches the server, but the response is blocked.\n\nCommon causes:\n- **Missing CORS middleware** on the backend (e.g., FastAPI needs `CORSMiddleware`, Express needs `cors`).\n- **Origin mismatch**: `localhost:3000` ≠ `127.0.0.1:3000`, `http` ≠ `https`, wrong port.\n- **Preflight (OPTIONS) failing**: non-simple requests (custom headers, `PUT`/`DELETE`, `application/json` with auth) trigger an OPTIONS preflight the server must handle.\n- **Credentials**: if sending cookies/auth, you need `Access-Control-Allow-Credentials: true` AND an explicit origin (not `*`).\n- **Wildcard + credentials**: `Allow-Origin: *` is rejected when credentials are included.\n\nTo help more specifically: what's the exact error message, and what stack is your backend?",
"A debouncer delays firing the search until the user pauses typing (e.g., 300ms of no keystrokes). Without it, every keystroke triggers a search — typing \"react\" fires 5 requests.\n\nBenefits:\n- **Fewer API calls** — cuts network load and backend cost\n- **Avoids race conditions** — stale responses arriving out of order\n- **Better UX** — no flickering results as the user types\n- **Less CPU work** — for expensive client-side filtering\n\nThrottle vs debounce: throttle fires at a fixed rate during activity; debounce waits for silence. Search inputs want debounce because you only care about the final query.",
"**Merge**: Combines branches by creating a new \"merge commit\" that ties their histories together. Preserves full history, including the branch topology. Non-destructive.\n\n**Rebase**: Replays your branch's commits on top of another branch, rewriting them as new commits. Produces a linear history with no merge commit.\n\nKey differences:\n- **History shape**: merge = branching graph; rebase = straight line\n- **Commit hashes**: merge preserves them; rebase rewrites them\n- **Conflicts**: merge resolves once; rebase may resolve per-commit\n- **Safety**: merge is safe on shared branches; rebase should be avoided on already-pushed/shared branches (rewrites history)\n\nRule of thumb: rebase local work to clean it up before sharing; merge when integrating shared branches.",
"**Queue**: point-to-point. One message → one consumer. Use for work distribution where each job must be processed exactly once (task queues, order processing, background jobs). Multiple workers compete; load balances across them.\n\n**Topic**: pub/sub. One message → many subscribers, each gets a copy. Use for broadcasting events to independent consumers (event notifications, cache invalidation, fan-out to analytics + audit + email).\n\nRule of thumb: \"who needs to react?\"\n- One of N workers → queue\n- All interested parties → topic\n\nModern brokers blur the line (Kafka topics with consumer groups act queue-like; SNS+SQS combines both)."
],
"__terse__": [
"React components re-render by default whenever their parent re-renders, regardless of whether their props changed. Common causes and fixes:\n\n1. **Default behavior** — Wrap the child in `React.memo(MyComponent)` to skip re-renders when props are shallow-equal.\n\n2. **New object/array/function props each render** — Even with `memo`, passing `{}`, `[]`, or inline `() => {}` creates new references every render, breaking memoization. Fix with `useMemo` / `useCallback` in the parent.\n\n3. **Children prop / context** — `memo` doesn't help if you pass `children` that change, or if the component consumes a context whose value changes.\n\n4. **New reference from hooks** — e.g. `useState` setters are stable, but values derived inline aren't.\n\nQuick check: add `console.log` with the props and see which one changes identity. If you share the component + parent code I can point to the exact culprit.",
"Database connection pooling maintains a cache of reusable database connections instead of opening/closing one per request.\n\n**Why it matters:** Opening a DB connection is expensive — TCP handshake, TLS, auth, session setup (often 50200ms). Doing this per query kills throughput.\n\n**How it works:**\n1. Pool pre-opens N connections at startup.\n2. App \"checks out\" a connection, runs queries, \"returns\" it.\n3. Idle connections stay warm, ready for reuse.\n4. If all are busy, new requests wait (or the pool grows up to a max).\n\n**Key parameters:**\n- `min_size` / `max_size` — floor and ceiling of connections\n- `timeout` — max wait for a free connection\n- `max_lifetime` / `idle_timeout` — recycle stale connections\n- `max_overflow` — temporary burst capacity above max_size\n\n**Common tools:** SQLAlchemy's `QueuePool`, `asyncpg.create_pool`, HikariCP (Java), PgBouncer (external pooler sitting in front of Postgres).\n\n**Gotchas:**\n- Pool size > DB's `max_connections` → errors. Size per-process, multiply by workers.\n- Long transactions starve the pool.\n- Serverless/short-lived processes benefit from an external pooler (PgBouncer) in transaction mode.\n- Connections can go stale (network drops, DB restarts) — use health checks / `pre_ping`.\n\n**Rule of thumb:** pool size ≈ `(cores * 2) + effective_spindles`, tuned by load testing. Bigger isn't better — contention at the DB beats connection reuse gains.",
"TCP is connection-oriented, reliable, and ordered: it establishes a handshake, guarantees delivery, retransmits lost packets, and preserves sequence — used for HTTP, SSH, email.\n\nUDP is connectionless and best-effort: no handshake, no delivery guarantee, no ordering, lower latency and overhead — used for DNS, video/voice streaming, games.\n\nKey tradeoff: TCP = reliability, UDP = speed.",
"Common approaches:\n\n1. **Profile heap**: Run with `node --inspect` and use Chrome DevTools → Memory → take heap snapshots at intervals, compare retained size to find growing objects.\n2. **Usual culprits**:\n - Unbounded caches/Maps → use `lru-cache` or `WeakMap`.\n - Event listeners not removed → check `emitter.listenerCount()`, call `off()`/`removeListener()`.\n - Closures holding large scopes (e.g. in timers, promises).\n - Global arrays that only grow.\n - Unclosed DB/HTTP connections or streams.\n3. **Monitor**: log `process.memoryUsage().heapUsed` over time; use `--max-old-space-size` only as a bandaid.\n4. **Tools**: `clinic.js doctor`, `heapdump`, `0x`, or `--heap-prof` flag for sampling.\n5. **Reproduce in isolation**: load-test one endpoint/job at a time to localize the leak.\n\nStart with a heap snapshot diff — it usually points straight at the retainer.",
"SQL `EXPLAIN` shows the query execution plan — how the database will run your query. Key info:\n\n- **Access method**: sequential scan vs index scan vs index-only scan\n- **Join strategy**: nested loop, hash join, merge join\n- **Row estimates**: how many rows the planner expects at each step\n- **Cost estimates**: relative startup/total cost units\n- **Order of operations**: which tables/filters/sorts happen first\n\nUse `EXPLAIN ANALYZE` to actually execute the query and get real timings + actual row counts (vs estimates), which is what you need to spot bad plans (e.g., estimate says 10 rows, reality is 10M → stale stats or bad selectivity).\n\nCommon things to look for:\n- Seq Scan on large tables → missing index\n- Big gap between estimated and actual rows → run `ANALYZE`\n- Nested loop over huge row counts → join/index problem\n- Sort spilling to disk → need `work_mem` or an index providing order",
"Hash tables handle collisions with two main strategies:\n\n1. **Separate chaining**: Each bucket holds a linked list (or tree) of entries. Collisions append to the list. Lookup scans the chain. Java's `HashMap` uses this (converting to a tree after 8 entries).\n\n2. **Open addressing**: Store entries directly in the array; on collision, probe for another slot:\n - **Linear probing**: try `i+1, i+2, ...`\n - **Quadratic probing**: try `i+1², i+2², ...`\n - **Double hashing**: step size from a second hash function\n\nChaining is simpler and degrades gracefully; open addressing is cache-friendlier but needs resizing before load factor gets high (~0.7).",
"CORS errors happen when your frontend (origin A) requests a resource from a server (origin B) that doesn't return the right `Access-Control-Allow-*` headers. The browser blocks the response — the request often reaches the server fine.\n\nCommon causes:\n1. **Missing header** — server doesn't send `Access-Control-Allow-Origin` matching your frontend origin.\n2. **Wildcard + credentials** — using `Access-Control-Allow-Origin: *` with `credentials: 'include'`. You must echo the exact origin and set `Access-Control-Allow-Credentials: true`.\n3. **Preflight failing** — non-simple requests (custom headers, `PUT`/`DELETE`, `application/json`) trigger an `OPTIONS` preflight. Server must handle `OPTIONS` and return `Access-Control-Allow-Methods` / `Access-Control-Allow-Headers`.\n4. **Protocol/port mismatch** — `http://localhost:3000` ≠ `http://localhost:8000` ≠ `https://...`. All count as different origins.\n5. **Redirects** — CORS headers must be on the final response; redirects can strip them.\n\nQuick fixes by stack:\n- **FastAPI**: `app.add_middleware(CORSMiddleware, allow_origins=[...], allow_credentials=True, allow_methods=[\"*\"], allow_headers=[\"*\"])`\n- **Express**: `app.use(cors({ origin: '...', credentials: true }))`\n- **Dev only**: use a Vite/Next proxy so requests are same-origin.\n\nShare the exact error message and your frontend/backend origins and I can pinpoint it.",
"A debouncer delays running the search until the user stops typing for a short interval (e.g., 300ms). Benefits:\n\n- **Fewer API calls / DB queries**: avoids firing a request on every keystroke (\"a\", \"ap\", \"app\"...) — only one fires after typing pauses.\n- **Lower cost & server load**: especially important for expensive backends or rate-limited APIs.\n- **Better UX**: prevents flickering results and out-of-order responses racing each other.\n- **Saves client work**: less re-rendering and state churn.\n\nRelated: *throttle* caps frequency (e.g., 1/sec), while *debounce* waits for silence — debounce is usually what you want for search inputs.",
"**Merge**: Combines branches by creating a new merge commit that ties both histories together. Preserves the full branch history and is non-destructive.\n\n**Rebase**: Replays your branch's commits on top of another branch, creating new commits with new hashes. Produces a linear history but rewrites commits.\n\nKey differences:\n- **History**: merge = branching graph; rebase = straight line\n- **Safety**: merge is safe on shared branches; rebase should be avoided on pushed/shared branches (rewrites history)\n- **Conflicts**: merge resolves once; rebase may require resolving per replayed commit\n- **Traceability**: merge keeps context of when/where branches diverged; rebase loses it\n\nRule of thumb: rebase local/private work to clean up, merge for integrating shared branches.",
"**Queue** (point-to-point): one message → one consumer. Use for work distribution, task processing, load balancing across workers. Each job should be handled exactly once (e.g., order processing, email sending, image resizing).\n\n**Topic** (pub/sub): one message → many subscribers. Use for event broadcasting where multiple independent systems react to the same event (e.g., \"OrderPlaced\" → inventory, billing, analytics, notifications all consume it).\n\n**Rule of thumb:**\n- \"Who does this work?\" → queue\n- \"Who needs to know?\" → topic\n\nModern brokers blur the line (Kafka topics with consumer groups act like queues per group; SNS+SQS fan-out combines both), but the semantic distinction still drives the design choice."
],
"caveman": [
"Parent re-render → child re-render by default. Props \"change\" each render if inline obj/array/fn → new ref → child sees new props.\n\nFix:\n- Wrap child: `React.memo(Child)`\n- Stabilize props: `useMemo` for obj/array, `useCallback` for fn\n- Lift state so parent re-render not needed\n\nCheck: if child memo'd but still re-renders, inline prop is culprit.",
"Pool = reuse open DB conns. No new conn per req → skip TCP+auth handshake. Fixed max size cap load on DB. Idle conns recycled. Win: latency ↓, throughput ↑.",
"TCP: connection-based. Handshake, ordered, reliable, retransmit lost packets, flow/congestion control. Slower. Use: web, SSH, files.\n\nUDP: connectionless. No handshake, no order, no retransmit. Fast, low overhead. Use: video, games, DNS, VoIP.",
"Use caveman skill (requested mode by system).\n\n**Find leak:**\n- Run with `node --inspect`. Open `chrome://inspect` → Memory tab.\n- Take 3 heap snapshots over time. Compare. Look \"Retained Size\" growing.\n- Or `clinic doctor -- node app.js` for quick diagnosis.\n\n**Common causes:**\n- Global vars / caches no eviction → use `lru-cache` with max.\n- Event listeners not removed → `emitter.off()` on cleanup. Watch `MaxListenersExceededWarning`.\n- Closures holding big objects → null refs when done.\n- Timers (`setInterval`) never cleared → `clearInterval`.\n- Promises never resolve → pile up.\n\n**Fix pattern:**\n```js\nconst cache = new LRU({ max: 500 });\nemitter.on('x', handler);\n// cleanup\nemitter.off('x', handler);\nclearInterval(timer);\n```\n\n**Monitor prod:** `process.memoryUsage().heapUsed`. Alert on growth trend. Restart worker if threshold hit (pm2/k8s).\n\nRoot cause fix > restart band-aid.",
"EXPLAIN show query plan. DB tell you *how* it run query, not result.\n\nReveal:\n- **Scan type**: seq scan (full table) vs index scan vs bitmap scan\n- **Join strategy**: nested loop, hash join, merge join\n- **Row estimates**: planner guess of rows per step\n- **Cost**: startup..total (arbitrary units)\n- **Order**: which step first, which last\n\n`EXPLAIN ANALYZE` = actually run query + show real time + real rows. Compare est vs actual → spot bad stats.\n\nUse for: find missing index (seq scan on big table), bad join order, wrong row estimate → `ANALYZE table`.",
"Caveman mode on.\n\nTwo main way:\n\n1. **Chaining**: each bucket hold linked list (or tree). Collide → append to list. Lookup walk list.\n2. **Open addressing**: collide → probe next slot. Variants: linear probe (+1), quadratic (+i²), double hash (step = hash2(key)).\n\nLoad factor high → resize + rehash. Java `HashMap` chain→tree at 8. Python dict + Go map use open addressing.",
"CORS = server no send `Access-Control-Allow-Origin` header match your origin. Browser block response.\n\nCommon cause:\n- Server miss CORS middleware\n- Origin not in allowlist\n- Preflight (OPTIONS) fail — need `Allow-Methods`, `Allow-Headers`\n- Credentials mode: need `Allow-Credentials: true` + explicit origin (no `*`)\n\nFix FastAPI:\n```python\nfrom fastapi.middleware.cors import CORSMiddleware\napp.add_middleware(CORSMiddleware, allow_origins=[\"http://localhost:3000\"], allow_credentials=True, allow_methods=[\"*\"], allow_headers=[\"*\"])\n```\n\nPaste exact error → me pinpoint.",
"Debouncer delay fn call until user stop typing. Without: every keystroke → API hit → spam backend, race conditions, laggy UI. With: wait ~300ms idle → one request. Saves cost, cuts load, better UX.",
"Merge = combine branches, make new merge commit. Keep history as-is, show branching.\n\nRebase = replay your commits on top of target branch. Rewrite history, linear shape.\n\nKey diff:\n- **Merge**: non-destructive, preserve context, extra merge commit, messy graph\n- **Rebase**: clean linear log, rewrite SHAs, dangerous on shared branches\n\nRule: merge for shared/public branches, rebase for local cleanup before push.",
"Queue = point-to-point. One msg → one consumer. Work distribution, load balance. Ex: job processing.\n\nTopic = pub/sub. One msg → many subscribers. Event broadcast, fan-out. Ex: order placed → notify billing + inventory + email.\n\nRule: task need doing once? Queue. Event many care about? Topic."
],
"caveman-cn": [
"父 render -> 子收到新 props 引用 -> 子重渲染。\n\n修:\n- `React.memo(Child)` 包子组件。\n- 父传的对象/数组/函数用 `useMemo` / `useCallback` 稳引用。\n- 否则 memo 浅比较次次 fail。",
"池 = 复用 DB 连接。省握手 -> 更快。限并发 -> 保护 DB。",
"TCP:有连接。三次握手。保序。重传。流控/拥塞控。慢但可靠。→ HTTP、SSH、DB。\n\nUDP:无连接。发了就忘。无序。可丢。无重传。快、开销小。→ DNS、视频、游戏、QUIC 底层。",
"用 `--inspect` 启动。Chrome DevTools -> Memory -> 拍两张 heap snapshot(间隔负载后)。对比 \"Comparison\" 视图,看 Delta 正的对象。\n\n常见源头:\n- 闭包抓大对象\n- 全局 `Map`/`Set` 只加不删 -> 换 `WeakMap`/`WeakRef` 或加 TTL\n- 监听器泄漏 -> `emitter.setMaxListeners`,记得 `off()`\n- 定时器未 `clearInterval`\n- 缓存无上限 -> 用 `lru-cache`\n\n监控:`process.memoryUsage().heapUsed` 打点。或 `clinic heapprofiler` / `0x`。\n\n复现:`node --expose-gc`,循环后 `global.gc()`,看 RSS 是否回落。不回落 = 泄漏。",
"查询执行计划。DB 怎么跑你的 SQL:用哪个 index、join 顺序、扫全表还是走索引、行数估算、成本。\n\nPostgres 用 `EXPLAIN ANALYZE` -> 真实执行时间 + 实际行数。对比 estimated vs actual 找烂计划。\n\n关键字段:\n- `Seq Scan` -> 全表扫,通常坏\n- `Index Scan` -> 走索引,好\n- `rows=` -> 优化器估算\n- `cost=` -> 相对开销\n- `Nested Loop` / `Hash Join` / `Merge Join` -> join 策略\n\n用途:debug 慢查询,决定加哪个 index。",
"链地址法:桶存链表/树,冲突元素串一起。开放寻址:另找空槽——线性探测、二次探测、双重哈希。负载因子过高 -> 扩容 rehash。Java 8 `HashMap`:链表长度 >8 转红黑树。",
"CORS = 浏览器拦。服务器没回 `Access-Control-Allow-Origin` 头,或回的 origin 不匹配。\n\n修:\n- 后端加头:`Access-Control-Allow-Origin: <你的前端域>`(或 `*`,但带 cookie 不行)\n- 预检 `OPTIONS` 要回 200 + `Allow-Methods` / `Allow-Headers`\n- 带 cookie`Allow-Credentials: true` + 明确 origin(不能 `*`\n- FastAPI`CORSMiddleware`。Express`cors` 包。\n\n贴报错原文 + 请求/响应头,能更准。",
"防止每字触发请求。用户打字 -> 等停顿(如300ms) -> 才发请求。省 API 调用、减抖动、少无用渲染。",
"merge: 保留两条历史,生成 merge commit。非破坏性。历史有分叉。\n\nrebase: 把你的 commit 摘下来,重放到目标分支顶端。线性历史。commit 变新 hash (重写)。\n\n选择:\n- 公共分支 -> merge (别重写共享历史)\n- 本地/私有分支整理 -> rebase\n- 想保留合并上下文 -> merge\n- 想干净线性 log -> rebase\n\n冲突:merge 解一次。rebase 可能每个 commit 解一次。",
"队列 = 点对点。一消息一消费者。工作分发、任务处理。\n\n主题 = 发布/订阅。一消息多订阅者。事件广播、扇出。\n\n选队列:负载均衡 workers、订单处理、job queue。\n选主题:多系统响应同事件(如 `user.created` -> email + analytics + CRM)。"
],
"caveman-es": [
"Padre re-render → hijos re-render por default. Props nuevas cada vez (obj/array/fn inline = ref nueva) rompen bail-out.\n\nFix:\n- `React.memo(Child)` para comparar props shallow\n- `useMemo`/`useCallback` en padre para props estables\n- Evitar `<Child data={{...}} onClick={() => ...} />` inline\n\nSin memo, React re-renderiza hijos siempre aunque props iguales.",
"Pool = reusar conexiones BD abiertas. Evita handshake TCP + auth por cada request → lento bajo carga.\n\n**Cómo funciona:**\n- App pide conn → pool da una libre (o crea si < max)\n- App termina → conn vuelve al pool, no se cierra\n- Pool lleno → request espera o falla\n\n**Params clave:**\n- `min`: conns calientes siempre\n- `max`: techo (proteger BD de saturación)\n- `idle_timeout`: matar conns ociosas\n- `acquire_timeout`: cuánto esperar conn libre\n\n**Gotchas:**\n- Max muy alto → BD muere (Postgres ~100 conns default)\n- Serverless → usar pooler externo (PgBouncer, Supavisor). Lambdas efímeras rompen pools in-process\n- Transacciones largas bloquean pool → leaks\n\nLibs: HikariCP (Java), pgbouncer, SQLAlchemy pool, `pg` node.",
"TCP: conexión, confiable, ordenado, handshake, retransmite pérdidas, control flujo/congestión. Lento pero seguro. Web/SSH/SQL.\n\nUDP: sin conexión, sin garantía, sin orden, cero handshake. Rápido, ligero. Pierde paquetes sin avisar. DNS/video/juegos/VoIP.\n\nClave: TCP = llega todo bien. UDP = llega rápido o no llega.",
"Pasos:\n\n1. **Reproducir + medir**: `node --inspect` + Chrome DevTools → Memory tab. O `process.memoryUsage()` en loop.\n2. **Heap snapshots**: tomar 3 (baseline, medio, tarde). DevTools \"Comparison\" → ver qué objetos crecen.\n3. **Sospechosos comunes**:\n - Listeners no removidos (`emitter.on` sin `off`) → `EventEmitter` warning a 10+\n - Closures reteniendo refs grandes\n - Caches sin límite (usar `lru-cache`)\n - Globals/singletons acumulando\n - Timers (`setInterval`) sin `clearInterval`\n - Promesas colgadas reteniendo scope\n4. **Fix**: limpiar listeners en cleanup, TTL en caches, `WeakMap`/`WeakRef` para refs opcionales.\n5. **Verificar**: snapshot post-fix, memoria estable bajo carga (`autocannon`/`k6`).\n\nHerramientas: `clinic.js doctor`, `heapdump`, `--heap-prof`.",
"EXPLAIN muestra plan ejecución query. Revela: orden joins, índices usados (o no), método scan (seq/index), filas estimadas, costo. `EXPLAIN ANALYZE` corre query real → tiempos + filas reales vs estimadas. Usar para diagnosticar queries lentas.",
"Colisiones hash — dos claves → mismo bucket. Soluciones principales:\n\n**1. Chaining (encadenamiento):** Bucket guarda lista enlazada. Colisión → append a lista. Lookup = hash + scan lista. Java `HashMap` usa esto (lista → árbol si >8).\n\n**2. Open addressing (direccionamiento abierto):** Una entrada por bucket. Colisión → probar otro slot:\n- **Linear probing:** `i+1, i+2...` (rápido, pero clustering)\n- **Quadratic:** `i+1², i+2²...` (menos clusters)\n- **Double hashing:** segundo hash como paso\n\nPython `dict`, Go `map` usan open addressing.\n\n**Trade-offs:** Chaining tolera load factor >1, simple borrado. Open addressing mejor cache locality, pero requiere resize antes (~0.7 load factor) y borrado usa tombstones.",
"Browser bloquea request cross-origin. Server no manda header `Access-Control-Allow-Origin` para tu origen.\n\nFix rápido:\n- **Backend**: agregar CORS middleware permitiendo tu origen frontend (ej FastAPI: `CORSMiddleware` con `allow_origins=[\"http://localhost:3000\"]`).\n- **Preflight `OPTIONS` falla** → permitir métodos/headers usados (`allow_methods`, `allow_headers`).\n- **Credentials (cookies)** → `allow_credentials=True` + origen explícito (no `*`).\n- **Dev hack**: proxy en dev server (Vite/Next) para evitar cross-origin.\n\nPegá el error exacto + stack (frontend/backend) para fix preciso.",
"Debouncer espera usuario parar tecleo antes disparar búsqueda. Sin él: request por cada tecla → spam API, renders extra, resultados desordenados (race). Con él (~300ms): una sola query tras pausa → menos carga servidor, menos costo, UX suave.",
"Rebase: reescribe commits encima de base nueva. Historia lineal, sin merge commit. Cambia SHAs → no usar en ramas compartidas.\n\nMerge: une ramas con commit nuevo. Preserva historia real, no destructivo. Seguro en ramas públicas.\n\nRegla: rebase local, merge público.",
"Cola: 1 productor → 1 consumidor (de grupo). Mensaje entregado 1 vez. Work distribution, tareas, jobs.\n\nTópico: 1 productor → N suscriptores. Cada uno recibe copia. Pub/sub, eventos, broadcast.\n\nRegla: ¿trabajo a repartir? cola. ¿evento a anunciar? tópico.\n\nHíbrido (Kafka/SNS+SQS): tópico con consumer groups = ambos. Cada grupo = cola lógica."
],
"compress": [
"React re-renders children by default when the parent renders. To prevent it:\n\n1. **Wrap in `React.memo`** — skips re-render if props are shallow-equal:\n ```jsx\n export default React.memo(MyComponent);\n ```\n\n2. **Stabilize prop references** — objects/arrays/functions created inline break memo. Use `useMemo`/`useCallback` in the parent:\n ```jsx\n const data = useMemo(() => ({...}), [deps]);\n const onClick = useCallback(() => {...}, [deps]);\n ```\n\n3. **Check context** — any component consuming a context re-renders when that context value changes.\n\nMost common cause: passing `{}`, `[]`, or `() => {}` inline as props defeats `memo`.",
"Database connection pooling reuses a set of open DB connections instead of opening/closing one per query.\n\n**Why:** Opening a TCP + auth handshake is expensive (often 10100ms). Pooling amortizes that cost.\n\n**How it works:**\n1. Pool opens N connections at startup.\n2. App \"checks out\" a connection, runs queries, \"returns\" it.\n3. Idle connections stay warm; pool enforces min/max size, timeouts, and health checks.\n\n**Key settings:**\n- `min_size` / `max_size` — floor/ceiling of connections\n- `max_idle_time` — recycle stale conns\n- `acquire_timeout` — fail fast when pool exhausted\n\n**Gotchas:**\n- Pool size > DB `max_connections` → errors. Size per-process; multiply by workers.\n- Long transactions starve the pool.\n- Serverless/short-lived functions → use an external pooler (PgBouncer, RDS Proxy).\n\n**Examples:** SQLAlchemy `QueuePool`, asyncpg `create_pool`, HikariCP (Java), PgBouncer (external).",
"TCP is connection-oriented, reliable, ordered, with handshakes and retransmission (web, email, SSH). UDP is connectionless, unreliable, no ordering, low overhead (DNS, video, games).",
"Common causes and fixes:\n\n1. **Profile first** — `node --inspect` + Chrome DevTools Memory tab, or `clinic doctor`. Take heap snapshots at intervals and diff them.\n\n2. **Frequent culprits**:\n - **Unbounded caches/Maps** → use `lru-cache` with max size\n - **Event listener leaks** → check `emitter.listenerCount()`; always `removeListener` on cleanup; raise/audit `setMaxListeners`\n - **Closures holding large objects** → avoid capturing big scopes in long-lived callbacks\n - **Global arrays that only grow** (logs, metrics buffers) → cap or flush\n - **Timers/intervals not cleared** → `clearInterval` on shutdown\n - **Promises never settling** → retained forever\n\n3. **Tools**: `heapdump`, `v8.writeHeapSnapshot()`, `process.memoryUsage()`, `--max-old-space-size` to delay OOM while debugging (not a fix).\n\n4. **Pattern**: snapshot → run workload → snapshot → compare retained size by constructor. Look for objects growing linearly with request count.\n\n5. **WeakMap/WeakRef** for caches keyed by objects you don't want to retain.\n\nStart with a heap diff — guessing wastes time.",
"SQL `EXPLAIN` shows the query execution plan — how the database will run your query. Key info:\n\n- **Access method**: sequential scan vs index scan vs index-only scan\n- **Join strategy**: nested loop, hash join, merge join\n- **Row estimates**: how many rows the planner expects at each step\n- **Cost estimates**: relative cost units (startup..total)\n- **Order of operations**: which tables/filters/joins run first\n\nUse `EXPLAIN ANALYZE` to actually execute the query and get real timings + actual row counts, which reveals bad estimates and slow steps. Add `BUFFERS` (Postgres) to see cache hits/disk reads.\n\nMain use: finding missing indexes, bad join orders, and costly seq scans.",
"Hash tables handle collisions mainly two ways:\n\n1. **Separate chaining**: each bucket stores a linked list (or tree) of entries that hash to the same index. On collision, append to the list. Lookup scans the list.\n\n2. **Open addressing**: store all entries in the array itself; on collision, probe for another slot:\n - *Linear probing*: try i+1, i+2, …\n - *Quadratic probing*: try i+1², i+2², …\n - *Double hashing*: step size from a second hash function.\n\nBoth degrade as load factor rises, so tables resize/rehash past a threshold (e.g., 0.75).",
"CORS errors happen when your frontend (e.g. `http://localhost:3000`) makes a request to a backend on a different origin (different scheme/host/port) and the server doesn't return the expected `Access-Control-Allow-*` headers.\n\nCommon causes:\n- **Missing headers**: server doesn't send `Access-Control-Allow-Origin` matching your frontend origin.\n- **Preflight fails**: browser sends `OPTIONS` before `POST`/`PUT`/custom headers; server must respond 2xx with `Allow-Methods` / `Allow-Headers`.\n- **Credentials mismatch**: using `credentials: 'include'` requires `Access-Control-Allow-Credentials: true` AND a specific origin (not `*`).\n- **Wrong URL**: hitting `http://` vs `https://`, or `localhost` vs `127.0.0.1`.\n\nFix (FastAPI example):\n```python\nfrom fastapi.middleware.cors import CORSMiddleware\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"http://localhost:3000\"],\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n```\n\nShare the exact error message + your stack and I'll pinpoint it.",
"A debouncer delays running the search until the user stops typing for a short interval (e.g., 300ms). Benefits:\n\n- **Fewer API calls**: avoids firing a request on every keystroke (\"r\", \"re\", \"rea\", \"reac\", \"react\" → 1 call instead of 5).\n- **Lower backend load & cost**: less traffic, fewer DB/LLM hits.\n- **Better UX**: results don't flicker/reorder mid-typing; avoids race conditions where a stale response overwrites a newer one.\n- **Saves client resources**: fewer re-renders and network round-trips.\n\nUse ~200400ms typically. For instant local filtering, debouncing is less critical; for remote search, it's essentially mandatory.",
"**Merge**: combines branches by creating a new merge commit that ties both histories together. Preserves full history, non-destructive.\n\n**Rebase**: replays your commits on top of another branch, creating new commits. Produces linear history, rewrites commits.\n\nRule of thumb: merge for shared/public branches, rebase for local cleanup before pushing.",
"Queue = point-to-point, one consumer processes each message (work distribution, task processing). Topic = pub/sub, every subscriber gets a copy (event broadcasting, fan-out).\n\nUse a **queue** when: load-balancing work across workers, ensuring exactly one handler, order/retry matters per job.\n\nUse a **topic** when: multiple independent systems react to the same event, decoupling producers from N consumers, event sourcing/notifications.\n\nHybrid (e.g. Kafka consumer groups, SNS→SQS): topic for fan-out + per-subscriber queue for durability and load-balancing within each group."
]
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "caveman",
"description": "Ultra-compressed communication mode. Cuts 65% of output tokens (measured) while keeping full technical accuracy by speaking like a caveman.",
"version": "1.0.1",
"contextFileName": "GEMINI.md"
}
+81
View File
@@ -0,0 +1,81 @@
# caveman — installer shim (Windows / PowerShell).
#
# Thin wrapper around cli/install.js (the unified Node installer). Every flag
# you'd pass to cli/install.js can be passed here; we just forward them.
#
# One-line install:
# irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex
#
# Local clone:
# pwsh install.ps1 [flags]
#
# Why a Node installer? install.sh + install.ps1 used to be parallel sources of
# truth and constantly drifted (issue #249 was a `node -e "..."` quoting bug
# that silently dropped the JSON merge step on every Windows install). One
# Node script works everywhere without quoting bugs.
#
# Why no top-level param() and everything inside a function? `irm | iex`
# executes this file as a string: script-path variables ($PSCommandPath,
# $MyInvocation.MyCommand.Path) are $null and a top-level param block cannot
# receive arguments through a pipe anyway (issue #565). Wrapping the logic in
# a function and forwarding $args keeps one script working for both the pipe
# path (no args, no script path) and the local-clone path.
function Install-Caveman {
param(
[string[]]$InstallerArgs = @()
)
$ErrorActionPreference = "Stop"
$Repo = "JuliusBrussee/caveman"
# Require Node ≥18.
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node) {
Write-Error @"
caveman: Node.js (>=18) required. Install:
- winget install OpenJS.NodeJS.LTS
- or download from https://nodejs.org
"@
exit 1
}
$nodeMajor = [int](& node -p "process.versions.node.split('.')[0]")
if ($nodeMajor -lt 18) {
Write-Error "caveman: Node $nodeMajor too old. Need Node >=18. Upgrade: https://nodejs.org"
exit 1
}
# If we're inside the repo clone, run the local installer directly.
# $PSCommandPath is $null when piped to iex (#565) — the old unguarded
# Split-Path on it was the "Cannot bind argument to parameter 'Path'
# because it is null" crash.
if ($PSCommandPath) {
$here = Split-Path -Parent $PSCommandPath
$local = Join-Path $here "cli/install.js"
if (Test-Path $local) {
& node $local @InstallerArgs
exit $LASTEXITCODE
}
}
# Curl-pipe path: delegate to npx.
$npx = Get-Command npx -ErrorAction SilentlyContinue
if (-not $npx) {
Write-Error "caveman: npx required (ships with Node >=18). Reinstall Node.js."
exit 1
}
# Do NOT pass `--` here — npm 7+ npx already forwards trailing args to the
# package, and a literal `--` was tripping cli/install.js's parseArgs as an
# unknown flag.
# npm >=12 defaults allow-git to "none", failing github: specs with
# EALLOWGIT (#698). Scope the override to this invocation.
$env:NPM_CONFIG_ALLOW_GIT = "all"
& npx -y "github:$Repo" @InstallerArgs
exit $LASTEXITCODE
}
# $args is the automatic variable: populated when run as a file
# (`pwsh install.ps1 --force`), empty under `irm | iex`.
Install-Caveman -InstallerArgs $args
Executable
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# caveman — installer shim.
#
# Thin wrapper around cli/install.js (the unified Node installer). Every flag
# you'd pass to cli/install.js can be passed here; we just forward them.
#
# One-line install:
# curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash
# curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash -s -- --all
#
# Local clone:
# bash install.sh [flags]
#
# Why a Node installer? install.sh + install.ps1 used to be parallel sources
# of truth and constantly drifted (issue #249, etc.). One Node script works
# everywhere without bash/PowerShell quoting bugs.
set -euo pipefail
REPO="JuliusBrussee/caveman"
# Require Node ≥18. nvm is a common path; print a hint if missing.
if ! command -v node >/dev/null 2>&1; then
echo "caveman: Node.js (≥18) required. Install:" >&2
echo " macOS: brew install node" >&2
echo " Linux: see https://nodejs.org or use nvm (https://github.com/nvm-sh/nvm)" >&2
exit 1
fi
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
if [ "$NODE_MAJOR" -lt 18 ]; then
echo "caveman: Node $NODE_MAJOR too old. Need Node ≥18." >&2
echo " Upgrade: https://nodejs.org" >&2
exit 1
fi
# If we're inside the repo clone, run the local installer directly — saves
# the npx round-trip and keeps offline installs working. BASH_SOURCE is unset
# when bash is invoked from stdin (curl | bash), and `set -u` would trip on a
# bare reference — default to empty so the curl-pipe path falls through cleanly.
here="$(cd "$(dirname "${BASH_SOURCE[0]:-}")" 2>/dev/null && pwd)" || here=""
if [ -n "$here" ] && [ -f "$here/cli/install.js" ]; then
exec node "$here/cli/install.js" "$@"
fi
# Curl-pipe path: delegate to npx. We do NOT pass `--` here — npm 7+ npx
# already forwards trailing args to the package, and a literal `--` tripped
# cli/install.js's parseArgs as an unknown flag.
if ! command -v npx >/dev/null 2>&1; then
echo "caveman: npx required (ships with Node ≥18). Reinstall Node.js." >&2
exit 1
fi
# npm >=12 defaults allow-git to "none", failing github: specs with EALLOWGIT
# (#698). Scope the override to this one invocation.
NPM_CONFIG_ALLOW_GIT=all exec npx -y "github:$REPO" "$@"
+35
View File
@@ -0,0 +1,35 @@
{
"name": "caveman-installer",
"version": "0.1.0",
"description": "Caveman installer — detects your AI coding agents and installs caveman for each one.",
"license": "MIT",
"author": "Julius Brussee",
"homepage": "https://github.com/JuliusBrussee/caveman",
"repository": {
"type": "git",
"url": "git+https://github.com/JuliusBrussee/caveman.git"
},
"bugs": {
"url": "https://github.com/JuliusBrussee/caveman/issues"
},
"bin": {
"caveman": "./cli/install.js"
},
"engines": {
"node": ">=18"
},
"scripts": {
"test": "node --test tests/installer/*.test.mjs"
},
"files": [
"cli/",
"src/",
"agents/",
"skills/",
"plugins/",
"commands/",
"dist/caveman.skill",
"README.md",
"LICENSE"
]
}
@@ -26,9 +26,14 @@
"Write"
],
"websiteURL": "https://github.com/JuliusBrussee/caveman",
"privacyPolicyURL": "https://github.com/JuliusBrussee/caveman/blob/main/README.md",
"termsOfServiceURL": "https://github.com/JuliusBrussee/caveman/blob/main/LICENSE",
"defaultPrompt": [
"Use caveman mode. Cut filler. Keep technical accuracy."
],
"composerIcon": "./assets/caveman-small.svg",
"logo": "./assets/caveman.svg",
"screenshots": [],
"brandColor": "#6B7280"
}
}
@@ -0,0 +1,47 @@
---
name: cavecrew-builder
description: >
Surgical 1-2 file edit. Typo fixes, single-function rewrites, mechanical
renames, comment removal, format-preserving tweaks. Hard refuses 3+ file
scope. Returns caveman diff receipt. Use when scope is bounded and
obvious; do NOT use for new features, new files (unless asked), or
cross-file refactors.
tools: [Read, Edit, Write, Grep, Glob]
---
Caveman-ultra. Drop articles/filler. Code/paths exact, backticked. No narration.
## Scope
1 file ideal. 2 OK. 3+ → refuse.
Edit existing only (new file iff user asked).
No new abstractions. No drive-by refactors. No comment additions.
No `Bash` available — cannot shell out, cannot push, cannot delete.
## Workflow
1. `Read` target(s). Never edit blind.
2. `Edit` smallest diff that work.
3. Re-`Read` to verify.
4. Return receipt.
## Output (receipt)
```
<path:line-range> — <change ≤10 words>.
<path:line-range> — <change ≤10 words>.
verified: <re-read OK | mismatch @ path:line>.
```
Diff is the artifact. Receipt is the proof. No exploration story.
## Refusals (terminal lines)
3+ files → `too-big. split: <n one-line tasks>.`
Destructive needed → `needs-confirm. op: <command>.`
Spec ambiguous → `ambiguous. ask: <one question>.`
Tests fail post-edit, can't fix in scope → `regressed. revert path:line. cause: <fragment>.`
## Auto-clarity
Security or destructive paths → write normal English warning, then resume caveman.
@@ -0,0 +1,57 @@
---
name: cavecrew-investigator
description: >
Read-only code locator. Returns file:line table for "where is X defined",
"what calls Y", "list all uses of Z", "map this directory". Output is
caveman-compressed so the main thread eats ~60% fewer tokens than
vanilla Explore. Refuses to suggest fixes.
tools: [Read, Grep, Glob, Bash]
model: haiku
---
Caveman-ultra. Drop articles/filler/hedging. Code/symbols/paths exact, backticked. Lead with answer.
## Job
Locate. Report. Stop. Never edit, never propose fix.
## Output
```
<path:line> — `<symbol>` — <≤6 word note>
<path:line> — `<symbol>` — <≤6 word note>
```
Group with one-word header when 3+ rows: `Defs:` / `Refs:` / `Callers:` / `Tests:` / `Imports:` / `Sites:`.
Single hit → one line, no header.
Zero hits → `No match.`
Last line → totals: `2 defs, 5 refs.` (omit if 0 or 1).
## Tools
`Grep` for symbols/strings. `Glob` for paths. `Read` only specific ranges. `Bash` for `git log -S`/`git grep`/`find` when faster.
## Refusals
Asked to fix → `Read-only. Spawn cavecrew-builder.`
Asked to design → `Read-only. Spawn cavecrew-builder or use main thread.`
## Auto-clarity
Security warnings, destructive ops → write normal English. Resume after.
## Example
Q: "where symlink-safe flag write?"
```
Defs:
- hooks/caveman-config.js:81 — `safeWriteFlag` — atomic write w/ O_NOFOLLOW
- hooks/caveman-config.js:160 — `readFlag` — paired reader
Callers:
- hooks/caveman-mode-tracker.js:33,87
- hooks/caveman-activate.js:40
Tests:
- tests/test_symlink_flag.js — 12 cases
2 defs, 3 callers, 1 test file.
```
@@ -0,0 +1,48 @@
---
name: cavecrew-reviewer
description: >
Diff/branch/file reviewer. One line per finding, severity-tagged, no praise,
no scope creep. Output format `path:line: <emoji> <severity>: <problem>. <fix>.`
Use for "review this PR", "review my diff", "audit this file". Skips
formatting nits unless they change meaning.
tools: [Read, Grep, Bash]
model: haiku
---
Caveman-ultra. Findings only. No "looks good", no "I'd suggest", no preamble.
## Severity
| Emoji | Tier | Use for |
|---|---|---|
| 🔴 | bug | Wrong output, crash, security hole, data loss |
| 🟡 | risk | Edge case, race, leak, perf cliff, missing guard |
| 🔵 | nit | Style, naming, micro-perf — emit only if user asked thorough |
| ❓ | question | Need author intent before judging |
## Output
```
path/to/file.ts:42: 🔴 bug: token expiry uses `<` not `<=`. Off-by-one allows expired tokens 1 tick.
path/to/file.ts:118: 🟡 risk: pool not closed on error path. Add `try/finally`.
src/utils.ts:7: ❓ question: why duplicate `.trim()` here?
totals: 1🔴 1🟡 1❓
```
Zero findings → `No issues.`
File order, ascending line numbers within file.
## Boundaries
- Review only what's in front of you. No "while we're here".
- No big-refactor proposals.
- Need more context → append `(see L<n> in <file>)`. Don't guess.
- Formatting nits skipped unless they change meaning.
## Tools
`Bash` only for `git diff`/`git log -p`/`git show`. No mutating commands.
## Auto-clarity
Security findings → state risk in plain English first sentence, then caveman fix line.
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Caveman">
<circle cx="32" cy="32" r="28" fill="#6B7280"/>
<path d="M18 40c4-10 24-10 28 0" fill="none" stroke="#F9FAFB" stroke-linecap="round" stroke-width="4"/>
<circle cx="24" cy="27" r="4" fill="#F9FAFB"/>
<circle cx="40" cy="27" r="4" fill="#F9FAFB"/>
<path d="M21 18c3-5 8-8 15-8 5 0 10 2 14 7" fill="none" stroke="#D1D5DB" stroke-linecap="round" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 471 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Caveman">
<rect width="128" height="128" rx="24" fill="#6B7280"/>
<path d="M28 79c8-22 48-22 56 0" fill="none" stroke="#F9FAFB" stroke-linecap="round" stroke-width="8"/>
<circle cx="48" cy="52" r="8" fill="#F9FAFB"/>
<circle cx="80" cy="52" r="8" fill="#F9FAFB"/>
<path d="M43 35c6-10 17-15 31-15 12 0 24 5 31 14" fill="none" stroke="#D1D5DB" stroke-linecap="round" stroke-width="8"/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

+82
View File
@@ -0,0 +1,82 @@
---
name: cavecrew
description: >
Decision guide for delegating to caveman-style subagents. Tells the main
thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder`
(1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the
work inline or using vanilla `Explore`. Subagent output is caveman-compressed
so the tool-result injected back into main context is ~60% smaller — main
context lasts longer across long sessions.
Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer",
"save context", "compressed agent output".
---
Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.
## When to use cavecrew vs alternatives
| Task | Use |
|---|---|
| "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` |
| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |
| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |
| New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` |
| Review diff, branch, or file for bugs | `cavecrew-reviewer` |
| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) |
| One-line answer you already know | Main thread, no subagent |
Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**
## Why this exists (the real win)
Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.
## Output contracts
What main thread can rely on per agent:
**`cavecrew-investigator`**
```
<Header>:
- path:line — `symbol` — short note
totals: <counts>.
```
Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`.
**`cavecrew-builder`**
```
<path:line-range> — <change ≤10 words>.
verified: <re-read OK | mismatch @ path:line>.
```
Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).
**`cavecrew-reviewer`**
```
path:line: <emoji> <severity>: <problem>. <fix>.
totals: N🔴 N🟡 N🔵 N❓
```
Or `No issues.` Findings sorted file → line ascending.
## Chaining patterns
**Locate → fix → verify** (most common):
1. `cavecrew-investigator` returns site list.
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.
3. `cavecrew-reviewer` audits the diff.
**Parallel scout** (when investigation is broad):
Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.
**Single-shot edit** (when site is already known):
Skip investigator. Hand exact path:line to `cavecrew-builder` directly.
## What NOT to do
- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.
- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.
- Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that.
- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.
## Auto-clarity (inherited)
Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.
@@ -4,14 +4,14 @@ description: >
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
to save input tokens. Preserves all technical substance, code, URLs, and structure.
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
Trigger: /caveman-compress <filepath> or "compress memory file"
Trigger: /caveman-compress FILEPATH or "compress memory file"
---
# Caveman Compress
## Purpose
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`.
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\caveman-compress\backups\<parent-dir-name>\` on Windows) so skill auto-loaders don't re-ingest it as a live file.
## Trigger
@@ -19,19 +19,19 @@ Compress natural language files (CLAUDE.md, todos, preferences) into caveman-spe
## Process
1. This SKILL.md lives alongside `memory/` in the same directory. Find that directory.
1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.
2. Run:
```
cd <directory_containing_this_SKILL.md> && python3 -m scripts <absolute_filepath>
```
2. From the directory containing this SKILL.md, run:
python3 -m scripts <absolute_filepath>
3. The CLI will:
- detect file type (no tokens)
- call Claude to compress
- validate output (no tokens)
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
- retry up to 2 times
- detect file type (no tokens)
- call Claude to compress
- validate output (no tokens)
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
- retry up to 2 times
- if still failing after 2 retries: report error to user, leave original file untouched
4. Return result to user
@@ -103,9 +103,9 @@ Compressed:
## Boundaries
- ONLY compress natural language files (.md, .txt, extensionless)
- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
- If file has mixed content (prose + code), compress ONLY the prose sections
- If unsure whether something is code or prose, leave it unchanged
- Original file is backed up as FILE.original.md before overwriting
- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file
- Never compress FILE.original.md (skip it)
@@ -0,0 +1,9 @@
"""Caveman compress scripts.
This package provides tools to compress natural language markdown files
into caveman format to save input tokens.
"""
__all__ = ["cli", "compress", "detect", "validate"]
__version__ = "1.0.0"
@@ -11,7 +11,7 @@ except ImportError:
try:
import tiktoken
_enc = tiktoken.get_encoding("cl100k_base")
_enc = tiktoken.get_encoding("o200k_base")
except ImportError:
_enc = None
@@ -23,12 +23,12 @@ def count_tokens(text):
def benchmark_pair(orig_path: Path, comp_path: Path):
orig_text = orig_path.read_text()
comp_text = comp_path.read_text()
orig_text = orig_path.read_text(encoding="utf-8", errors="ignore")
comp_text = comp_path.read_text(encoding="utf-8", errors="ignore")
orig_tokens = count_tokens(orig_text)
comp_tokens = count_tokens(comp_text)
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
result = validate(orig_path, comp_path)
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
@@ -44,8 +44,8 @@ def print_table(rows):
def main():
# Direct file pair: python3 benchmark.py original.md compressed.md
if len(sys.argv) == 3:
orig = Path(sys.argv[1])
comp = Path(sys.argv[2])
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
if not orig.exists():
print(f"❌ Not found: {orig}")
sys.exit(1)
@@ -56,7 +56,9 @@ def main():
return
# Glob mode: repo_root/tests/caveman-compress/
tests_dir = Path(__file__).parent.parent.parent / "tests" / "caveman-compress"
# __file__ lives at <repo_root>/skills/caveman-compress/scripts/benchmark.py
# Walk up four dirs: scripts → caveman-compress → skills → repo_root.
tests_dir = Path(__file__).resolve().parents[3] / "tests" / "caveman-compress"
if not tests_dir.exists():
print(f"❌ Tests dir not found: {tests_dir}")
sys.exit(1)
@@ -1,15 +1,27 @@
#!/usr/bin/env python3
"""
Caveman Memory CLI
Caveman Compress CLI
Usage:
caveman <filepath>
"""
import sys
# Force UTF-8 on stdout/stderr before any code can print. Windows consoles
# default to cp1252 and crash on the ❌ glyphs in error/validation branches,
# masking the real error and leaving the user with a half-compressed file.
for _stream in (sys.stdout, sys.stderr):
reconfigure = getattr(_stream, "reconfigure", None)
if callable(reconfigure):
try:
reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from pathlib import Path
from .compress import compress_file
from .compress import backup_dir_for, compress_file
from .detect import detect_file_type, should_compress
@@ -33,6 +45,8 @@ def main():
print(f"❌ Not a file: {filepath}")
sys.exit(1)
filepath = filepath.resolve()
# Detect file type
file_type = detect_file_type(filepath)
@@ -50,7 +64,7 @@ def main():
if success:
print("\nCompression completed successfully")
backup_path = filepath.with_name(filepath.stem + ".original.md")
backup_path = backup_dir_for(filepath) / (filepath.stem + ".original.md")
print(f"Compressed: {filepath}")
print(f"Original: {backup_path}")
sys.exit(0)
@@ -0,0 +1,414 @@
#!/usr/bin/env python3
"""
Caveman Memory Compression Orchestrator
Usage:
python scripts/compress.py <filepath>
"""
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List
OUTER_FENCE_REGEX = re.compile(
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
)
# YAML frontmatter: starts at file start with --- on its own line, ends with --- on its own line.
# Captures the entire block (including delimiters and trailing newline) and the body after.
FRONTMATTER_REGEX = re.compile(
r"\A(---\r?\n.*?\r?\n---\r?\n)(.*)", re.DOTALL
)
def split_frontmatter(text: str):
"""Split YAML frontmatter from body. Returns (frontmatter, body).
Memory files (and many other markdown docs) start with a YAML frontmatter
block delimited by `---` lines. The compression LLM has a habit of stripping
or rewriting these despite preserve-structure rules in the prompt — so we
surgically remove the frontmatter before compression and prepend it back
verbatim to the output. Files without frontmatter pass through unchanged.
"""
m = FRONTMATTER_REGEX.match(text)
if m:
return m.group(1), m.group(2)
return "", text
# Filenames and paths that almost certainly hold secrets or PII. Compressing
# them ships raw bytes to the Anthropic API — a third-party data boundary that
# developers on sensitive codebases cannot cross. detect.py already skips .env
# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
# slip through the natural-language filter. This is a hard refuse before read.
SENSITIVE_BASENAME_REGEX = re.compile(
r"(?ix)^("
r"\.env(\..+)?"
r"|\.netrc"
r"|credentials(\..+)?"
r"|secrets?(\..+)?"
r"|passwords?(\..+)?"
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
r"|authorized_keys"
r"|known_hosts"
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
r")$"
)
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
SENSITIVE_NAME_TOKENS = (
"secret", "credential", "password", "passwd",
"apikey", "accesskey", "token", "privatekey",
)
def backup_dir_for(filepath: Path) -> Path:
"""Resolve the out-of-tree backup directory for a given source file.
Backups must live OUTSIDE the source directory so skill auto-loaders
(Claude Code rules/, opencode instructions/, etc.) stop re-ingesting the
`.original.md` copies as live files. Base dir is platform-aware:
- Windows: %LOCALAPPDATA%\\caveman-compress\\backups
- else: $XDG_DATA_HOME/caveman-compress/backups if set,
else ~/.local/share/caveman-compress/backups
The source file's parent-dir name is mirrored under the base to reduce
cross-project collisions (e.g. two `task.md` files in different repos).
"""
if os.name == "nt" or sys.platform == "win32":
local_appdata = os.environ.get("LOCALAPPDATA")
base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"
base = base / "caveman-compress" / "backups"
else:
xdg = os.environ.get("XDG_DATA_HOME")
base = Path(xdg) if xdg else Path.home() / ".local" / "share"
base = base / "caveman-compress" / "backups"
return base / filepath.parent.name
def is_sensitive_path(filepath: Path) -> bool:
"""Heuristic denylist for files that must never be shipped to a third-party API."""
name = filepath.name
if SENSITIVE_BASENAME_REGEX.match(name):
return True
lowered_parts = {p.lower() for p in filepath.parts}
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
return True
# Normalize separators so "api-key" and "api_key" both match "apikey".
lower = re.sub(r"[_\-\s.]", "", name.lower())
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
def strip_llm_wrapper(text: str) -> str:
"""Strip outer ```markdown ... ``` fence when it wraps the entire output."""
m = OUTER_FENCE_REGEX.match(text)
if m:
return m.group(2)
return text
def write_text_atomic(path: Path, text: str) -> None:
"""Write ``text`` to ``path`` atomically as UTF-8.
Path.write_text() truncates the destination before encoding the string —
a UnicodeEncodeError (or any other failure) partway through leaves a
0-byte file, destroying whatever was there before (issue #655). Encode
first, write the bytes to a sibling temp file, fsync, then os.replace()
so the destination only ever moves from one complete, valid file to
another. Preserves the original file's permission bits across the swap.
"""
data = text.encode("utf-8")
fd, tmp_name = tempfile.mkstemp(
dir=str(path.parent), prefix=path.name + ".", suffix=".tmp"
)
tmp_path = Path(tmp_name)
try:
with os.fdopen(fd, "wb") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
if path.exists():
os.chmod(tmp_path, stat.S_IMODE(path.stat().st_mode))
os.replace(tmp_path, path)
except Exception:
try:
tmp_path.unlink()
except OSError:
pass
raise
def first_nonblank_line(text: str) -> str:
"""Return the first non-blank line, stripped — used to detect a prose
preamble smuggled in ahead of the real content (issue #588)."""
for line in text.splitlines():
if line.strip():
return line.strip()
return ""
def _write_target(filepath: Path, text: str, backup_path: Path) -> None:
"""Write to the target file, surfacing the backup location if the write
itself fails. write_text_atomic already leaves the target untouched on
failure, but the caller still needs to know where the pre-compression
original lives instead of being left to guess (issue #652)."""
try:
write_text_atomic(filepath, text)
except Exception:
print(f"❌ Write to {filepath} failed. Original preserved at backup: {backup_path}")
raise
from .detect import should_compress
from .validate import validate
MAX_RETRIES = 2
# ---------- Claude Calls ----------
def call_claude(prompt: str) -> str:
"""Send a prompt to Claude.
Prefers the Anthropic SDK when ANTHROPIC_API_KEY is set; otherwise falls
back to the ``claude --print`` CLI (which handles desktop auth).
On Windows the CLI subprocess decoding defaults to the system codepage
(cp1251 / cp1252) and crashes on UTF-8 output — see issue #152. Pinning
``encoding="utf-8"`` with ``errors="replace"`` matches the CLI's actual
native I/O and prevents the UnicodeDecodeError before validation can
report. Windows users with non-ASCII content can also set
``ANTHROPIC_API_KEY`` to route through the SDK and skip the subprocess.
"""
api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key:
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
msg = client.messages.create(
model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
return strip_llm_wrapper(msg.content[0].text.strip())
except ImportError:
pass # anthropic not installed, fall back to CLI
# Fallback: use claude CLI (handles desktop auth).
# Resolve binary via shutil.which so Windows .cmd/.bat shims (e.g.
# %APPDATA%\npm\claude.CMD) work without shell=True. On POSIX,
# shutil.which returns the same absolute path as the implicit lookup,
# so this is a no-op there. Falls back to bare "claude" if not found
# on PATH so subprocess raises a clear FileNotFoundError.
claude_bin = shutil.which("claude") or "claude"
try:
result = subprocess.run(
[claude_bin, "--print"],
input=prompt,
text=True,
capture_output=True,
check=True,
encoding="utf-8",
errors="replace",
)
return strip_llm_wrapper(result.stdout.strip())
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
def build_compress_prompt(original: str) -> str:
return f"""
Compress this markdown into caveman format.
STRICT RULES:
- Do NOT modify anything inside ``` code blocks
- Do NOT modify anything inside inline backticks
- Preserve ALL URLs exactly
- Preserve ALL headings exactly
- Preserve file paths and commands
- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.
Only compress natural language.
TEXT:
{original}
"""
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
errors_str = "\n".join(f"- {e}" for e in errors)
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
CRITICAL RULES:
- DO NOT recompress or rephrase the file
- ONLY fix the listed errors — leave everything else exactly as-is
- The ORIGINAL is provided as reference only (to restore missing content)
- Preserve caveman style in all untouched sections
ERRORS TO FIX:
{errors_str}
HOW TO FIX:
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
- Do not touch any section not mentioned in the errors
ORIGINAL (reference only):
{original}
COMPRESSED (fix this):
{compressed}
Return ONLY the fixed compressed file. No explanation.
"""
# ---------- Core Logic ----------
def compress_file(filepath: Path) -> bool:
# Resolve and validate path
filepath = filepath.resolve()
MAX_FILE_SIZE = 500_000 # 500KB
if not filepath.exists():
raise FileNotFoundError(f"File not found: {filepath}")
if filepath.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"File too large to compress safely (max 500KB): {filepath}")
# Refuse files that look like they contain secrets or PII. Compressing ships
# the raw bytes to the Anthropic API — a third-party boundary — so we fail
# loudly rather than silently exfiltrate credentials or keys. Override is
# intentional: the user must rename the file if the heuristic is wrong.
if is_sensitive_path(filepath):
raise ValueError(
f"Refusing to compress {filepath}: filename looks sensitive "
"(credentials, keys, secrets, or known private paths). "
"Compression sends file contents to the Anthropic API. "
"Rename the file if this is a false positive."
)
print(f"Processing: {filepath}")
if not should_compress(filepath):
print("Skipping (not natural language)")
return False
original_text = filepath.read_text(encoding="utf-8", errors="ignore")
# Store backup outside the source directory so skill auto-loaders don't
# re-ingest the `.original.md` copy as a live file. Mirror the source's
# parent-dir name + stem under a platform-aware base to reduce collisions.
backup_dir = backup_dir_for(filepath)
backup_dir.mkdir(parents=True, exist_ok=True)
backup_path = backup_dir / (filepath.stem + ".original.md")
if not original_text.strip():
print("❌ Refusing to compress: file is empty or whitespace-only.")
return False
# Check if backup already exists to prevent accidental overwriting
if backup_path.exists():
print(f"⚠️ Backup file already exists: {backup_path}")
print("The original backup may contain important content.")
print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.")
return False
# Split YAML frontmatter off before compression. Claude tends to strip or
# rewrite frontmatter despite preserve-structure rules; we keep it verbatim
# by removing it from the input and re-prepending it to the output.
frontmatter, body = split_frontmatter(original_text)
if frontmatter:
print(f"Detected YAML frontmatter ({len(frontmatter)} chars) — preserving verbatim")
if not body.strip():
print("❌ Refusing to compress: body is empty after frontmatter removal.")
return False
# Step 1: Compress (body only, frontmatter excluded)
print("Compressing with Claude...")
compressed_body = call_claude(build_compress_prompt(body))
if compressed_body is None or not compressed_body.strip():
print("❌ Compression aborted: Claude returned an empty response.")
print(" Original file is untouched (no backup created).")
return False
# Compare the BODY (not the whole file) — frontmatter is preserved verbatim
# and would never change, so identity must be judged on the compressible part.
if compressed_body.strip() == body.strip():
print("❌ Compression aborted: output is identical to input.")
print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is")
print(" already in caveman form. Original file is untouched (no backup created).")
return False
# Reassemble: frontmatter (verbatim) + compressed body
compressed = frontmatter + compressed_body
# Save original as backup, then verify the backup readback before
# touching the input file. If the filesystem dropped bytes (encoding,
# antivirus, disk full), unlink the bad backup and abort instead of
# leaving the user with a corrupt backup + compressed primary.
write_text_atomic(backup_path, original_text)
backup_readback = backup_path.read_text(encoding="utf-8", errors="ignore")
if backup_readback != original_text:
print(f"❌ Backup write verification failed: {backup_path}")
print(" In-memory original differs from on-disk backup. Aborting before touching the input file.")
try:
backup_path.unlink()
except OSError:
pass
return False
_write_target(filepath, compressed, backup_path)
# Step 2: Validate + Retry
for attempt in range(MAX_RETRIES):
print(f"\nValidation attempt {attempt + 1}")
result = validate(backup_path, filepath)
if result.is_valid:
print("Validation passed")
break
print("❌ Validation failed:")
for err in result.errors:
print(f" - {err}")
if attempt == MAX_RETRIES - 1:
# Restore original on failure
_write_target(filepath, original_text, backup_path)
backup_path.unlink(missing_ok=True)
print("❌ Failed after retries — original restored")
return False
print("Fixing with Claude...")
compressed = call_claude(
build_fix_prompt(original_text, compressed, result.errors)
)
if compressed is None or not compressed.strip():
print("❌ Fix attempt aborted: Claude returned an empty response.")
print(" Skipping this attempt.")
continue
# Guard against a prose preamble smuggled in ahead of the real fixed
# content (issue #588). Only enforced when the original starts with a
# structural anchor (frontmatter `---` or a heading) — plain-prose
# first lines get legitimately rewritten by compression, and requiring
# them verbatim would reject every valid fix.
anchor = first_nonblank_line(original_text)
if anchor.startswith(("---", "#")) and first_nonblank_line(compressed) != anchor:
print("❌ Fix attempt aborted: output does not start with the original's first line.")
print(" Possible preamble leak. Skipping this attempt.")
continue
_write_target(filepath, compressed, backup_path)
return True
@@ -6,7 +6,7 @@ import re
from pathlib import Path
# Extensions that are natural language and compressible
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst"}
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"}
# Extensions that are code/config and should be skipped
SKIP_EXTENSIONS = {
@@ -17,6 +17,16 @@ SKIP_EXTENSIONS = {
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
}
# Well-known build/config files that carry no (or a misleading) extension —
# `Dockerfile` has no suffix so `.dockerfile` above never matches it, and
# `CMakeLists.txt` would ride the compressible `.txt` rule. Checked by
# basename before any extension rule.
KNOWN_CODE_FILENAMES = {
"dockerfile", "makefile", "gnumakefile", "jenkinsfile", "vagrantfile",
"rakefile", "gemfile", "justfile", "procfile", "brewfile",
"cmakelists.txt",
}
# Patterns that indicate a line is code
CODE_PATTERNS = [
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
@@ -67,6 +77,10 @@ def detect_file_type(filepath: Path) -> str:
"""
ext = filepath.suffix.lower()
# Known code filenames win over any extension rule
if filepath.name.lower() in KNOWN_CODE_FILENAMES:
return "code"
# Extension-based classification
if ext in COMPRESSIBLE_EXTENSIONS:
return "natural_language"
@@ -76,12 +90,16 @@ def detect_file_type(filepath: Path) -> str:
# Extensionless files (like CLAUDE.md, TODO) — check content
if not ext:
try:
text = filepath.read_text(errors="ignore")
text = filepath.read_text(encoding="utf-8", errors="ignore")
except (OSError, PermissionError):
return "unknown"
lines = text.splitlines()[:50]
# Shebang means executable script, never prose
if text.startswith("#!"):
return "code"
if _is_json_content(text[:10000]):
return "config"
if _is_yaml_content(lines):
@@ -115,7 +133,7 @@ if __name__ == "__main__":
sys.exit(1)
for path_str in sys.argv[1:]:
p = Path(path_str)
p = Path(path_str).resolve()
file_type = detect_file_type(p)
compress = should_compress(p)
print(f" {p.name:30s} type={file_type:20s} compress={compress}")
@@ -1,14 +1,16 @@
#!/usr/bin/env python3
import re
from collections import Counter
from pathlib import Path
URL_REGEX = re.compile(r"https?://[^\s)]+")
CODE_BLOCK_REGEX = re.compile(r"```.*?```", re.DOTALL)
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
# crude but effective path detection
PATH_REGEX = re.compile(r"(\./|\../|/|[A-Za-z]:\\)[\w\-/\\\.]+")
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
class ValidationResult:
@@ -26,7 +28,7 @@ class ValidationResult:
def read_file(path: Path) -> str:
return path.read_text(errors="ignore")
return path.read_text(encoding="utf-8")
# ---------- Extractors ----------
@@ -37,7 +39,47 @@ def extract_headings(text):
def extract_code_blocks(text):
return CODE_BLOCK_REGEX.findall(text)
"""Line-based fenced code block extractor.
Handles ``` and ~~~ fences with variable length (CommonMark: closing
fence must use same char and be at least as long as opening). Supports
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
content).
"""
blocks = []
lines = text.split("\n")
i = 0
n = len(lines)
while i < n:
m = FENCE_OPEN_REGEX.match(lines[i])
if not m:
i += 1
continue
fence_char = m.group(2)[0]
fence_len = len(m.group(2))
open_line = lines[i]
block_lines = [open_line]
i += 1
closed = False
while i < n:
close_m = FENCE_OPEN_REGEX.match(lines[i])
if (
close_m
and close_m.group(2)[0] == fence_char
and len(close_m.group(2)) >= fence_len
and close_m.group(3).strip() == ""
):
block_lines.append(lines[i])
closed = True
i += 1
break
block_lines.append(lines[i])
i += 1
if closed:
blocks.append("\n".join(block_lines))
# Unclosed fences are silently skipped — they indicate malformed markdown
# and including them would cause false-positive validation failures.
return blocks
def extract_urls(text):
@@ -52,6 +94,20 @@ def count_bullets(text):
return len(BULLET_REGEX.findall(text))
def extract_inline_codes(text):
"""Backtick-delimited inline spans, with fenced code blocks stripped first.
Previously used a column-0-anchored regex to strip fences, which misses
fences indented 1-3 spaces (valid CommonMark). Reuse extract_code_blocks
(FENCE_OPEN_REGEX-based, indentation-aware) instead so an indented fence's
body backticks don't leak into inline-code pairing.
"""
text_without_fences = text
for block in extract_code_blocks(text):
text_without_fences = text_without_fences.replace(block, "", 1)
return re.findall(r"`([^`]+)`", text_without_fences)
# ---------- Validators ----------
@@ -103,6 +159,22 @@ def validate_bullets(orig, comp, result):
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
def validate_inline_codes(orig, comp, result):
c1 = Counter(extract_inline_codes(orig))
c2 = Counter(extract_inline_codes(comp))
if c1 != c2:
lost = set(c1.keys()) - set(c2.keys())
added = set(c2.keys()) - set(c1.keys())
for code, count in c1.items():
if code in c2 and c2[code] < count:
lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)")
if lost:
result.add_error(f"Inline code lost: {lost}")
if added:
result.add_warning(f"Inline code added: {added}")
# ---------- Main ----------
@@ -117,6 +189,7 @@ def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
validate_urls(orig, comp, result)
validate_paths(orig, comp, result)
validate_bullets(orig, comp, result)
validate_inline_codes(orig, comp, result)
return result
@@ -130,8 +203,8 @@ if __name__ == "__main__":
print("Usage: python validate.py <original> <compressed>")
sys.exit(1)
orig = Path(sys.argv[1])
comp = Path(sys.argv[2])
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
res = validate(orig, comp)
@@ -0,0 +1,12 @@
---
name: caveman-stats
description: >
Show real token usage and estimated savings for the current session.
Reads directly from the Claude Code session log — no AI estimation.
Triggers on /caveman-stats. Output is injected by the mode-tracker hook;
the model itself does not compute the numbers.
---
This skill is delivered by `hooks/caveman-stats.js` (read by `hooks/caveman-mode-tracker.js` on `/caveman-stats`). The model does not need to do anything when this skill fires — the hook returns `decision: "block"` with the formatted stats as the reason. The user sees the numbers immediately.
Output also includes `Est. rule overhead` and `Est. net` lines wherever a savings estimate exists with a known turn count. Rule overhead is the estimated per-turn INPUT-token cost of the injected caveman rules (default 1,250 tokens/turn, override with `CAVEMAN_RULE_OVERHEAD_TOKENS`) times the turn count. Net is savings minus that overhead — when negative, the output says so plainly and suggests turning caveman off for that workload, rather than hiding the net-negative regime behind a gross-savings number (see `docs/HONEST-NUMBERS.md`).
+56 -88
View File
@@ -1,120 +1,88 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra.
Ultra-compressed communication mode. Cuts output tokens 65% (measured) by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
wenyan-lite, wenyan-full, wenyan-ultra.
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
---
# Caveman Mode
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Core Rule
## Persistence
Respond like smart caveman. Cut articles, filler, pleasantries. Keep all technical substance.
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default intensity: **full**. Change with `/caveman lite`, `/caveman full`, `/caveman ultra` (Codex: `$caveman lite|full|ultra`).
Default: **full**. Switch: `/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off`.
## Grammar
## Rules
- Drop articles (a, an, the)
- Drop filler (just, really, basically, actually, simply)
- Drop pleasantries (sure, certainly, of course, happy to)
- Short synonyms (big not extensive, fix not "implement a solution for")
- No hedging (skip "it might be worth considering")
- Fragments fine. No need full sentence
- Technical terms stay exact. "Polymorphism" stays "polymorphism"
- Code blocks unchanged. Caveman speak around code, not in code
- Error messages quoted exact. Caveman only for explanation
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked — quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) — tokenizer split them same as full word: zero token saved, reader still decode. Full word cheaper AND clearer. No causal arrows (→) either — own token, save nothing. Technical terms exact. Code blocks unchanged. Errors quoted exact.
## Pattern
Never drop not/never/no/only/except — flip meaning worse than any token saved. Numbers, units exact.
```
[thing] [action] [reason]. [next step].
```
Tool calls: fire direct. No preamble, plan, or progress note before or between calls. After result: next call direct or final answer — never announce next call. Text before call only to clarify, warn security/irreversible, or resolve ambiguity.
Not:
> Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by...
Preserve user's dominant language exactly — reply in the language user writes, never switch regardless of example text or multilingual context elsewhere. Compress the style, not the language. Every emitted line in that language — openings, pre-tool status lines, all — not just final reply. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim — unless user explicitly ask for translation.
Yes:
> Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:
'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them — grammar, not filler; compress politeness/filler instead.
## Examples
No self-reference. Never name or announce the style. No "caveman mode on", "me caveman think", no third-person caveman tags. Output caveman-only — never normal answer plus "Caveman:" recap. Exception: user explicitly ask what the mode is.
**User:** Why is my React component re-rendering?
Pattern: `[thing] [action] [reason]. [next step].`
**Normal (69 tokens):** "The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I'd recommend using useMemo to memoize the object."
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
**Caveman (19 tokens):** "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
## Intensity
---
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations |
| **ultra** | Strip conjunctions when cause-then-effect stay unambiguous. One word when one word enough. State each fact once. NO prose abbreviations (cfg/impl/req/res/fn/auth), NO arrows (X → Y) — measured zero token saving under tokenizer, cost decode clarity. Code symbols, function names, API names, error strings: never touch |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction — chars, not tokens. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
**User:** How do I set up a PostgreSQL connection pool?
Example — "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop, new ref, re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。"
- wenyan-ultra: "新參照則重繪。useMemo 包之。"
**Caveman:**
```
Use `pg` pool:
```
```js
const pool = new Pool({
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
})
```
```
max = concurrent connections. Keep under DB limit. idleTimeout kill stale conn.
```
Example — "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool reuse open DB connections. No per-request handshake."
- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。"
- wenyan-ultra: "池蓄連,免逐請新開,省握手。"
## Intensity Levels
Classical chars = wenyan modes only. Never swap a word to a classical char to shrink at non-wenyan levels.
### Lite — trim the fat
## Auto-Clarity
Professional tone, just no fluff. Grammar stays intact.
Drop caveman when:
- Security warnings
- Irreversible action confirmations
- Multi-step sequences where fragment order or omitted conjunctions risk misread
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions)
- User asks to clarify or repeats question
- Drop filler and pleasantries (same list as full)
- Drop hedging
- Keep articles, keep full sentences
- Prefer short synonyms where natural
Resume caveman after clear part done.
### Full (default)
Example shows FORMAT only — write warning in session language, not example's.
Classic caveman. Rules from Grammar section above apply.
### Ultra — maximum grunt
Telegraphic. Every word earn its place or die.
- All full rules, plus:
- Abbreviate common terms (DB, auth, config, req, res, fn, impl)
- Strip conjunctions where possible
- One word answer when one word enough
- Arrow notation for causality (X → Y)
## Intensity Examples
**User:** Why is my React component re-rendering?
**Lite:** "Your component re-renders because you create a new object reference each render. Inline object props fail shallow comparison every time. Wrap it in `useMemo`."
**Full:** "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
**Ultra:** "Inline obj prop → new ref → re-render. `useMemo`."
---
**User:** Explain database connection pooling.
**Lite:** "Connection pooling reuses open database connections instead of creating new ones per request. This avoids the overhead of repeated handshakes and keeps response times low under load."
**Full:** "Pool reuse open DB connections. No new connection per request. Skip repeated handshake overhead. Response time stay low under load."
**Ultra:** "Pool = reuse DB conn. Skip handshake overhead → fast under load."
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
> ```sql
> DROP TABLE users;
> ```
> Caveman resume. Verify backup exist first.
## Boundaries
- Code: write normal. Caveman English only
- Git commits: normal
- PR descriptions: normal
- User say "stop caveman" or "normal mode": revert immediately
- Intensity level persist until changed or session end
Persisted outside chat: write normal prose — code, comments, commits, docs, issue/PR/MR text, memory files, third-party messages (/caveman-compress exempt). "stop caveman" or "normal mode": revert. Level persist until changed or session end.
@@ -0,0 +1,6 @@
interface:
display_name: "Caveman"
short_description: "Talk like caveman. Cut filler. Keep technical accuracy."
icon_small: "./assets/caveman-small.svg"
icon_large: "./assets/caveman.svg"
default_prompt: "Use $caveman to answer briefly, cut filler, and preserve exact technical substance."
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Caveman">
<circle cx="32" cy="32" r="28" fill="#6B7280"/>
<path d="M18 40c4-10 24-10 28 0" fill="none" stroke="#F9FAFB" stroke-linecap="round" stroke-width="4"/>
<circle cx="24" cy="27" r="4" fill="#F9FAFB"/>
<circle cx="40" cy="27" r="4" fill="#F9FAFB"/>
<path d="M21 18c3-5 8-8 15-8 5 0 10 2 14 7" fill="none" stroke="#D1D5DB" stroke-linecap="round" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 471 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Caveman">
<rect width="128" height="128" rx="24" fill="#6B7280"/>
<path d="M28 79c8-22 48-22 56 0" fill="none" stroke="#F9FAFB" stroke-linecap="round" stroke-width="8"/>
<circle cx="48" cy="52" r="8" fill="#F9FAFB"/>
<circle cx="80" cy="52" r="8" fill="#F9FAFB"/>
<path d="M43 35c6-10 17-15 31-15 12 0 24 5 31 14" fill="none" stroke="#D1D5DB" stroke-linecap="round" stroke-width="8"/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

+11
View File
@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"cavecrew": {
"source": "JuliusBrussee/caveman",
"sourceType": "github",
"skillPath": "skills/cavecrew/SKILL.md",
"computedHash": "06d45a7308d8603365313decf400020106b985cb5ce500ee169bfba9c71dd147"
}
}
}
+61
View File
@@ -0,0 +1,61 @@
# cavecrew
Decision guide. When to delegate to caveman subagents instead of doing the work inline.
## What it does
Tells the main thread when to spawn a caveman-style subagent versus the vanilla equivalent. The win: subagent tool-results inject back into main context verbatim, and caveman output is roughly 1/3 the size of vanilla prose. Across 20 delegations in one session, that is the difference between context exhaustion and finishing the task.
Three subagents:
| Subagent | Job | Use when |
|----------|-----|----------|
| `cavecrew-investigator` | Locate code (read-only) | "Where is X defined / what calls Y / list uses of Z" |
| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |
| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |
Use vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.
This skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.
## How to invoke
Triggers on phrases like "delegate to subagent", "use cavecrew", "spawn investigator", "save context", "compressed agent output".
## Example chaining
Locate → fix → verify (most common):
1. `cavecrew-investigator` returns site list (`path:line — symbol — note`)
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`
3. `cavecrew-reviewer` audits the resulting diff
Parallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.
## Model overrides
By default, `cavecrew-reviewer` and `cavecrew-investigator` pin `model: haiku` in their frontmatter; `cavecrew-builder` has no `model:` line (uses the API session default). Set env vars in your shell before launching Claude Code to override per-agent:
| Env var | Agent |
|---|---|
| `CAVECREW_REVIEWER_MODEL` | `cavecrew-reviewer` |
| `CAVECREW_BUILDER_MODEL` | `cavecrew-builder` |
| `CAVECREW_INVESTIGATOR_MODEL` | `cavecrew-investigator` |
Example — run reviewer on sonnet, keep others on default:
```sh
export CAVECREW_REVIEWER_MODEL=sonnet
```
Use the same model name strings you'd use in any Claude Code agent frontmatter (e.g. `haiku`, `sonnet`, `opus`).
Overrides patch only the `model:` line in the installed agent's frontmatter; the prompt body is untouched and keeps receiving upstream updates. Plugin installs only — standalone hook installs have no local agent files to patch. Unset or blank = no change. The patch persists in the installed file until the plugin is updated or reinstalled.
## See also
- [`SKILL.md`](./SKILL.md) — full decision matrix and output contracts
- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)
- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)
- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)
- [Caveman README](../../README.md) — repo overview
+82
View File
@@ -0,0 +1,82 @@
---
name: cavecrew
description: >
Decision guide for delegating to caveman-style subagents. Tells the main
thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder`
(1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the
work inline or using vanilla `Explore`. Subagent output is caveman-compressed
so the tool-result injected back into main context is ~60% smaller — main
context lasts longer across long sessions.
Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer",
"save context", "compressed agent output".
---
Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.
## When to use cavecrew vs alternatives
| Task | Use |
|---|---|
| "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` |
| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |
| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |
| New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` |
| Review diff, branch, or file for bugs | `cavecrew-reviewer` |
| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) |
| One-line answer you already know | Main thread, no subagent |
Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**
## Why this exists (the real win)
Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.
## Output contracts
What main thread can rely on per agent:
**`cavecrew-investigator`**
```
<Header>:
- path:line — `symbol` — short note
totals: <counts>.
```
Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`.
**`cavecrew-builder`**
```
<path:line-range> — <change ≤10 words>.
verified: <re-read OK | mismatch @ path:line>.
```
Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).
**`cavecrew-reviewer`**
```
path:line: <emoji> <severity>: <problem>. <fix>.
totals: N🔴 N🟡 N🔵 N❓
```
Or `No issues.` Findings sorted file → line ascending.
## Chaining patterns
**Locate → fix → verify** (most common):
1. `cavecrew-investigator` returns site list.
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.
3. `cavecrew-reviewer` audits the diff.
**Parallel scout** (when investigation is broad):
Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.
**Single-shot edit** (when site is already known):
Skip investigator. Hand exact path:line to `cavecrew-builder` directly.
## What NOT to do
- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.
- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.
- Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that.
- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.
## Auto-clarity (inherited)
Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.
+44
View File
@@ -0,0 +1,44 @@
# caveman-commit
Terse Conventional Commits. Why over what.
## What it does
Generates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context.
Outputs only the message. Does not stage, commit, or amend.
## How to invoke
```
/caveman-commit
```
Also triggers on phrases like "write a commit", "commit message", "generate commit".
## Example output
Diff: new endpoint for user profile.
```
feat(api): add GET /users/:id/profile
Mobile client needs profile data without the full user payload
to reduce LTE bandwidth on cold-launch screens.
Closes #128
```
Diff: breaking API rename.
```
feat(api)!: rename /v1/orders to /v1/checkout
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
before 2026-06-01. Old route returns 410 after that date.
```
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview
+65
View File
@@ -0,0 +1,65 @@
---
name: caveman-commit
description: >
Ultra-compressed commit message generator. Cuts noise from commit messages while preserving
intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why"
isn't obvious. Use when user says "write a commit", "commit message", "generate commit",
"/commit", or invokes /caveman-commit. Auto-triggers when staging changes.
---
Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what.
## Rules
**Subject line:**
- `<type>(<scope>): <imperative summary>``<scope>` optional
- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`
- Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding"
- ≤50 chars when possible, hard cap 72
- No trailing period
- Match project convention for capitalization after the colon
**Body (only if needed):**
- Skip entirely when subject is self-explanatory
- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues
- Wrap at 72 chars
- Bullets `-` not `*`
- Reference issues/PRs at end: `Closes #42`, `Refs #17`
**What NEVER goes in:**
- "This commit does X", "I", "we", "now", "currently" — the diff says what
- "As requested by..." — use Co-authored-by trailer
- "Generated with Claude Code" or any AI attribution — unless the user's own rule requires an `Assisted-by`/AI-attribution trailer, then add it as a trailer
- Emoji (unless project convention requires)
- Restating the file name when scope already says it
## Examples
Diff: new endpoint for user profile with body explaining the why
- ❌ "feat: add a new endpoint to get user profile information from the database"
-
```
feat(api): add GET /users/:id/profile
Mobile client needs profile data without the full user payload
to reduce LTE bandwidth on cold-launch screens.
Closes #128
```
Diff: breaking API change
- ✅
```
feat(api)!: rename /v1/orders to /v1/checkout
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
before 2026-06-01. Old route returns 410 after that date.
```
## Auto-Clarity
Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.
## Boundaries
Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style.
@@ -25,7 +25,7 @@ CLAUDE.md ← compressed (Claude reads this — fewer tokens every sess
CLAUDE.original.md ← human-readable backup (you edit this)
```
Original never lost. You can read and edit `.original.md`. Run skill again to re-compress after edits.
Original never lost. Backup lives in a data dir, not next to your file — `$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/` (macOS/Linux) or `%LOCALAPPDATA%\caveman-compress\backups\<parent-dir-name>\` (Windows) — so skill auto-loaders don't re-read it as a live file. You can read and edit `.original.md` there. Run skill again to re-compress after edits.
## Benchmarks
@@ -35,10 +35,10 @@ Real results on real project files:
|------|----------:|----------:|------:|
| `claude-md-preferences.md` | 706 | 285 | **59.6%** |
| `project-notes.md` | 1145 | 535 | **53.3%** |
| `claude-md-project.md` | 1122 | 687 | **38.8%** |
| `claude-md-project.md` | 1122 | 636 | **43.3%** |
| `todo-list.md` | 627 | 388 | **38.1%** |
| `mixed-with-code.md` | 888 | 574 | **35.4%** |
| **Average** | **898** | **494** | **45%** |
| `mixed-with-code.md` | 888 | 560 | **36.9%** |
| **Average** | **898** | **481** | **46%** |
All validations passed ✅ — headings, code blocks, URLs, file paths preserved exactly.
@@ -55,7 +55,7 @@ All validations passed ✅ — headings, code blocks, URLs, file paths preserved
</td>
<td width="50%">
### 🪨 Caveman (285 tokens)
### <img src="../../docs/assets/dancing-rock.svg" width="20" height="20" alt="rock"/> Caveman (285 tokens)
> "Prefer TypeScript strict mode always. No `any` unless unavoidable — comment why if used. Proper types catch bugs early."
@@ -65,16 +65,18 @@ All validations passed ✅ — headings, code blocks, URLs, file paths preserved
**Same instructions. 60% fewer tokens. Every. Single. Session.**
## Security
`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do.
## Install
```bash
cp -r ~/.claude/skills/caveman-compress <path-to-skill>
```
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.
Or if you have the caveman repo:
If you need local files, the compress skill lives at:
```bash
cp -r skills/caveman-compress ~/.claude/skills/caveman-compress
caveman-compress/
```
**Requires:** Python 3.10+
@@ -96,7 +98,7 @@ Examples:
| Type | Compress? |
|------|-----------|
| `.md`, `.txt`, `.rst` | ✅ Yes |
| `.md`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | ✅ Yes |
| Extensionless natural language | ✅ Yes |
| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |
| `*.original.md` | ❌ Skip (backup files) |
@@ -142,15 +144,15 @@ Caveman compress natural language. It never touch:
`CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs tokens every single time you open a project. Over 100 sessions that's 100,000 tokens of overhead — just for context you already wrote.
Caveman cut that by ~45% on average. Same instructions. Same accuracy. Less waste.
Caveman cut that by ~46% on average. Same instructions. Same accuracy. Less waste.
```
┌──────────────────────────────────────────┐
│ TOKEN SAVINGS PER FILE ████████ 45% │
│ SESSIONS THAT BENEFIT ████████ 100% │
│ INFORMATION PRESERVED ████████ 100% │
│ SETUP TIME █ 1x
└──────────────────────────────────────────┘
┌────────────────────────────────────────────
│ TOKEN SAVINGS PER FILE █████ 46% │
│ SESSIONS THAT BENEFIT ██████████ 100% │
│ INFORMATION PRESERVED ██████████ 100% │
│ SETUP TIME █ 1x │
└────────────────────────────────────────────
```
## Part of Caveman
@@ -158,4 +160,4 @@ Caveman cut that by ~45% on average. Same instructions. Same accuracy. Less wast
This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit — making Claude use fewer tokens without losing accuracy.
- **caveman** — make Claude *speak* like caveman (cuts response tokens ~65%)
- **caveman-compress** — make Claude *read* less (cuts context tokens ~45%)
- **caveman-compress** — make Claude *read* less (cuts context tokens ~46%)
+31
View File
@@ -0,0 +1,31 @@
# Security
## Snyk High Risk Rating
`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.
### What triggers the rating
1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument.
2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved to an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\caveman-compress\backups\<parent-dir-name>\` on Windows). Beyond the target file and that backup location, no files are read or written.
### What the skill does NOT do
- Does not execute user file content as code
- Does not make network requests except to Anthropic's API (via SDK or CLI)
- Does not access files outside the path the user provides
- Does not use shell=True or string interpolation in subprocess calls
- Does not collect or transmit any data beyond the file being compressed
### Auth behavior
If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.
### File size limit
Files larger than 500KB are rejected before any API call is made.
### Reporting a vulnerability
If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.
+111
View File
@@ -0,0 +1,111 @@
---
name: caveman-compress
description: >
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
to save input tokens. Preserves all technical substance, code, URLs, and structure.
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
Trigger: /caveman-compress FILEPATH or "compress memory file"
---
# Caveman Compress
## Purpose
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`, but NOT beside the source file — it lives in an out-of-tree data dir (`$XDG_DATA_HOME/caveman-compress/backups/<parent-dir-name>/`, or `%LOCALAPPDATA%\caveman-compress\backups\<parent-dir-name>\` on Windows) so skill auto-loaders don't re-ingest it as a live file.
## Trigger
`/caveman-compress <filepath>` or when user asks to compress a memory file.
## Process
1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.
2. From the directory containing this SKILL.md, run:
python3 -m scripts <absolute_filepath>
3. The CLI will:
- detect file type (no tokens)
- call Claude to compress
- validate output (no tokens)
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
- retry up to 2 times
- if still failing after 2 retries: report error to user, leave original file untouched
4. Return result to user
## Compression Rules
### Remove
- Articles: a, an, the
- Filler: just, really, basically, actually, simply, essentially, generally
- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend"
- Hedging: "it might be worth", "you could consider", "it would be good to"
- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because"
- Connective fluff: "however", "furthermore", "additionally", "in addition"
### Preserve EXACTLY (never modify)
- Code blocks (fenced ``` and indented)
- Inline code (`backtick content`)
- URLs and links (full URLs, markdown links)
- File paths (`/src/components/...`, `./config.yaml`)
- Commands (`npm install`, `git commit`, `docker build`)
- Technical terms (library names, API names, protocols, algorithms)
- Proper nouns (project names, people, companies)
- Dates, version numbers, numeric values
- Environment variables (`$HOME`, `NODE_ENV`)
### Preserve Structure
- All markdown headings (keep exact heading text, compress body below)
- Bullet point hierarchy (keep nesting level)
- Numbered lists (keep numbering)
- Tables (compress cell text, keep structure)
- Frontmatter/YAML headers in markdown files
### Compress
- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"
- Fragments OK: "Run tests before commit" not "You should always run tests before committing"
- Drop "you should", "make sure to", "remember to" — just state the action
- Merge redundant bullets that say the same thing differently
- Keep one example where multiple examples show the same pattern
CRITICAL RULE:
Anything inside ``` ... ``` must be copied EXACTLY.
Do not:
- remove comments
- remove spacing
- reorder lines
- shorten commands
- simplify anything
Inline code (`...`) must be preserved EXACTLY.
Do not modify anything inside backticks.
If file contains code blocks:
- Treat code blocks as read-only regions
- Only compress text outside them
- Do not merge sections around code
## Pattern
Original:
> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.
Compressed:
> Run tests before push to main. Catch bugs early, prevent broken prod deploys.
Original:
> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.
Compressed:
> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.
## Boundaries
- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
- If file has mixed content (prose + code), compress ONLY the prose sections
- If unsure whether something is code or prose, leave it unchanged
- Original file is backed up as FILE.original.md before overwriting — in the out-of-tree backup data dir (see Purpose), not beside the source file
- Never compress FILE.original.md (skip it)
@@ -0,0 +1,9 @@
"""Caveman compress scripts.
This package provides tools to compress natural language markdown files
into caveman format to save input tokens.
"""
__all__ = ["cli", "compress", "detect", "validate"]
__version__ = "1.0.0"
@@ -0,0 +1,3 @@
from .cli import main
main()
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
# Support both direct execution and module import
try:
from .validate import validate
except ImportError:
sys.path.insert(0, str(Path(__file__).parent))
from validate import validate
try:
import tiktoken
_enc = tiktoken.get_encoding("o200k_base")
except ImportError:
_enc = None
def count_tokens(text):
if _enc is None:
return len(text.split()) # fallback: word count
return len(_enc.encode(text))
def benchmark_pair(orig_path: Path, comp_path: Path):
orig_text = orig_path.read_text(encoding="utf-8", errors="ignore")
comp_text = comp_path.read_text(encoding="utf-8", errors="ignore")
orig_tokens = count_tokens(orig_text)
comp_tokens = count_tokens(comp_text)
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
result = validate(orig_path, comp_path)
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
def print_table(rows):
print("\n| File | Original | Compressed | Saved % | Valid |")
print("|------|----------|------------|---------|-------|")
for r in rows:
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'' if r[4] else ''} |")
def main():
# Direct file pair: python3 benchmark.py original.md compressed.md
if len(sys.argv) == 3:
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
if not orig.exists():
print(f"❌ Not found: {orig}")
sys.exit(1)
if not comp.exists():
print(f"❌ Not found: {comp}")
sys.exit(1)
print_table([benchmark_pair(orig, comp)])
return
# Glob mode: repo_root/tests/caveman-compress/
# __file__ lives at <repo_root>/skills/caveman-compress/scripts/benchmark.py
# Walk up four dirs: scripts → caveman-compress → skills → repo_root.
tests_dir = Path(__file__).resolve().parents[3] / "tests" / "caveman-compress"
if not tests_dir.exists():
print(f"❌ Tests dir not found: {tests_dir}")
sys.exit(1)
rows = []
for orig in sorted(tests_dir.glob("*.original.md")):
comp = orig.with_name(orig.stem.removesuffix(".original") + ".md")
if comp.exists():
rows.append(benchmark_pair(orig, comp))
if not rows:
print("No compressed file pairs found.")
return
print_table(rows)
if __name__ == "__main__":
main()
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Caveman Compress CLI
Usage:
caveman <filepath>
"""
import sys
# Force UTF-8 on stdout/stderr before any code can print. Windows consoles
# default to cp1252 and crash on the ❌ glyphs in error/validation branches,
# masking the real error and leaving the user with a half-compressed file.
for _stream in (sys.stdout, sys.stderr):
reconfigure = getattr(_stream, "reconfigure", None)
if callable(reconfigure):
try:
reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from pathlib import Path
from .compress import backup_dir_for, compress_file
from .detect import detect_file_type, should_compress
def print_usage():
print("Usage: caveman <filepath>")
def main():
if len(sys.argv) != 2:
print_usage()
sys.exit(1)
filepath = Path(sys.argv[1])
# Check file exists
if not filepath.exists():
print(f"❌ File not found: {filepath}")
sys.exit(1)
if not filepath.is_file():
print(f"❌ Not a file: {filepath}")
sys.exit(1)
filepath = filepath.resolve()
# Detect file type
file_type = detect_file_type(filepath)
print(f"Detected: {file_type}")
# Check if compressible
if not should_compress(filepath):
print("Skipping: file is not natural language (code/config)")
sys.exit(0)
print("Starting caveman compression...\n")
try:
success = compress_file(filepath)
if success:
print("\nCompression completed successfully")
backup_path = backup_dir_for(filepath) / (filepath.stem + ".original.md")
print(f"Compressed: {filepath}")
print(f"Original: {backup_path}")
sys.exit(0)
else:
print("\n❌ Compression failed after retries")
sys.exit(2)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"\n❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
+414
View File
@@ -0,0 +1,414 @@
#!/usr/bin/env python3
"""
Caveman Memory Compression Orchestrator
Usage:
python scripts/compress.py <filepath>
"""
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List
OUTER_FENCE_REGEX = re.compile(
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
)
# YAML frontmatter: starts at file start with --- on its own line, ends with --- on its own line.
# Captures the entire block (including delimiters and trailing newline) and the body after.
FRONTMATTER_REGEX = re.compile(
r"\A(---\r?\n.*?\r?\n---\r?\n)(.*)", re.DOTALL
)
def split_frontmatter(text: str):
"""Split YAML frontmatter from body. Returns (frontmatter, body).
Memory files (and many other markdown docs) start with a YAML frontmatter
block delimited by `---` lines. The compression LLM has a habit of stripping
or rewriting these despite preserve-structure rules in the prompt — so we
surgically remove the frontmatter before compression and prepend it back
verbatim to the output. Files without frontmatter pass through unchanged.
"""
m = FRONTMATTER_REGEX.match(text)
if m:
return m.group(1), m.group(2)
return "", text
# Filenames and paths that almost certainly hold secrets or PII. Compressing
# them ships raw bytes to the Anthropic API — a third-party data boundary that
# developers on sensitive codebases cannot cross. detect.py already skips .env
# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
# slip through the natural-language filter. This is a hard refuse before read.
SENSITIVE_BASENAME_REGEX = re.compile(
r"(?ix)^("
r"\.env(\..+)?"
r"|\.netrc"
r"|credentials(\..+)?"
r"|secrets?(\..+)?"
r"|passwords?(\..+)?"
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
r"|authorized_keys"
r"|known_hosts"
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
r")$"
)
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
SENSITIVE_NAME_TOKENS = (
"secret", "credential", "password", "passwd",
"apikey", "accesskey", "token", "privatekey",
)
def backup_dir_for(filepath: Path) -> Path:
"""Resolve the out-of-tree backup directory for a given source file.
Backups must live OUTSIDE the source directory so skill auto-loaders
(Claude Code rules/, opencode instructions/, etc.) stop re-ingesting the
`.original.md` copies as live files. Base dir is platform-aware:
- Windows: %LOCALAPPDATA%\\caveman-compress\\backups
- else: $XDG_DATA_HOME/caveman-compress/backups if set,
else ~/.local/share/caveman-compress/backups
The source file's parent-dir name is mirrored under the base to reduce
cross-project collisions (e.g. two `task.md` files in different repos).
"""
if os.name == "nt" or sys.platform == "win32":
local_appdata = os.environ.get("LOCALAPPDATA")
base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"
base = base / "caveman-compress" / "backups"
else:
xdg = os.environ.get("XDG_DATA_HOME")
base = Path(xdg) if xdg else Path.home() / ".local" / "share"
base = base / "caveman-compress" / "backups"
return base / filepath.parent.name
def is_sensitive_path(filepath: Path) -> bool:
"""Heuristic denylist for files that must never be shipped to a third-party API."""
name = filepath.name
if SENSITIVE_BASENAME_REGEX.match(name):
return True
lowered_parts = {p.lower() for p in filepath.parts}
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
return True
# Normalize separators so "api-key" and "api_key" both match "apikey".
lower = re.sub(r"[_\-\s.]", "", name.lower())
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
def strip_llm_wrapper(text: str) -> str:
"""Strip outer ```markdown ... ``` fence when it wraps the entire output."""
m = OUTER_FENCE_REGEX.match(text)
if m:
return m.group(2)
return text
def write_text_atomic(path: Path, text: str) -> None:
"""Write ``text`` to ``path`` atomically as UTF-8.
Path.write_text() truncates the destination before encoding the string —
a UnicodeEncodeError (or any other failure) partway through leaves a
0-byte file, destroying whatever was there before (issue #655). Encode
first, write the bytes to a sibling temp file, fsync, then os.replace()
so the destination only ever moves from one complete, valid file to
another. Preserves the original file's permission bits across the swap.
"""
data = text.encode("utf-8")
fd, tmp_name = tempfile.mkstemp(
dir=str(path.parent), prefix=path.name + ".", suffix=".tmp"
)
tmp_path = Path(tmp_name)
try:
with os.fdopen(fd, "wb") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
if path.exists():
os.chmod(tmp_path, stat.S_IMODE(path.stat().st_mode))
os.replace(tmp_path, path)
except Exception:
try:
tmp_path.unlink()
except OSError:
pass
raise
def first_nonblank_line(text: str) -> str:
"""Return the first non-blank line, stripped — used to detect a prose
preamble smuggled in ahead of the real content (issue #588)."""
for line in text.splitlines():
if line.strip():
return line.strip()
return ""
def _write_target(filepath: Path, text: str, backup_path: Path) -> None:
"""Write to the target file, surfacing the backup location if the write
itself fails. write_text_atomic already leaves the target untouched on
failure, but the caller still needs to know where the pre-compression
original lives instead of being left to guess (issue #652)."""
try:
write_text_atomic(filepath, text)
except Exception:
print(f"❌ Write to {filepath} failed. Original preserved at backup: {backup_path}")
raise
from .detect import should_compress
from .validate import validate
MAX_RETRIES = 2
# ---------- Claude Calls ----------
def call_claude(prompt: str) -> str:
"""Send a prompt to Claude.
Prefers the Anthropic SDK when ANTHROPIC_API_KEY is set; otherwise falls
back to the ``claude --print`` CLI (which handles desktop auth).
On Windows the CLI subprocess decoding defaults to the system codepage
(cp1251 / cp1252) and crashes on UTF-8 output — see issue #152. Pinning
``encoding="utf-8"`` with ``errors="replace"`` matches the CLI's actual
native I/O and prevents the UnicodeDecodeError before validation can
report. Windows users with non-ASCII content can also set
``ANTHROPIC_API_KEY`` to route through the SDK and skip the subprocess.
"""
api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key:
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
msg = client.messages.create(
model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
return strip_llm_wrapper(msg.content[0].text.strip())
except ImportError:
pass # anthropic not installed, fall back to CLI
# Fallback: use claude CLI (handles desktop auth).
# Resolve binary via shutil.which so Windows .cmd/.bat shims (e.g.
# %APPDATA%\npm\claude.CMD) work without shell=True. On POSIX,
# shutil.which returns the same absolute path as the implicit lookup,
# so this is a no-op there. Falls back to bare "claude" if not found
# on PATH so subprocess raises a clear FileNotFoundError.
claude_bin = shutil.which("claude") or "claude"
try:
result = subprocess.run(
[claude_bin, "--print"],
input=prompt,
text=True,
capture_output=True,
check=True,
encoding="utf-8",
errors="replace",
)
return strip_llm_wrapper(result.stdout.strip())
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
def build_compress_prompt(original: str) -> str:
return f"""
Compress this markdown into caveman format.
STRICT RULES:
- Do NOT modify anything inside ``` code blocks
- Do NOT modify anything inside inline backticks
- Preserve ALL URLs exactly
- Preserve ALL headings exactly
- Preserve file paths and commands
- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.
Only compress natural language.
TEXT:
{original}
"""
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
errors_str = "\n".join(f"- {e}" for e in errors)
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
CRITICAL RULES:
- DO NOT recompress or rephrase the file
- ONLY fix the listed errors — leave everything else exactly as-is
- The ORIGINAL is provided as reference only (to restore missing content)
- Preserve caveman style in all untouched sections
ERRORS TO FIX:
{errors_str}
HOW TO FIX:
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
- Do not touch any section not mentioned in the errors
ORIGINAL (reference only):
{original}
COMPRESSED (fix this):
{compressed}
Return ONLY the fixed compressed file. No explanation.
"""
# ---------- Core Logic ----------
def compress_file(filepath: Path) -> bool:
# Resolve and validate path
filepath = filepath.resolve()
MAX_FILE_SIZE = 500_000 # 500KB
if not filepath.exists():
raise FileNotFoundError(f"File not found: {filepath}")
if filepath.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"File too large to compress safely (max 500KB): {filepath}")
# Refuse files that look like they contain secrets or PII. Compressing ships
# the raw bytes to the Anthropic API — a third-party boundary — so we fail
# loudly rather than silently exfiltrate credentials or keys. Override is
# intentional: the user must rename the file if the heuristic is wrong.
if is_sensitive_path(filepath):
raise ValueError(
f"Refusing to compress {filepath}: filename looks sensitive "
"(credentials, keys, secrets, or known private paths). "
"Compression sends file contents to the Anthropic API. "
"Rename the file if this is a false positive."
)
print(f"Processing: {filepath}")
if not should_compress(filepath):
print("Skipping (not natural language)")
return False
original_text = filepath.read_text(encoding="utf-8", errors="ignore")
# Store backup outside the source directory so skill auto-loaders don't
# re-ingest the `.original.md` copy as a live file. Mirror the source's
# parent-dir name + stem under a platform-aware base to reduce collisions.
backup_dir = backup_dir_for(filepath)
backup_dir.mkdir(parents=True, exist_ok=True)
backup_path = backup_dir / (filepath.stem + ".original.md")
if not original_text.strip():
print("❌ Refusing to compress: file is empty or whitespace-only.")
return False
# Check if backup already exists to prevent accidental overwriting
if backup_path.exists():
print(f"⚠️ Backup file already exists: {backup_path}")
print("The original backup may contain important content.")
print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.")
return False
# Split YAML frontmatter off before compression. Claude tends to strip or
# rewrite frontmatter despite preserve-structure rules; we keep it verbatim
# by removing it from the input and re-prepending it to the output.
frontmatter, body = split_frontmatter(original_text)
if frontmatter:
print(f"Detected YAML frontmatter ({len(frontmatter)} chars) — preserving verbatim")
if not body.strip():
print("❌ Refusing to compress: body is empty after frontmatter removal.")
return False
# Step 1: Compress (body only, frontmatter excluded)
print("Compressing with Claude...")
compressed_body = call_claude(build_compress_prompt(body))
if compressed_body is None or not compressed_body.strip():
print("❌ Compression aborted: Claude returned an empty response.")
print(" Original file is untouched (no backup created).")
return False
# Compare the BODY (not the whole file) — frontmatter is preserved verbatim
# and would never change, so identity must be judged on the compressible part.
if compressed_body.strip() == body.strip():
print("❌ Compression aborted: output is identical to input.")
print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is")
print(" already in caveman form. Original file is untouched (no backup created).")
return False
# Reassemble: frontmatter (verbatim) + compressed body
compressed = frontmatter + compressed_body
# Save original as backup, then verify the backup readback before
# touching the input file. If the filesystem dropped bytes (encoding,
# antivirus, disk full), unlink the bad backup and abort instead of
# leaving the user with a corrupt backup + compressed primary.
write_text_atomic(backup_path, original_text)
backup_readback = backup_path.read_text(encoding="utf-8", errors="ignore")
if backup_readback != original_text:
print(f"❌ Backup write verification failed: {backup_path}")
print(" In-memory original differs from on-disk backup. Aborting before touching the input file.")
try:
backup_path.unlink()
except OSError:
pass
return False
_write_target(filepath, compressed, backup_path)
# Step 2: Validate + Retry
for attempt in range(MAX_RETRIES):
print(f"\nValidation attempt {attempt + 1}")
result = validate(backup_path, filepath)
if result.is_valid:
print("Validation passed")
break
print("❌ Validation failed:")
for err in result.errors:
print(f" - {err}")
if attempt == MAX_RETRIES - 1:
# Restore original on failure
_write_target(filepath, original_text, backup_path)
backup_path.unlink(missing_ok=True)
print("❌ Failed after retries — original restored")
return False
print("Fixing with Claude...")
compressed = call_claude(
build_fix_prompt(original_text, compressed, result.errors)
)
if compressed is None or not compressed.strip():
print("❌ Fix attempt aborted: Claude returned an empty response.")
print(" Skipping this attempt.")
continue
# Guard against a prose preamble smuggled in ahead of the real fixed
# content (issue #588). Only enforced when the original starts with a
# structural anchor (frontmatter `---` or a heading) — plain-prose
# first lines get legitimately rewritten by compression, and requiring
# them verbatim would reject every valid fix.
anchor = first_nonblank_line(original_text)
if anchor.startswith(("---", "#")) and first_nonblank_line(compressed) != anchor:
print("❌ Fix attempt aborted: output does not start with the original's first line.")
print(" Possible preamble leak. Skipping this attempt.")
continue
_write_target(filepath, compressed, backup_path)
return True
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Detect whether a file is natural language (compressible) or code/config (skip)."""
import json
import re
from pathlib import Path
# Extensions that are natural language and compressible
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"}
# Extensions that are code/config and should be skipped
SKIP_EXTENSIONS = {
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml",
".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml",
".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c",
".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua",
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
}
# Well-known build/config files that carry no (or a misleading) extension —
# `Dockerfile` has no suffix so `.dockerfile` above never matches it, and
# `CMakeLists.txt` would ride the compressible `.txt` rule. Checked by
# basename before any extension rule.
KNOWN_CODE_FILENAMES = {
"dockerfile", "makefile", "gnumakefile", "jenkinsfile", "vagrantfile",
"rakefile", "gemfile", "justfile", "procfile", "brewfile",
"cmakelists.txt",
}
# Patterns that indicate a line is code
CODE_PATTERNS = [
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
re.compile(r"^\s*(def |class |function |async function |export )"),
re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"),
re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets
re.compile(r"^\s*@\w+"), # decorators/annotations
re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value
re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal
]
def _is_code_line(line: str) -> bool:
"""Check if a line looks like code."""
return any(p.match(line) for p in CODE_PATTERNS)
def _is_json_content(text: str) -> bool:
"""Check if content is valid JSON."""
try:
json.loads(text)
return True
except (json.JSONDecodeError, ValueError):
return False
def _is_yaml_content(lines: list[str]) -> bool:
"""Heuristic: check if content looks like YAML."""
yaml_indicators = 0
for line in lines[:30]:
stripped = line.strip()
if stripped.startswith("---"):
yaml_indicators += 1
elif re.match(r"^\w[\w\s]*:\s", stripped):
yaml_indicators += 1
elif stripped.startswith("- ") and ":" in stripped:
yaml_indicators += 1
# If most non-empty lines look like YAML
non_empty = sum(1 for l in lines[:30] if l.strip())
return non_empty > 0 and yaml_indicators / non_empty > 0.6
def detect_file_type(filepath: Path) -> str:
"""Classify a file as 'natural_language', 'code', 'config', or 'unknown'.
Returns:
One of: 'natural_language', 'code', 'config', 'unknown'
"""
ext = filepath.suffix.lower()
# Known code filenames win over any extension rule
if filepath.name.lower() in KNOWN_CODE_FILENAMES:
return "code"
# Extension-based classification
if ext in COMPRESSIBLE_EXTENSIONS:
return "natural_language"
if ext in SKIP_EXTENSIONS:
return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config"
# Extensionless files (like CLAUDE.md, TODO) — check content
if not ext:
try:
text = filepath.read_text(encoding="utf-8", errors="ignore")
except (OSError, PermissionError):
return "unknown"
lines = text.splitlines()[:50]
# Shebang means executable script, never prose
if text.startswith("#!"):
return "code"
if _is_json_content(text[:10000]):
return "config"
if _is_yaml_content(lines):
return "config"
code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l))
non_empty = sum(1 for l in lines if l.strip())
if non_empty > 0 and code_lines / non_empty > 0.4:
return "code"
return "natural_language"
return "unknown"
def should_compress(filepath: Path) -> bool:
"""Return True if the file is natural language and should be compressed."""
if not filepath.is_file():
return False
# Skip backup files
if filepath.name.endswith(".original.md"):
return False
return detect_file_type(filepath) == "natural_language"
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python detect.py <file1> [file2] ...")
sys.exit(1)
for path_str in sys.argv[1:]:
p = Path(path_str).resolve()
file_type = detect_file_type(p)
compress = should_compress(p)
print(f" {p.name:30s} type={file_type:20s} compress={compress}")
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
import re
from collections import Counter
from pathlib import Path
URL_REGEX = re.compile(r"https?://[^\s)]+")
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
# crude but effective path detection
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
class ValidationResult:
def __init__(self):
self.is_valid = True
self.errors = []
self.warnings = []
def add_error(self, msg):
self.is_valid = False
self.errors.append(msg)
def add_warning(self, msg):
self.warnings.append(msg)
def read_file(path: Path) -> str:
return path.read_text(encoding="utf-8")
# ---------- Extractors ----------
def extract_headings(text):
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
def extract_code_blocks(text):
"""Line-based fenced code block extractor.
Handles ``` and ~~~ fences with variable length (CommonMark: closing
fence must use same char and be at least as long as opening). Supports
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
content).
"""
blocks = []
lines = text.split("\n")
i = 0
n = len(lines)
while i < n:
m = FENCE_OPEN_REGEX.match(lines[i])
if not m:
i += 1
continue
fence_char = m.group(2)[0]
fence_len = len(m.group(2))
open_line = lines[i]
block_lines = [open_line]
i += 1
closed = False
while i < n:
close_m = FENCE_OPEN_REGEX.match(lines[i])
if (
close_m
and close_m.group(2)[0] == fence_char
and len(close_m.group(2)) >= fence_len
and close_m.group(3).strip() == ""
):
block_lines.append(lines[i])
closed = True
i += 1
break
block_lines.append(lines[i])
i += 1
if closed:
blocks.append("\n".join(block_lines))
# Unclosed fences are silently skipped — they indicate malformed markdown
# and including them would cause false-positive validation failures.
return blocks
def extract_urls(text):
return set(URL_REGEX.findall(text))
def extract_paths(text):
return set(PATH_REGEX.findall(text))
def count_bullets(text):
return len(BULLET_REGEX.findall(text))
def extract_inline_codes(text):
"""Backtick-delimited inline spans, with fenced code blocks stripped first.
Previously used a column-0-anchored regex to strip fences, which misses
fences indented 1-3 spaces (valid CommonMark). Reuse extract_code_blocks
(FENCE_OPEN_REGEX-based, indentation-aware) instead so an indented fence's
body backticks don't leak into inline-code pairing.
"""
text_without_fences = text
for block in extract_code_blocks(text):
text_without_fences = text_without_fences.replace(block, "", 1)
return re.findall(r"`([^`]+)`", text_without_fences)
# ---------- Validators ----------
def validate_headings(orig, comp, result):
h1 = extract_headings(orig)
h2 = extract_headings(comp)
if len(h1) != len(h2):
result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
if h1 != h2:
result.add_warning("Heading text/order changed")
def validate_code_blocks(orig, comp, result):
c1 = extract_code_blocks(orig)
c2 = extract_code_blocks(comp)
if c1 != c2:
result.add_error("Code blocks not preserved exactly")
def validate_urls(orig, comp, result):
u1 = extract_urls(orig)
u2 = extract_urls(comp)
if u1 != u2:
result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}")
def validate_paths(orig, comp, result):
p1 = extract_paths(orig)
p2 = extract_paths(comp)
if p1 != p2:
result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}")
def validate_bullets(orig, comp, result):
b1 = count_bullets(orig)
b2 = count_bullets(comp)
if b1 == 0:
return
diff = abs(b1 - b2) / b1
if diff > 0.15:
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
def validate_inline_codes(orig, comp, result):
c1 = Counter(extract_inline_codes(orig))
c2 = Counter(extract_inline_codes(comp))
if c1 != c2:
lost = set(c1.keys()) - set(c2.keys())
added = set(c2.keys()) - set(c1.keys())
for code, count in c1.items():
if code in c2 and c2[code] < count:
lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)")
if lost:
result.add_error(f"Inline code lost: {lost}")
if added:
result.add_warning(f"Inline code added: {added}")
# ---------- Main ----------
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
result = ValidationResult()
orig = read_file(original_path)
comp = read_file(compressed_path)
validate_headings(orig, comp, result)
validate_code_blocks(orig, comp, result)
validate_urls(orig, comp, result)
validate_paths(orig, comp, result)
validate_bullets(orig, comp, result)
validate_inline_codes(orig, comp, result)
return result
# ---------- CLI ----------
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("Usage: python validate.py <original> <compressed>")
sys.exit(1)
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
res = validate(orig, comp)
print(f"\nValid: {res.is_valid}")
if res.errors:
print("\nErrors:")
for e in res.errors:
print(f" - {e}")
if res.warnings:
print("\nWarnings:")
for w in res.warnings:
print(f" - {w}")
+38
View File
@@ -0,0 +1,38 @@
# caveman-help
Quick-reference card. One shot, no mode change.
## What it does
Prints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.
## How to invoke
```
/caveman-help
```
Also triggers on "caveman help", "what caveman commands", "how do I use caveman".
## Example output
```
Modes:
/caveman full (default)
/caveman lite lighter
/caveman ultra extreme
/caveman wenyan classical Chinese
Skills:
/caveman-commit terse Conventional Commits
/caveman-review one-line PR comments
/caveman-stats session token savings
Deactivate:
"stop caveman" or "normal mode"
```
## See also
- [`SKILL.md`](./SKILL.md) — full reference card
- [Caveman README](../../README.md) — repo overview
+63
View File
@@ -0,0 +1,63 @@
---
name: caveman-help
description: >
Quick-reference card for all caveman modes, skills, and commands.
One-shot display, not a persistent mode. Trigger: /caveman-help,
"caveman help", "what caveman commands", "how do I use caveman".
---
# Caveman Help
Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.
## Modes
| Mode | Trigger | What change |
|------|---------|-------------|
| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. |
| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. |
| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. |
| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. |
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. |
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. |
Mode stick until changed or session end.
## Skills
| Skill | Trigger | What it do |
|-------|---------|-----------|
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
| **caveman-help** | `/caveman-help` | This card. |
## Deactivate
Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
## Language
Keep user's language by default. User write Portuguese → reply Portuguese caveman. Compress the style, not the language. Technical terms, code, commands, commit types, and exact error strings stay verbatim unless user ask for translation.
## Configure Default Mode
Default mode = `full`. Change it:
**Environment variable** (highest priority):
```bash
export CAVEMAN_DEFAULT_MODE=ultra
```
**Config file** (`~/.config/caveman/config.json` macOS/Linux, `%APPDATA%\caveman\config.json` Windows):
```json
{ "defaultMode": "lite" }
```
Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`.
Resolution: env var > config file > `full`.
## More
Full docs: https://github.com/JuliusBrussee/caveman
+33
View File
@@ -0,0 +1,33 @@
# caveman-review
One-line PR comments. Location, problem, fix. No throat-clearing.
## What it does
Generates code review comments in `L<line>: <severity> <problem>. <fix>.` format. One line per finding. Severity emoji: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes.
Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the *why*. Resumes terse for the rest.
Output only — does not approve, request changes, or run linters.
## How to invoke
```
/caveman-review
```
Also triggers on "review this PR", "code review", "review the diff".
## Example output
```
L42: 🔴 bug: user can be null after .find(). Add guard before .email.
L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.
L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).
L107: ❓ q: why drop the cache here? Reads on next request will miss.
```
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview
+55
View File
@@ -0,0 +1,55 @@
---
name: caveman-review
description: >
Ultra-compressed code review comments. Cuts noise from PR feedback while preserving
the actionable signal. Each comment is one line: location, problem, fix. Use when user
says "review this PR", "code review", "review the diff", "/review", or invokes
/caveman-review. Auto-triggers when reviewing pull requests.
---
Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing.
## Rules
**Format:** `L<line>: <problem>. <fix>.` — or `<file>:L<line>: ...` when reviewing multi-file diffs.
**Severity prefix (optional, when mixed):**
- `🔴 bug:` — broken behavior, will cause incident
- `🟡 risk:` — works but fragile (race, missing null check, swallowed error)
- `🔵 nit:` — style, naming, micro-optim. Author can ignore
- `❓ q:` — genuine question, not a suggestion
**Drop:**
- "I noticed that...", "It seems like...", "You might want to consider..."
- "This is just a suggestion but..." — use `nit:` instead
- "Great work!", "Looks good overall but..." — say it once at the top, not per comment
- Restating what the line does — the reviewer can read the diff
- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:`
**Keep:**
- Exact line numbers
- Exact symbol/function/variable names in backticks
- Concrete fix, not "consider refactoring this"
- The *why* if the fix isn't obvious from the problem statement
## Examples
❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here."
`L42: 🔴 bug: user can be null after .find(). Add guard before .email.`
❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability."
`L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.`
❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case."
`L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).`
## Auto-Clarity
Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest.
## Boundaries
Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style.
+36
View File
@@ -0,0 +1,36 @@
# caveman-stats
Real session token receipts. No AI estimation.
## What it does
Reads the current Claude Code session log directly and reports actual input/output token usage plus estimated savings versus a non-caveman baseline. Numbers come from the JSONL session log on disk — the model itself does not compute or estimate them. Output is injected by the `caveman-mode-tracker` hook, which intercepts `/caveman-stats` and returns the formatted stats as a blocked-decision reason.
Output also includes an `Est. rule overhead` and `Est. net` line whenever the savings figure above them is unambiguous (a single benchmarked mode with a known turn count — no guessing across mixed or unattributed spans). Overhead estimates the per-turn INPUT-token cost of the rules the skill injects every turn — default 1,250 tokens/turn, override with `CAVEMAN_RULE_OVERHEAD_TOKENS` if you've measured your own setup. Net is savings minus that overhead. On short, terse replies this can go negative — caveman's OUTPUT savings don't clear its INPUT cost — and the line says so directly instead of hiding it behind a gross-savings number. Background: `docs/HONEST-NUMBERS.md`.
Each run also writes a lifetime-savings suffix file used by the statusline badge (`⛏ 12.4k`). That badge stays a gross-savings figure on purpose — it is a glanceable summary, not a full accounting; run `/caveman-stats` for the net picture.
## How to invoke
```
/caveman-stats
```
## Example output
```
Session: 47 turns
Input: 12,304 tokens
Output: 3,891 tokens (caveman)
Baseline: 11,247 tokens (estimated without caveman)
Saved: 7,356 tokens (~65%)
Est. rule overhead: 58,750 (input, ~1,250/turn over 47 turns)
Est. net: -51,394 (caveman cost more than it saved for this workload — consider turning it off)
```
(Numbers above are illustrative — see `docs/HONEST-NUMBERS.md` for why short, terse-reply sessions tend to land net-negative even at a healthy output-savings percentage.)
## See also
- [`SKILL.md`](./SKILL.md) — hook contract and mechanics
- [Caveman README](../../README.md) — repo overview

Some files were not shown because too many files have changed in this diff Show More