Compare commits

...
185 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
154 changed files with 15647 additions and 602 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"
}
]
}
]
}
}
-63
View File
@@ -1,63 +0,0 @@
---
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,
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.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Rules
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"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
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:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
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 .Wrap之。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
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 DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
## Auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user confused. Resume caveman after clear part done.
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/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
+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 -9
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,19 +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
cp skills/caveman/SKILL.md .cursor/skills/caveman/SKILL.md
- name: Sync caveman-compress skill to plugin
run: |
# 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: |
cd skills && zip -r ../caveman.skill caveman/
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 .cursor/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
+8 -1
View File
@@ -3,8 +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`, `.cursor/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>
+257 -183
View File
@@ -1,231 +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="#before--after">Before/After</a>
<a href="#install">Install</a> •
<a href="#intensity-levels">Levels</a> •
<a href="#caveman-skills">Skills</a> •
<a href="#benchmarks">Benchmarks</a>
<a href="#evals">Evals</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. Now with [文言文 mode](#文言文-wenyan-mode), [terse commits](#caveman-commit), [one-line code reviews](#caveman-review), and a [compression tool](#caveman-compress) that cuts **~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.**
**Pick your level of grunt:**
<table>
<tr>
<td width="25%">
#### 🪶 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`."
</td>
<td width="25%">
#### 🪨 Full
> "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
</td>
<td width="25%">
#### 🔥 Ultra
> "Inline obj prop → new ref → re-render. `useMemo`."
</td>
<td width="25%">
#### 📜 文言文
> "物出新參照,致重繪。useMemo Wrap之。"
</td>
</tr>
</table>
**Same answer. You pick how many word.**
Same fix. Third of the words. Nothing technical lost.
```
┌─────────────────────────────────────┐
TOKENS SAVED ████████ 75% │
TECHNICAL ACCURACY ████████ 100%│
SPEED INCREASE ████████ ~3x
VIBES ████████ OOG │
└─────────────────────────────────────┘
┌────────────────────────────────────────────
output tokens saved █████████ 65% │
input tokens saved ░░░░░░░░░ 0%
technical accuracy █████████ 100%
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
Caveman no make brain smaller. Caveman make *mouth* smaller. Shrinks what the agent **says**, not what it knows.
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.
## Install
**One command. Finds every agent on your machine. Installs for each.**
```bash
npx skills add JuliusBrussee/caveman
# macOS · Linux · WSL · Git Bash
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash
```
`npx skills` supports 40+ agents — Claude Code, GitHub Copilot, Cursor, Windsurf, Cline, and more. To install for a specific agent:
```powershell
# Windows · PowerShell 5.1+
irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex
```
~30 seconds. Needs Node ≥18. Skips agents you no have. Safe to re-run.
Prefer one agent at a time? Each has its own path:
```bash
# Claude Code plugin
claude plugin marketplace add JuliusBrussee/caveman && claude plugin install caveman@caveman
# Gemini CLI extension
gemini extensions install https://github.com/JuliusBrussee/caveman --consent
# Cursor / Windsurf / Cline / Codex / 30+ more, via the skills registry
npx skills add JuliusBrussee/caveman -a cursor
npx skills add JuliusBrussee/caveman -a github-copilot
npx skills add JuliusBrussee/caveman -a cline
npx skills add JuliusBrussee/caveman -a windsurf
npx skills add JuliusBrussee/caveman -a codex
```
Or with Claude Code plugin system:
The full per-agent matrix, all flags, dry-run, and uninstall live in **[INSTALL.md](./INSTALL.md)**.
```bash
claude plugin marketplace add JuliusBrussee/caveman
claude plugin install caveman@caveman
```
> [!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.
Codex:
**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.
1. Clone repo
2. Open Codex in repo
3. Run `/plugins`
4. Search `Caveman`
5. Install plugin
## 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]
> **Windows Codex users:** Clone repo → VS Code → Codex Settings → Plugins → find `Caveman` under local marketplace → Install → Reload Window. Also enable `git config core.symlinks true` before cloning (requires developer mode or admin).
> **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.
Install once. Use in all sessions after that. One rock. That it.
## What you get
## Usage
| 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. |
Trigger with:
- `/caveman` or Codex `$caveman`
- "talk like caveman"
- "caveman mode"
- "less tokens please"
Stop with: "stop caveman" or "normal mode"
### Intensity Levels
| Level | Trigger | What it do |
|-------|---------|------------|
| **Lite** | `/caveman lite` | Drop filler, keep grammar. Professional but no fluff |
| **Full** | `/caveman full` | Default caveman. Drop articles, fragments, full grunt |
| **Ultra** | `/caveman ultra` | Maximum compression. Telegraphic. Abbreviate everything |
### 文言文 (Wenyan) Mode
Classical Chinese literary compression — same technical accuracy, but in the most token-efficient written language humans ever invented.
| Level | Trigger | What it do |
|-------|---------|------------|
| **Wenyan-Lite** | `/caveman wenyan-lite` | Semi-classical. Grammar intact, filler gone |
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness |
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget |
Level stick until you change it or session end.
## Caveman Skills
| Skill | What it do | Trigger |
|-------|-----------|---------|
| **caveman-commit** | Terse commit messages. Conventional Commits. ≤50 char subject. Why over what. | `/caveman-commit` |
| **caveman-review** | One-line PR comments: `L42: 🔴 bug: user null. Add guard.` No throat-clearing. | `/caveman-review` |
### caveman-compress
Caveman make Claude *speak* with fewer tokens. **Compress** make Claude *read* fewer tokens.
Your `CLAUDE.md` loads on **every session start**. Caveman Compress rewrites memory 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)
```
| 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%** |
| `todo-list.md` | 627 | 388 | **38.1%** |
| `mixed-with-code.md` | 888 | 574 | **35.4%** |
| **Average** | **898** | **494** | **45%** |
Code blocks, URLs, file paths, commands, headings, dates, version numbers — anything technical passes through untouched. Only prose gets compressed. See the full [caveman-compress README](caveman-compress/README.md) for details. [Security note](./caveman-compress/SECURITY.md): Snyk flags this as High Risk due to subprocess/file patterns — it's a false positive.
> [!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% |
@@ -237,42 +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)**.
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.
### Independently measured: JetBrains, 86 tasks
## Evals
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.
Caveman not just claim 75%. Caveman **prove** it.
| Workload | Output tokens saved | Measured by |
|---|---:|---|
| Chat-style prose | **65%** | us, table above |
| Agentic coding run | **8.5%** | JetBrains, 86 tasks |
The `evals/` directory has a three-arm eval harness that measures real token compression against a proper control — not just "verbose vs skill" but "terse vs skill". Because comparing caveman to verbose Claude conflate the skill with generic terseness. That cheating. Caveman not cheat.
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.
Pick the number that matches your workload:
- **Agent writes you prose** — explanations, review, docs, debugging walkthroughs → 65% territory.
- **Agent works a repo unattended** → single digits. Not zero, not 65%.
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.
Two things follow:
- **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.
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.
<details>
<summary><strong>caveman-compress receipts</strong> — real memory files, cutting input tokens forever</summary>
<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 | 636 | **43.3%** |
| `todo-list.md` | 627 | 388 | **38.1%** |
| `mixed-with-code.md` | 888 | 560 | **36.9%** |
| **Average** | **898** | **481** | **46%** |
Every session after, that file loads ~46% smaller. Input tokens saved forever, not just one reply.
</details>
## The whole cave
<table>
<tr><td>
### <img src="docs/assets/dancing-rock.svg" width="20" height="20" alt=""> Want the whole agent, not just its mouth? → caveman-code
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.
```bash
# Run the eval (needs claude CLI)
uv run python evals/llm_run.py
# Read results (no API key, runs offline)
uv run --with tiktoken python evals/measure.py
npm install -g @juliusbrussee/caveman-code
```
Snapshots committed to git. CI runs free. Every number change reviewable as diff. Add a skill, add a prompt — harness pick it up automatically.
[**▶ Try caveman-code →**](https://github.com/JuliusBrussee/caveman-code)
## Star This Repo
</td></tr>
</table>
If caveman save you mass token, mass money — leave mass star. ⭐
Five tools, one idea: **agent do more with less.**
[![Star History Chart](https://api.star-history.com/svg?repos=JuliusBrussee/caveman&type=Date)](https://star-history.com/#JuliusBrussee/caveman&Date)
| 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) |
## Also by Julius Brussee
<details>
<summary><strong>Also: five sibling skills, one install</strong></summary>
- **[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)
<br>
## License
[**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
-162
View File
@@ -1,162 +0,0 @@
#!/usr/bin/env python3
"""
Caveman Memory Compression Orchestrator
Usage:
python scripts/compress.py <filepath>
"""
import os
import subprocess
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:
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=8096,
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text.strip()
except ImportError:
pass # anthropic not installed, fall back to CLI
# Fallback: use claude CLI (handles desktop auth)
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:
# 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}")
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")
# 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
# 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"\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
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
BIN
View File
Binary file not shown.
-63
View File
@@ -1,63 +0,0 @@
---
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,
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.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Rules
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"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
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:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
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 .Wrap之。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
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 DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
## Auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user confused. Resume caveman after clear part done.
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/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
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]."
-1
View File
@@ -1 +0,0 @@
../caveman-compress/SKILL.md
-1
View File
@@ -1 +0,0 @@
../caveman-compress/scripts
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".
```
+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"
]
}
@@ -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.
+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.
@@ -1,29 +1,29 @@
---
name: compress
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"
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
`/caveman:compress <filepath>` or when user asks to compress a memory file.
`/caveman-compress <filepath>` or when user asks to compress a memory file.
## Process
1. This SKILL.md lives alongside `scripts/` 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:
2. From the directory containing this SKILL.md, run:
cd <directory_containing_this_SKILL.md> && python3 -m scripts <absolute_filepath>
python3 -m scripts <absolute_filepath>
3. The CLI will:
- detect file type (no tokens)
@@ -31,6 +31,7 @@ cd <directory_containing_this_SKILL.md> && python3 -m scripts <absolute_filepath
- 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
@@ -102,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)
@@ -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)
@@ -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)
@@ -7,9 +7,21 @@ Usage:
"""
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
@@ -52,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):
@@ -1,9 +1,10 @@
#!/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)
@@ -27,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 ----------
@@ -38,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):
@@ -53,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 ----------
@@ -104,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 ----------
@@ -118,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
@@ -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`).
+39 -14
View File
@@ -1,7 +1,7 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
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",
@@ -10,11 +10,25 @@ description: >
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Persistence
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default: **full**. Switch: `/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off`.
## Rules
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"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
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.
Never drop not/never/no/only/except — flip meaning worse than any token saved. Numbers, units exact.
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.
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.
'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them — grammar, not filler; compress politeness/filler instead.
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.
Pattern: `[thing] [action] [reason]. [next step].`
@@ -26,30 +40,41 @@ Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
| **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. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **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 |
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`."
- ultra: "Inline obj prop, new ref, re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "物出新參照,重繪useMemo .Wrap之。"
- wenyan-ultra: "新參照重繪。useMemo Wrap。"
- wenyan-full: "每繪新生對象參照,重繪;以 useMemo 包之則免。"
- wenyan-ultra: "新參照重繪。useMemo 包之。"
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 DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
- ultra: "Pool reuse open DB connections. No per-request handshake."
- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。"
- wenyan-ultra: "池蓄連,免逐請新開,省握手。"
Classical chars = wenyan modes only. Never swap a word to a classical char to shrink at non-wenyan levels.
## Auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user confused. Resume caveman after clear part done.
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
Resume caveman after clear part done.
Example shows FORMAT only — write warning in session language, not example's.
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
@@ -60,4 +85,4 @@ Example — destructive op:
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. 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.
-1
View File
@@ -1 +0,0 @@
../../../../caveman-compress/SKILL.md
-1
View File
@@ -1 +0,0 @@
../../../../caveman-compress/scripts
+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
+1 -1
View File
@@ -29,7 +29,7 @@ Write commit messages terse and exact. Conventional Commits format. No fluff. Wh
**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
- "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
@@ -17,7 +17,7 @@ Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman m
## What It Do
```
/caveman:compress CLAUDE.md
/caveman-compress CLAUDE.md
```
```
@@ -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."
@@ -71,7 +71,7 @@ All validations passed ✅ — headings, code blocks, URLs, file paths preserved
## Install
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman:compress`.
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.
If you need local files, the compress skill lives at:
@@ -84,21 +84,21 @@ caveman-compress/
## Usage
```
/caveman:compress <filepath>
/caveman-compress <filepath>
```
Examples:
```
/caveman:compress CLAUDE.md
/caveman:compress docs/preferences.md
/caveman:compress todos.md
/caveman-compress CLAUDE.md
/caveman-compress docs/preferences.md
/caveman-compress todos.md
```
### What files work
| 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) |
@@ -106,7 +106,7 @@ Examples:
## How It Work
```
/caveman:compress CLAUDE.md
/caveman-compress CLAUDE.md
detect file type (no tokens)
@@ -144,11 +144,11 @@ 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% │
│ TOKEN SAVINGS PER FILE █████ 46% │
│ SESSIONS THAT BENEFIT ██████████ 100% │
│ INFORMATION PRESERVED ██████████ 100% │
│ SETUP TIME █ 1x │
@@ -160,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%)
@@ -8,7 +8,7 @@
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 alongside it. No files outside the user-specified path are read or written.
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
+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
+1 -1
View File
@@ -52,4 +52,4 @@ Drop terse mode for: security findings (CVE-class bugs need full explanation + r
## 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.
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
+12
View File
@@ -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`).
+48
View File
@@ -0,0 +1,48 @@
# caveman
Talk like smart caveman. Same brain, fewer tokens.
## What it does
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts 65% of output tokens (measured) with full accuracy preserved. Mode persists for the whole session until changed or stopped.
Six intensity levels:
| Level | What change |
|-------|-------------|
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
| `full` | Default. Drop articles, fragments OK, short synonyms. |
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
| `wenyan-lite` | Classical Chinese register, light compression. |
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
| `wenyan-ultra` | Extreme classical compression. |
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
## How to invoke
```
/caveman # full mode (default)
/caveman lite # lighter compression
/caveman ultra # extreme compression
/caveman wenyan # classical Chinese
stop caveman # back to normal prose
```
## Example output
Question: "Why does my React component re-render?"
Normal prose:
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
Caveman (full):
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
Caveman (ultra):
> Inline obj prop → new ref → re-render. `useMemo`.
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview, install, benchmarks
+39 -14
View File
@@ -1,7 +1,7 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
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",
@@ -10,11 +10,25 @@ description: >
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Persistence
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default: **full**. Switch: `/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off`.
## Rules
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"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
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.
Never drop not/never/no/only/except — flip meaning worse than any token saved. Numbers, units exact.
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.
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.
'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them — grammar, not filler; compress politeness/filler instead.
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.
Pattern: `[thing] [action] [reason]. [next step].`
@@ -26,30 +40,41 @@ Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
| **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. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **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 |
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`."
- ultra: "Inline obj prop, new ref, re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "物出新參照,重繪useMemo .Wrap之。"
- wenyan-ultra: "新參照重繪。useMemo Wrap。"
- wenyan-full: "每繪新生對象參照,重繪;以 useMemo 包之則免。"
- wenyan-ultra: "新參照重繪。useMemo 包之。"
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 DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
- ultra: "Pool reuse open DB connections. No per-request handshake."
- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。"
- wenyan-ultra: "池蓄連,免逐請新開,省握手。"
Classical chars = wenyan modes only. Never swap a word to a classical char to shrink at non-wenyan levels.
## Auto-Clarity
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user confused. Resume caveman after clear part done.
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
Resume caveman after clear part done.
Example shows FORMAT only — write warning in session language, not example's.
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
@@ -60,4 +85,4 @@ Example — destructive op:
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. 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.
-1
View File
@@ -1 +0,0 @@
../../caveman-compress/SKILL.md
-1
View File
@@ -1 +0,0 @@
../../caveman-compress/scripts
+111
View File
@@ -0,0 +1,111 @@
# Caveman Hooks
These hooks are **bundled with the caveman plugin** and activate automatically when the plugin is installed. No manual setup required.
If you installed caveman standalone (without the plugin), the unified Node installer at `cli/install.js` wires them into your `settings.json` for you — run `node cli/install.js --only claude` from a clone, or `npx -y github:JuliusBrussee/caveman -- --only claude` for the curl-pipe path.
## What's Included
### `caveman-activate.js` — SessionStart hook
- Runs once when Claude Code starts
- Writes `full` to `$CLAUDE_CONFIG_DIR/.caveman-active` (default `~/.claude/.caveman-active`) via the symlink-safe `safeWriteFlag` helper
- Emits caveman rules as hidden SessionStart context
- Detects missing statusline config and emits setup nudge (Claude will offer to help)
### `caveman-mode-tracker.js` — UserPromptSubmit hook
- Fires on every user prompt, checks for `/caveman` commands and natural-language activation/deactivation phrases ("talk like caveman", "stop caveman", "normal mode")
- Writes the active mode to the flag file when a caveman command is detected; deletes it on deactivation
- Emits a small per-turn reinforcement reminder when the flag is set to a non-independent mode (`lite`/`full`/`ultra`/`wenyan*`)
- Supports: `lite`, `full`, `ultra`, `wenyan`, `wenyan-lite`, `wenyan-full`, `wenyan-ultra`, `commit`, `review`, `compress`
### `caveman-statusline.sh` / `caveman-statusline.ps1` — Statusline badge script
- Reads `$CLAUDE_CONFIG_DIR/.caveman-active` (default `~/.claude/.caveman-active`) and outputs a colored badge
- Shows `[CAVEMAN]`, `[CAVEMAN:ULTRA]`, `[CAVEMAN:WENYAN]`, etc.
- Appends the lifetime savings suffix `⛏ 12.4k` from `$CLAUDE_CONFIG_DIR/.caveman-statusline-suffix` (written by `caveman-stats.js` on each `/caveman-stats` run; absent until the first run, so fresh installs render no fake number). Opt out with `CAVEMAN_STATUSLINE_SAVINGS=0`.
## Statusline Badge
The statusline badge shows which caveman mode is active directly in your Claude Code status bar.
**Plugin users:** If you do not already have a `statusLine` configured, Claude will detect that on your first session after install and offer to set it up for you. Accept and you're done.
If you already have a custom statusline, caveman does not overwrite it and Claude stays quiet. Add the badge snippet to your existing script instead.
**Standalone users:** the unified installer (`cli/install.js`, invoked by the `install.sh` / `install.ps1` shims at the repo root) wires the statusline automatically if you do not already have a custom statusline. If you do, the installer leaves it alone and prints the merge note.
**Manual setup:** If you need to configure it yourself, add one of these to `~/.claude/settings.json`:
```json
{
"statusLine": {
"type": "command",
"command": "bash /path/to/caveman-statusline.sh"
}
}
```
```json
{
"statusLine": {
"type": "command",
"command": "powershell -ExecutionPolicy Bypass -File C:\\path\\to\\caveman-statusline.ps1"
}
}
```
Replace the path with the actual script location (e.g. `~/.claude/hooks/` for standalone installs, or the plugin install directory for plugin installs).
**Custom statusline:** If you already have a statusline script, add this snippet to it:
```bash
caveman_text=""
caveman_flag="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.caveman-active"
if [ -f "$caveman_flag" ]; then
caveman_mode=$(cat "$caveman_flag" 2>/dev/null)
if [ "$caveman_mode" = "full" ] || [ -z "$caveman_mode" ]; then
caveman_text=$'\033[38;5;172m[CAVEMAN]\033[0m'
else
caveman_suffix=$(echo "$caveman_mode" | tr '[:lower:]' '[:upper:]')
caveman_text=$'\033[38;5;172m[CAVEMAN:'"${caveman_suffix}"$']\033[0m'
fi
fi
```
Badge examples:
- `/caveman``[CAVEMAN]`
- `/caveman ultra``[CAVEMAN:ULTRA]`
- `/caveman wenyan``[CAVEMAN:WENYAN]`
- `/caveman-commit``[CAVEMAN:COMMIT]`
- `/caveman-review``[CAVEMAN:REVIEW]`
## How It Works
```
SessionStart hook ──writes "full"──▶ $CLAUDE_CONFIG_DIR/.caveman-active ◀──writes mode── UserPromptSubmit hook
reads
Statusline script
[CAVEMAN:ULTRA] │ ...
```
SessionStart stdout is injected as hidden system context — Claude sees it, users don't. The statusline runs as a separate process. The flag file is the bridge.
## Uninstall
If installed via plugin: disable the plugin — hooks deactivate automatically.
If installed via the standalone Node installer:
```bash
npx -y github:JuliusBrussee/caveman -- --uninstall
# or, from a clone:
node cli/install.js --uninstall
```
Or manually:
1. Remove the caveman hook files from `$CLAUDE_CONFIG_DIR/hooks/` (default `~/.claude/hooks/`): `caveman-activate.js`, `caveman-mode-tracker.js`, `caveman-stats.js`, `caveman-config.js`, and `caveman-statusline.{sh,ps1}`.
2. Remove the SessionStart, UserPromptSubmit, and statusLine entries from `$CLAUDE_CONFIG_DIR/settings.json`.
3. Delete `$CLAUDE_CONFIG_DIR/.caveman-active` (and `$CLAUDE_CONFIG_DIR/.caveman-statusline-suffix` if you ran `/caveman-stats`).
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env node
// cavecrew model overrides — patch installed agent frontmatter from env vars.
//
// Called by caveman-activate.js early in SessionStart so users can pin
// per-agent models without shadow-copying entire agent files.
//
// Env vars:
// CAVECREW_REVIEWER_MODEL → agents/cavecrew-reviewer.md
// CAVECREW_BUILDER_MODEL → agents/cavecrew-builder.md
// CAVECREW_INVESTIGATOR_MODEL → agents/cavecrew-investigator.md
//
// Rules:
// - Unset / blank → no-op.
// - Values containing newlines or control characters → ignored.
// - Existing `model:` line in frontmatter → replaced in-place.
// - No `model:` line → inserted after `tools:` (or before closing `---`).
// - File missing / outside plugin layout → silent no-op.
// - All filesystem errors → silent fail.
const fs = require('fs');
const path = require('path');
const AGENT_ENV_MAP = [
{ envVar: 'CAVECREW_REVIEWER_MODEL', file: path.join('agents', 'cavecrew-reviewer.md') },
{ envVar: 'CAVECREW_BUILDER_MODEL', file: path.join('agents', 'cavecrew-builder.md') },
{ envVar: 'CAVECREW_INVESTIGATOR_MODEL', file: path.join('agents', 'cavecrew-investigator.md') },
];
// Return the plugin root directory given the hooks directory path.
// Layouts (#645): plugin/repo checkout puts this file at <root>/src/hooks/
// (agents/ lives two levels up); standalone installs at <config>/hooks/
// (one level up). Prefer CLAUDE_PLUGIN_ROOT, then the first candidate that
// actually contains an agents/ directory.
function resolvePluginRoot(hookDir) {
const candidates = [];
if (process.env.CLAUDE_PLUGIN_ROOT) candidates.push(process.env.CLAUDE_PLUGIN_ROOT);
candidates.push(path.resolve(hookDir, '..', '..'), path.resolve(hookDir, '..'));
for (const root of candidates) {
try {
if (fs.statSync(path.join(root, 'agents')).isDirectory()) return root;
} catch (e) {}
}
return path.resolve(hookDir, '..');
}
// Patch the YAML frontmatter of `content` to set `model: <modelValue>`.
// Returns the patched string, or the original if no frontmatter or already identical.
// Rejects `modelValue` strings that contain newlines or control characters.
function patchFrontmatterModel(content, modelValue) {
// Reject blank or unsafe model strings
if (!modelValue || /[\x00-\x1f\x7f]/.test(modelValue)) return content;
// Must begin with YAML frontmatter delimiter
if (!content.startsWith('---')) return content;
// Find the closing ---
const closeIdx = content.indexOf('\n---', 3);
if (closeIdx === -1) return content;
const fmRaw = content.slice(0, closeIdx); // opening --- through last fm line
const after = content.slice(closeIdx); // \n--- onward (body)
// Preserve original line ending so we don't create mixed CRLF/LF on Windows
const nl = fmRaw.includes('\r\n') ? '\r\n' : '\n';
const modelLine = 'model: ' + modelValue;
const modelRe = /^model:[ \t]*.*$/m;
if (modelRe.test(fmRaw)) {
// Replace existing model: line
const patched = fmRaw.replace(modelRe, modelLine);
if (patched === fmRaw) return content; // already identical
return patched + after;
}
// Insert after tools: line when present; else before closing ---
const toolsMatch = fmRaw.match(/^tools:[ \t]*.*$/m);
if (toolsMatch) {
const toolsEnd = fmRaw.indexOf(toolsMatch[0]) + toolsMatch[0].length;
return fmRaw.slice(0, toolsEnd) + nl + modelLine + fmRaw.slice(toolsEnd) + after;
}
// Append before closing delimiter
return fmRaw + nl + modelLine + after;
}
// Apply all env-var overrides to agent files under `pluginRoot`.
// `env` defaults to process.env; pass an object in tests.
function applyOverrides(pluginRoot, env) {
const envArg = env || process.env;
for (const { envVar, file } of AGENT_ENV_MAP) {
const raw = envArg[envVar];
if (!raw || !raw.trim()) continue;
const modelValue = raw.trim();
if (/[\x00-\x1f\x7f]/.test(modelValue)) continue;
const agentPath = path.join(pluginRoot, file);
let content;
try {
content = fs.readFileSync(agentPath, 'utf8');
} catch (e) {
continue; // missing file or wrong layout → silent no-op
}
const patched = patchFrontmatterModel(content, modelValue);
if (patched === content) continue;
try {
fs.writeFileSync(agentPath, patched, 'utf8');
} catch (e) {
// Silent fail — never block session start
}
}
}
module.exports = { resolvePluginRoot, patchFrontmatterModel, applyOverrides, AGENT_ENV_MAP };
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env node
// caveman — Claude Code SessionStart activation hook
//
// Runs on every session start:
// 1. Writes flag file at $CLAUDE_CONFIG_DIR/.caveman-active (statusline reads this)
// 2. Emits caveman ruleset as hidden SessionStart context
// 3. Detects missing statusline config and emits setup nudge
const fs = require('fs');
const path = require('path');
const os = require('os');
const { getDefaultMode, safeWriteFlag, recordModeChange, readFlag, VALID_MODES } = require('./caveman-config');
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
const flagPath = path.join(claudeDir, '.caveman-active');
const settingsPath = path.join(claudeDir, 'settings.json');
// Apply per-agent model overrides from env vars before emitting rules.
// Best-effort: any error is swallowed so SessionStart is never blocked.
try {
const { applyOverrides, resolvePluginRoot } = require('./cavecrew-model-overrides');
applyOverrides(resolvePluginRoot(__dirname));
} catch (e) {}
// SessionStart re-fires mid-conversation (resume, /clear, context compaction),
// not just at true session start. Re-firing must not clobber a mode the user
// switched to mid-session (#691): branch on the hook payload's `source` field —
// only a real `startup` resets to the configured default; resume/clear/compact
// preserve a valid existing flag.
// Sync stdin read assumes the parent (Claude Code) writes the payload and
// closes the pipe — it always does. A parent that held the pipe open forever
// would block here; no such caller exists, and a TTY (manual run) skips it.
let source = 'startup';
try {
if (!process.stdin.isTTY) {
const raw = fs.readFileSync(0, 'utf8');
if (raw) {
const data = JSON.parse(raw);
if (data && typeof data.source === 'string') source = data.source;
}
}
} catch (e) { /* no/bad stdin → treat as startup */ }
let mode = getDefaultMode();
if (source !== 'startup') {
const existing = readFlag(flagPath);
if (existing && VALID_MODES.includes(existing)) mode = existing;
}
// "off" mode — skip activation entirely, don't write flag or emit rules
if (mode === 'off') {
recordModeChange(claudeDir, null); // #601: timestamped transition log
try { fs.unlinkSync(flagPath); } catch (e) {}
process.stdout.write('OK');
process.exit(0);
}
// 1. Write flag file (symlink-safe)
recordModeChange(claudeDir, mode); // #601
safeWriteFlag(flagPath, mode);
// 2. Emit full caveman ruleset, filtered to the active intensity level.
// The old 2-sentence summary was too weak — models drifted back to verbose
// mid-conversation, especially after context compression pruned it away.
// Full rules with examples anchor behavior much more reliably.
//
// Reads SKILL.md at runtime so edits to the source of truth propagate
// automatically — no hardcoded duplication to go stale.
// Modes that have their own independent skill files — not caveman intensity levels.
// For these, emit a short activation line; the skill itself handles behavior.
const INDEPENDENT_MODES = new Set(['commit', 'review', 'compress']);
if (INDEPENDENT_MODES.has(mode)) {
process.stdout.write('CAVEMAN MODE ACTIVE — level: ' + mode + '. Behavior defined by /caveman-' + mode + ' skill.');
process.exit(0);
}
// Resolve the canonical label for wenyan alias
const modeLabel = mode === 'wenyan' ? 'wenyan-full' : mode;
// Read SKILL.md — the single source of truth for caveman behavior.
// Candidate locations, tried in order (#587/#589 — the old single '..' path
// resolved to <plugin_root>/src/skills/, which doesn't exist, so plugin
// installs silently used the stale fallback ruleset):
// 1. $CLAUDE_PLUGIN_ROOT/skills/caveman/SKILL.md — Claude Code sets
// CLAUDE_PLUGIN_ROOT when invoking plugin hooks; authoritative when present.
// 2. ../../skills/caveman/SKILL.md — hook at <plugin_root>/src/hooks/
// (plugin.json layout) or a repo checkout.
// 3. ../skills/caveman/SKILL.md — standalone install with hooks at
// $CLAUDE_CONFIG_DIR/hooks/ and the skill at $CLAUDE_CONFIG_DIR/skills/caveman/.
// All misses fall through to the hardcoded fallback ruleset below.
const skillCandidates = [];
if (process.env.CLAUDE_PLUGIN_ROOT) {
skillCandidates.push(path.join(process.env.CLAUDE_PLUGIN_ROOT, 'skills', 'caveman', 'SKILL.md'));
}
skillCandidates.push(
path.join(__dirname, '..', '..', 'skills', 'caveman', 'SKILL.md'),
path.join(__dirname, '..', 'skills', 'caveman', 'SKILL.md')
);
let skillContent = '';
for (const candidate of skillCandidates) {
try {
skillContent = fs.readFileSync(candidate, 'utf8');
break;
} catch (e) { /* try next candidate */ }
}
let output;
if (skillContent) {
// Strip YAML frontmatter
const body = skillContent.replace(/^---[\s\S]*?---\s*/, '');
// Filter intensity table: keep header rows + only the active level's row
const filtered = body.split('\n').reduce((acc, line) => {
// Intensity table rows start with | **level** |
const tableRowMatch = line.match(/^\|\s*\*\*(\S+?)\*\*\s*\|/);
if (tableRowMatch) {
// Keep only the active level's row (and always keep header/separator)
if (tableRowMatch[1] === modeLabel) {
acc.push(line);
}
return acc;
}
// Example lines start with "- level:" — keep only lines matching active level
const exampleMatch = line.match(/^- (\S+?):\s/);
if (exampleMatch) {
if (exampleMatch[1] === modeLabel) {
acc.push(line);
}
return acc;
}
acc.push(line);
return acc;
}, []);
output = 'CAVEMAN MODE ACTIVE — level: ' + modeLabel + '\n\n' + filtered.join('\n');
} else {
// Fallback when SKILL.md is not found (standalone hook install without skills dir).
// This is the minimum viable ruleset — better than nothing.
output =
'CAVEMAN MODE ACTIVE — level: ' + modeLabel + '\n\n' +
'Respond terse like smart caveman. All technical substance stay. Only fluff die.\n\n' +
'## Persistence\n\n' +
'ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".\n\n' +
'Current level: **' + modeLabel + '**. Switch: `/caveman lite|full|ultra`.\n\n' +
'## Rules\n\n' +
'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"). Technical terms exact. Code blocks unchanged. Errors quoted exact.\n\n' +
"Preserve user's dominant language. User write Portuguese → reply Portuguese caveman. Compress the style, not the language. Technical terms, code, API names, commands, error strings stay verbatim.\n\n" +
'No self-reference. Never name or announce the style. No "caveman mode on" tags. Output caveman-only.\n\n' +
'Pattern: `[thing] [action] [reason]. [next step].`\n\n' +
'Not: "Sure! I\'d be happy to help you with that. The issue you\'re experiencing is likely caused by..."\n' +
'Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"\n\n' +
'## Auto-Clarity\n\n' +
'Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.\n\n' +
'## Boundaries\n\n' +
'Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.';
}
// 3. Detect missing statusline config — nudge Claude to help set it up.
// One-shot (#661): the nudge costs ~90 tokens per session, so a marker file
// gates it to the first session only. Users who declined stop paying for it.
const nudgeMarkerPath = path.join(claudeDir, '.caveman-nudge-shown');
try {
let hasStatusline = false;
if (fs.existsSync(settingsPath)) {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
if (settings.statusLine) {
hasStatusline = true;
}
}
if (!hasStatusline && !fs.existsSync(nudgeMarkerPath)) {
safeWriteFlag(nudgeMarkerPath, '1');
const isWindows = process.platform === 'win32';
const scriptName = isWindows ? 'caveman-statusline.ps1' : 'caveman-statusline.sh';
const scriptPath = path.join(__dirname, scriptName);
const command = isWindows
? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"`
: `bash "${scriptPath}"`;
const statusLineSnippet =
'"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }';
output += "\n\n" +
"STATUSLINE SETUP NEEDED: The caveman plugin includes a statusline badge showing active mode " +
"(e.g. [CAVEMAN], [CAVEMAN:ULTRA]). It is not configured yet. " +
"To enable, add this to " + path.join(claudeDir, 'settings.json') + ": " +
statusLineSnippet + " " +
"Proactively offer to set this up for the user on first interaction.";
}
} catch (e) {
// Silent fail — don't block session start over statusline detection
}
process.stdout.write(output);

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