Commit Graph

17 Commits

Author SHA1 Message Date
Julius Brussee 1b7ff8fae9 fix(proxy): scope repo intelligence to project source and require direct evidence (#901)
* fix(proxy): skip vendor trees in repo intel

BM25 was indexing .pixi/envs and other dependency
dirs, then injecting those file:line guesses every
turn. Exclude known vendor trees and drop the path
block when no local scout is configured.

Closes #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): detect dep trees by content, not name

Name denylist missed conda envs, undotted pixi, and
virtualenvs under any name, while dropping real
first-party build/ and vendor/ dirs.

- listFiles prefers `git ls-files --exclude-standard`,
  so the project's own .gitignore defines vendored
- walk fallback identifies environments by contents
  (conda-meta, pyvenv.cfg, site-packages child),
  catching the interpreter stdlib they carry
- ambiguous build/dist/target/vendor need a sibling
  manifest before exclusion
- symlink and junction dirs are never followed

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): treat the indexed repo as untrusted input

Second review found the git listing both unsafe and
incomplete.

- gitCommand pins core.fsmonitor and other exec
  vectors off and strips GIT_*; fsmonitor runs during
  an index read, so `ls-files` in a hostile clone ran
  attacker code at session start
- git-listed paths now run the same content filter as
  the walk, so an un-ignored conda prefix or committed
  vendor tree no longer rides in on git's say-so
- an empty-but-successful listing means the cwd is
  ignored by an enclosing repo; fall back to the walk
  instead of reporting a complete map
- Direct matches the file name, not the whole path:
  a term like "src" marked every file beneath it
- ListingBasis discloses which path ran, hashed in

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): log declined evidence, document invariants

A declined turn stays silent to the model but now
records repository_evidence_declined_no_direct_match,
so weak evidence is distinguishable from a broken or
disabled mechanism.

Adds a positive control to the suppression test and
records the gating, filtering, and git-hardening
invariants in proxy/CLAUDE.md.

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(installer): make prune path flavour explicit, not host-derived

pruneOrphanedManagedHooks guards against pruning a hook whose path is
absolute on the other platform. That guard is POSIX-only by construction:
win32's absolute set is a superset of posix's, so a Windows host reads
C:\... natively and prunes it. The test asserting the guard hard-coded a
Windows path and so failed on the windows CI runner.

Select the path flavour from an explicit platform arg (default
process.platform, same as rewriteLegacyManagedHookCommands) so the POSIX
side stays reachable from a Windows runner. Host behaviour unchanged.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* test(cli): escape stub preload path in NODE_OPTIONS

Node parses NODE_OPTIONS itself and treats a backslash inside a quoted
value as an escape, so the raw Windows path reached --require with every
separator eaten:

  Cannot find module 'C:UsersRUNNER~1AppDataLocalTempcave-pi-enableXbinstub-dispatch.cjs'

Every stub-backed process died in the preload, failing all six pi enable
and wrap round trips. The quotes must stay for profile names with spaces,
so double the separators instead. POSIX is unaffected — stubEnv returns
before this on non-Windows.

Never caught before because npm test failed earlier in the same job, so
test:windows has not run since these tests joined the gate.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): harden every untrusted git call, not just repo intelligence

`repointel` hardened its own `git ls-files` and left three other call sites
raw. `nativehook.currentRepositoryState` runs `git status` on EVERY hook
event, and `status` refreshes the index, so it triggers `core.fsmonitor`
harder than `ls-files` does — verified: the sentinel hook fires. Same for
`store/detect_outcomes` and the two `execFileSync` sites in the CLI.

- `proxy/internal/gitsafe` and `packages/cli/src/git-safe.ts` now own the
  `-c` override list; all five call sites route through them.
- `verify_repo.py` fails the build on a NEW raw `git -C <dir>` call site,
  which is the regression class this bug actually belongs to.
- `Bundle.DirectOnly()` strips metadata-only items BEFORE the evidence
  object is written. Filtering only at render time left the BM25-proximity
  guesses stored behind the `ccr://` handle, one retrieval from the model,
  which is what the block refuses to render.
- `gitListBudget` caps git at half the caller's remaining warm budget. 2s
  of a 3s window left the fallback walk — the MORE expensive path — unable
  to finish, so the map failed closed in exactly the case the fallback
  exists for.
- `NormalizeTerms` floors terms at 3 characters, matching the hook that
  produces them: direct evidence is a substring test on a file's own name,
  so `go`/`py`/`js` marked every file of that language a direct hit.

Regression tests verified to fail without their fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(build): stop wedge generation racing itself on Windows

mcp, shrink, and browse each run gen-wedge-installer from their own
prepack/pretest, and every run writes all four targets, not just its own.
`pnpm -r test` runs those packages in parallel, so several processes
copy onto the same file at once and Windows fails the loser:

  EBUSY: resource busy or locked, copyfile
  packages/shared/binary-installer/installer.mjs
  -> mcp/bin/binary-installer.generated.mjs

The bytes are identical every time and the outputs are committed, so the
race was over writes that change nothing. Skip when the destination
already matches, and land a real change through a temp file + rename so
no concurrent reader sees a partial file. Verified with a 12-way
concurrent storm, and against corrupted outputs to prove the write path
still restores them.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): keep system git config readable while hardening

GIT_CONFIG_NOSYSTEM defends against nothing here — /etc/gitconfig is
root-owned, the threat is the repository's own .git/config, and the `-c`
overrides beat every config level anyway. What it does cost is org-wide
`safe.directory` allowances: without them `git status` fails "dubious
ownership" on shared and CI machines, repository state comes back empty,
and reuse silently stops. Dropped from both wrappers, with a test so it
does not come back.

Also names the fsmonitor tradeoff at the hot call site: disabling it taxes
the people who configured it because their repo is huge, and status has a
100ms budget. That degrades safely; moving the budget is a latency
decision for every user and needs a measurement this fix does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(build): pin wedge generation against the concurrency it now survives

The EBUSY fix has no committed regression proof, and the failure only
appears when several packages run the script at once — which is exactly
what `pnpm -r test` does and what a single-process check never reproduces.
Eight concurrent runs must all succeed and leave the shared outputs
byte-identical; a drifted output must still be repaired, so skip-if-
identical cannot regress into skip-always.

Runs in root `npm test`, which is the gate the Windows job failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
Co-authored-by: Julius Brussee <juliusbrussee@getlockedin.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 01:12:21 +02:00
Julius Brussee e4e4989085 fix(xplat): stop the installer and uninstaller from breaking their own state
Marker handling, both directions. `installOpencode` and caveman-init treated
"both markers present" as a fence, so an orphan BEGIN or an END above a
BEGIN ran the slice arithmetic on end === -1: AGENTS.md re-appended itself
from byte 19 and compounded on every re-run, and a per-repo refresh would
have replaced everything between an orphan BEGIN and a later block's END,
user content included. Both now require exactly one matched pair, in order,
and report damage instead of writing.

Uninstall ordering. uninstall.sh/uninstall.ps1 deleted the hook files first
and edited settings.json second, so a failed or refused edit left Claude
Code pointing at scripts that no longer exist — `Cannot find module
…caveman-activate.js` on every session start (#471). Settings first; files
only once nothing references them. Neither script overwrites an existing
.bak any more (it is the user's only pre-caveman copy), and a JSONC
settings.json now aborts the edit with the working command instead of
half-uninstalling.

install.ps1's "already wired" probe matched the bare substring `caveman`,
which any user hook mentioning the word in a path satisfies (#593) — it
silently skipped wiring and left caveman inactive. It now probes for the
exact script we write per event, mirroring install.sh.

The SessionStart watchdog no longer claims `startup` when it fires. That is
the one source that resets the mode, so a payload slow enough to time out on
a compact/resume event dropped a mid-session `ultra` back to the default.
An unknown source preserves a valid existing flag, and the deadline moves to
3s — still well inside the host's 5s budget, but above a cold Windows/AV
start.

caveman-init writes retry a rename that Windows loses to OneDrive/AV
handles (EPERM/EBUSY/EACCES), the shrink MCP refuses an implausible .cmd
shim before reading it, and pruneOrphanedManagedHooks no longer prunes a
hook whose path is absolute on the other platform — existence is only
knowable for our own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmbqWDtNexzDfH5V3BMRz
2026-08-20 04:14:49 +02:00
Julius Brussee 5429b5da47 fix(xplat): stop hooks stalling on Windows; own only our own config entries
Whole-repo review of the surfaces that mutate a user's agent config, plus a
cross-platform sweep focused on Windows.

Hooks (Windows):
- caveman-mode-tracker finished its work in ~50ms then blocked until the host
  killed it at the 5s budget: the 'data' listener REFERENCES the stdin handle,
  so pause() alone keeps the event loop alive until the host closes the write
  end, and on Windows that close lags arbitrarily. Reproduced off-Windows.
  This is the shape of #819. Fixed with process.stdin.unref().
- caveman-activate used readFileSync(0), which blocks INSIDE the read syscall
  until EOF — no deadline can interrupt it. The #729/#833 fix had been applied
  to the tracker and never to its sibling. Rewritten event-driven, acting on
  the first complete payload, with a watchdog so a payload that never arrives
  still activates instead of forfeiting the session.
- hookCommand emitted PowerShell call-operator syntax into settings.json, but
  Claude Code's Windows hook shell is Git Bash, which rejects it outright:
  `syntax error near unexpected token '&'`. Every standalone Windows install
  had two dead hooks (#835). Both emitters now route through one bash-safe
  shape.
- activate now threads the payload cwd, so a checked-in .caveman.json
  defaultMode "off" is honored for the session's directory (#634), and a JSONC
  settings.json no longer suppresses the statusline nudge.

Config ownership:
- src/hooks/uninstall.{sh,ps1} matched hooks by the substring "caveman" and
  filtered whole entries, deleting user hooks that merely mention the word and
  dropping foreign handlers that shared a matcher group (#593). Ported the
  exact-basename matcher from bin/lib/settings.js and switched to filtering the
  inner handler list. Verified against real PowerShell: the old code left zero
  user hooks standing.
- install.{sh,ps1} backed up settings.json unguarded, so a --force reinstall
  overwrote the only pre-caveman copy with the merged file. Back up once.
- OpenClaw overwrote a hand-edited workspace skill with no backup.
- caveman-init appended unfenced blocks to tracked repo files, so they could
  be neither refreshed nor removed; now marker-fenced and written atomically.
- opencode AGENTS.md and OpenClaw SOUL.md were presence-checked, so a ruleset
  edit never reached anyone already installed. Both refresh in place.
- opencode's system-prompt hook had no idempotency guard; it now rewrites its
  line rather than stacking another onto the context window.
- readSettings reports when it stripped comments so both write paths warn and
  point at the backup instead of silently dropping a user's annotations.

Skills: skills/ is auto-discovered wholesale by the plugin loader, so 13
undocumented skills would have shipped into every user's agent on the next
release. Documented them and added a verify_repo check that fails when a
skills/ directory is named in neither README.md nor CLAUDE.md.

CI: a windows-powershell job covering the surface engine-ci never exercised —
the PowerShell installer round trip, and a bash -n gate over every hook command
the installer writes. That gate alone catches #835.

Tests: every fix above is pinned by a test that fails against the old code,
including tests/test_hook_stdin_lifecycle.js, which reproduces the Windows
lagging-close condition on any platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
2026-08-20 04:14:48 +02:00
Julius Brussee 783dbeafb5 fix(xplat): close Windows/macOS data-loss, encoding, and hook-budget gaps
Findings from a whole-repo cross-platform audit.

Data loss:
- evals/llm_run.py wrote the snapshot with write_text(ensure_ascii=False) and
  no encoding: on Windows that truncates results.json to 0 bytes, then raises
  at the first non-cp1252 char. Destroyed the committed snapshot and the run.
- compress.py read the user's target with errors="ignore", silently dropping
  undecodable bytes, backing up the mangled text, passing its own readback
  check, then overwriting the original. Now decodes strictly and refuses.

Broken on Windows:
- benchmarks/ and evals/ read SKILL.md, README.md and results.json without an
  encoding; all three raise on a cp1252 decode.
- cavemem's Python client used text=True with no encoding, so a non-ASCII
  memory failed to store and any recall came back mojibake.
- native-hook-fast spawned a second full Node process from inside a hook with
  no timeout at all.

Also fixed:
- Backported the pnpm cross-drive shim fix to the five copies that never got
  it; hoisted two inline parsers so all of them are unit-testable.
- portable-command now resolves through PATH/PATHEXT instead of silently
  no-op'ing on a bare command name.
- uninstall.ps1 writes a temp .js instead of node -e (matches install.ps1).
- Real backoff on the flag rename retry; EPERM/EBUSY retry when replacing a
  running binary.
- Hooks act on the first complete JSON payload instead of blocking on EOF;
  the stats child timeout drops below the host's hook budget.
- Statusline tests run the .ps1 on Windows instead of skipping.
- Added the macOS CI job the repo never had.

Left unfixed and annotated in place: the win32 hookCommand emits PowerShell
call-operator syntax while #835 describes the runner as git-bash /bin/sh.
Needs a real Windows host to settle; guessing would break whichever works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NveRZs9mV97iX7HSgq3BYF
2026-08-20 04:14:48 +02:00
Julius Brussee 53c040bfd1 fix(opencode): drop provider-less model values from agent frontmatter
opencode 1.18.18 parses `model: haiku` as provider "haiku" plus an empty model
ID and fails at runtime with `Model not found: haiku/`. The installer transform
stripped Claude-only `tools:` frontmatter but left Claude-only model aliases.

Drops any `model:` value with no provider prefix while copying agent
frontmatter for opencode, so the subagent inherits the invoking primary model.
Slash-qualified ids stay byte-identical.

The test is the shape — does the value contain a slash — not membership in a
list of known aliases. A closed {haiku, sonnet, opus} set covers today's
shipped agents and silently lets through every other provider-less value Claude
Code accepts: `inherit`, a full dated id like claude-haiku-4-5-20251001, or any
future alias, each producing the same runtime failure. A trailing YAML comment
is stripped before the check; an empty value is left alone rather than
silently repaired.

Rewriting to a concrete id (anthropic/claude-haiku-4-5) would preserve the
cheap-model intent but assumes an anthropic provider is configured; for an
opencode pointed elsewhere that turns a working subagent into a hard failure.
A subagent costing more than intended still works.

Renames stripOpencodeAgentTools to transformOpencodeAgentFrontmatter — it no
longer only strips tools.

Fixes #840
2026-08-15 16:51:52 +02:00
Julius Brussee c72984e439 fix(windows): harden hooks and BSL docs
Improve Windows support across installer, CLI hooks, binary lookup, hidden child processes, JetBrains detection, and Chrome discovery. Add real Windows ACL checks for SQLite parent directories instead of skipping the security guard.

Also expand BSL-1.1 coverage/documentation for new engine-linked runtime modules (`cacheengine`, `rewriter`, `browse`) and add repo/CI checks so license boundaries and cross-platform test paths stay enforced.
2026-08-13 17:27:59 +02:00
Julius Brussee 82864c8f4c feat: ship Caveman 2.0.0 — engine, proxy, SDKs, unified CLI
Single-commit port of the Caveman 2 tree onto the original repo history
(candidate caveman-2@88878a4 applied over main@3098342). No unrelated-history
merge; original history untouched.

Preserved v1 surfaces:
- skills/caveman/SKILL.md byte-for-byte, plus plugin mirror and caveman.skill zip
- legacy CI (Node 18/20/22 installer + Python 3.11 suites) alongside engine-ci
- cli/install.js compatibility shim forwarding to bin/install.js
- .codex/config.toml ships both hooks and codex_hooks for older Codex (#617)
- README Trendshift + Skills.sh badges and local star-history asset
- v1.10.0-pinned curl installs; benchmarks/tmp-starcharts ignore rules

Fixed for public same-repo release:
- SECURITY.md vulnerability reporting points at this repository
- release-binaries.yml publishes to this repo via job-scoped GITHUB_TOKEN
  (cross-repo PAT removed); binary-release environment still gates signing
- packages/cli/PUBLISHING.md rewritten for packages/ layout and same-repo flow
- profile-contract and provider-catalog lanes skip lane-scope gates on
  initial import so the launch PR can gate on their real validation steps
- ANNOUNCEMENT.md network claim corrected: compression is local; requests
  still go to the provider you configured

Caveman 2 stays opt-in: bin-v1.0.0 binaries and @caveman-ai/cli publish
separately after this lands and soaks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLvb5QniwYjmcawyssQNw9
2026-08-11 16:46:10 +02:00
Julius Brussee 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 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
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 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 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
Julius Brussee 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 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 Brussee 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 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