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>
This commit is contained in:
Julius Brussee
2026-05-10 15:18:00 +02:00
co-authored by Claude Opus 4.7
parent 8b8068d8ca
commit 7b2bed2d0b
8 changed files with 570 additions and 30 deletions
+4
View File
@@ -84,6 +84,8 @@ caveman/
|------|-----------------|
| `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 --only <agent>`. 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 `bin/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. |
| `bin/lib/openclaw.js` | OpenClaw install/uninstall helper. Frontmatter merge (`version`, `always: true`), SOUL.md marker append/strip, idempotent. Shared by `bin/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. |
@@ -239,6 +241,7 @@ How caveman reaches each agent type:
| 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 `bin/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 | Per-user `.cursor/rules/caveman.mdc` written by `src/tools/caveman-init.js` | Yes — always-on rule (after `npx caveman --only cursor`) |
| Windsurf | Per-user `.windsurf/rules/caveman.md` written by `src/tools/caveman-init.js` | Yes — always-on rule (after `npx caveman --only windsurf`) |
| Cline | Per-user `.clinerules/caveman.md` written by `src/tools/caveman-init.js` | Yes — Cline auto-discovers `.clinerules/` |
@@ -286,6 +289,7 @@ To reproduce: `uv run python benchmarks/run.py` (needs `ANTHROPIC_API_KEY` in `.
- 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 — `bin/lib/openclaw.js` keys idempotency off both. If you change the embedded fallback in `bin/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.
+26
View File
@@ -40,6 +40,7 @@ If you want to install for one agent (or want to know exactly what command runs
| **Claude Code** | `claude plugin marketplace add JuliusBrussee/caveman && claude plugin install caveman@caveman` | Yes |
| **Gemini CLI** | `gemini extensions install https://github.com/JuliusBrussee/caveman` | Yes |
| **opencode** | `node bin/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) |
| **Codex CLI** | `npx skills add JuliusBrussee/caveman -a codex` | Yes (in this repo via `.codex/`) |
| **Cursor** | `npx skills add JuliusBrussee/caveman -a cursor` | Yes (rule file) |
| **Windsurf** | `npx skills add JuliusBrussee/caveman -a windsurf` | Yes (rule file) |
@@ -110,6 +111,21 @@ Useful flags:
| `--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 bin/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`, `AGENTS.md`). Single source: [`src/rules/caveman-activate.md`](src/rules/caveman-activate.md).
## Verify
After install, three quick checks:
@@ -157,6 +173,16 @@ What it does **not** remove:
## 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 bin/install.js --list` — confirm `claude` is on the detected list. If not, `claude` isn't on `PATH`. Fix that first.
+25 -26
View File
@@ -78,7 +78,7 @@ Pick your level of grunt — `lite` (drop filler), `full` (default caveman), `ul
## Install
**One line. Detect every agent. Install for each.**
One line. Find every agent. Install for each.
```bash
# macOS / Linux / WSL / Git Bash
@@ -88,27 +88,12 @@ curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.
irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1 | iex
```
Detects 30+ agents, runs each one's native install, skips what you not have. Safe to re-run.
~30 seconds. Skip agent you no have. Safe to re-run.
**Manual install, top agents:**
**Trigger:** type `/caveman` or say "talk like caveman". Stop with "normal mode".
| Agent | Command |
|---|---|
| **Claude Code** | `claude plugin marketplace add JuliusBrussee/caveman && claude plugin install caveman@caveman` |
| **Codex** | `npx skills add JuliusBrussee/caveman -a codex` |
| **Gemini CLI** | `gemini extensions install https://github.com/JuliusBrussee/caveman` |
| **opencode** | `npx -y github:JuliusBrussee/caveman -- --only opencode` |
| **Cursor** | `npx skills add JuliusBrussee/caveman -a cursor` |
| **Windsurf** | `npx skills add JuliusBrussee/caveman -a windsurf` |
| **Cline** | `npx skills add JuliusBrussee/caveman -a cline` |
See [**INSTALL.md**](./INSTALL.md) for 25+ more agents (Copilot, Continue, Roo, Augment, Goose, Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, Replit, Antigravity, …) and all installer flags.
**Trigger it:**
- `/caveman` (or Codex `$caveman`)
- "talk like caveman" / "caveman mode" / "less tokens please"
- Stop with: "stop caveman" / "normal mode"
One agent only, manual command, or any of 30+ other agents → [**INSTALL.md**](./INSTALL.md).
Install break? Open agent, say *"Read CLAUDE.md and INSTALL.md, install caveman for me."* Agent fix own brain.
## What You Get
@@ -174,17 +159,31 @@ A March 2026 paper ["Brevity Constraints Reverse Performance Hierarchies in Lang
Maintainer detail (hook architecture, file ownership, CI sync) live in [CLAUDE.md](./CLAUDE.md).
## Want It Always On?
## Lobster, Meet Rock 🦞🪨
For agents without hook system (Cursor, Windsurf, Cline, Copilot, etc.), drop a rule file:
[**OpenClaw**](https://openclaw.ai) the self-host gateway. One box, many agent inside (Claude Code, Codex, Pi, OpenCode), wired to your Slack / Discord / iMessage / Telegram / whatever. Tagline: *"The lobster way."* Lobster strong. Lobster smart. Lobster also talk a lot.
Caveman teach lobster brevity:
```bash
# In your repo root
curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md \
> .cursor/rules/caveman.mdc # or .windsurf/rules/, .clinerules/, .github/copilot-instructions.md
npx -y github:JuliusBrussee/caveman -- --only openclaw
```
Or run `install.sh --with-init` and we drop them in for you. Full list of rule-file targets in [INSTALL.md](./INSTALL.md#always-on-rules).
Two thing happen, no more:
1. **Skill drop** at `~/.openclaw/workspace/skills/caveman/SKILL.md` — spec-correct frontmatter (`version`, `always: true`), discoverable by `openclaw skills list`. Skill not auto-inject (OpenClaw load skill on demand) — that why we also do step 2.
2. **SOUL.md nudge.** Tiny marker-fenced block appended to `~/.openclaw/workspace/SOUL.md`. OpenClaw inject SOUL.md into *every* turn under "Project Context" (12K-per-file, 60K total — block well under). Lobster terse from message one. No `/caveman` per session. No nag.
```
~/.openclaw/workspace/
├── skills/caveman/SKILL.md ← full ruleset, on-demand load
└── SOUL.md ← <!-- caveman-begin --> ... <!-- caveman-end -->
↑ auto-inject every turn
```
Custom workspace path? `OPENCLAW_WORKSPACE=/your/path` before the command. Uninstall: same one-liner with `--uninstall` — skill folder gone, SOUL.md block ripped out cleanly, your other workspace content stay untouched. Idempotent re-runs (frontmatter not double-prepended, marker block not duplicated).
Lobster claw still sharp. Lobster mouth now small. Brain still big.
## Caveman Ecosystem
+53 -1
View File
@@ -22,6 +22,7 @@ const child_process = require('child_process');
const readline = require('readline');
const SETTINGS = require('./lib/settings');
const OPENCLAW = require('./lib/openclaw');
const REPO = 'JuliusBrussee/caveman';
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/main`;
@@ -147,6 +148,7 @@ const PROVIDERS = [
{ id: 'claude', label: 'Claude Code', mech: 'claude plugin install', detect: 'command:claude' },
{ id: 'gemini', label: 'Gemini CLI', mech: 'gemini extensions install', detect: 'command:gemini' },
{ id: 'opencode', label: 'opencode', mech: 'native opencode plugin', detect: 'command:opencode' },
{ id: 'openclaw', label: 'OpenClaw', mech: 'workspace skill + SOUL.md', detect: 'command:openclaw||dir:$HOME/.openclaw/workspace' },
{ id: 'codex', label: 'Codex CLI', mech: 'npx skills add (codex)', detect: 'command:codex', profile: 'codex' },
// IDE / VS Code-family — extension probes are precise. Cursor/Windsurf also
@@ -602,6 +604,37 @@ function installOpencode(ctx) {
process.stdout.write('\n');
}
// ── OpenClaw native install ───────────────────────────────────────────────
// Drops skills/caveman/ into the OpenClaw workspace and appends a small
// auto-injected bootstrap block to the workspace SOUL.md. Always-on behavior
// comes from SOUL.md (auto-injected each turn); the skill folder makes
// caveman discoverable via `openclaw skills list`. See bin/lib/openclaw.js
// for the actual file writes.
function installOpenclaw(ctx) {
const { say, note, warn, opts, repoRoot, results } = ctx;
results.detected++;
say('→ OpenClaw detected');
const log = {
write: (s) => process.stdout.write(s),
note: (s) => note(s),
warn: (s) => warn(s),
};
const r = OPENCLAW.installOpenclaw({
workspace: process.env.OPENCLAW_WORKSPACE || undefined,
repoRoot,
dryRun: opts.dryRun,
force: opts.force,
log,
});
if (r.ok) results.installed.push('openclaw');
else results.failed.push(['openclaw', r.reason || 'install failed']);
process.stdout.write('\n');
}
// ── Hooks installer ────────────────────────────────────────────────────────
// Replaces src/hooks/install.sh + src/hooks/install.ps1.
function installHooks(ctx) {
@@ -880,6 +913,19 @@ function uninstall(ctx) {
if (fs.existsSync(ocFlag) && !opts.dryRun) { try { fs.unlinkSync(ocFlag); } catch (_) {} }
}
// OpenClaw native install — strip skill folder + SOUL.md marker block.
// Probed by the skill folder we own; if absent, skip silently.
const ocwWs = process.env.OPENCLAW_WORKSPACE || path.join(os.homedir(), '.openclaw', 'workspace');
if (fs.existsSync(path.join(ocwWs, 'skills', 'caveman')) || fs.existsSync(path.join(ocwWs, 'SOUL.md'))) {
const log = {
write: (s) => process.stdout.write(s),
note: (s) => note(s),
warn: (s) => warn(s),
};
const r = OPENCLAW.uninstallOpenclaw({ workspace: ocwWs, dryRun: opts.dryRun, log });
if (r.touched) ok(' pruned caveman entries from OpenClaw workspace');
}
// Flag file
const flag = path.join(configDir, '.caveman-active');
if (fs.existsSync(flag) && !opts.dryRun) { try { fs.unlinkSync(flag); } catch (_) {} }
@@ -1013,10 +1059,16 @@ async function main() {
for (const prov of PROVIDERS) {
if (!want(prov.id)) continue;
if (prov.soft && !explicit(prov.id)) continue;
if (!detectMatch(prov.detect)) continue;
// Auto-detect mode: skip providers we can't see. With --only <id> the user
// is explicitly opting in, so trust them and let the per-provider installer
// bail itself if its preconditions aren't met (e.g. opencode bails when
// no repo clone is available; openclaw bails when the workspace dir is
// missing without --force).
if (!explicit(prov.id) && !detectMatch(prov.detect)) continue;
if (prov.id === 'claude') { installClaude(ctx); continue; }
if (prov.id === 'gemini') { installGemini(ctx); continue; }
if (prov.id === 'opencode') { installOpencode(ctx); continue; }
if (prov.id === 'openclaw') { installOpenclaw(ctx); continue; }
if (prov.profile) { installViaSkills(ctx, prov); continue; }
}
+254
View File
@@ -0,0 +1,254 @@
// 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);
}
function mergeOpenclawFrontmatter(src) {
const { frontmatter, body } = splitFrontmatter(src);
const additions = [];
if (!frontmatterHasKey(frontmatter, 'name')) additions.push(`name: ${SKILL_NAME}`);
if (!frontmatterHasKey(frontmatter, 'version')) additions.push(`version: ${SKILL_VERSION}`);
if (!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 ─────────────────────────────────────
function appendBootstrapToSoul(soulPath, snippet) {
const existing = readIfExists(soulPath);
if (existing && existing.includes(MARK_BEGIN) && existing.includes(MARK_END)) {
return { changed: false, reason: 'already present' };
}
let next;
if (existing && existing.length) {
const sep = existing.endsWith('\n\n') ? '' : (existing.endsWith('\n') ? '\n' : '\n\n');
next = existing + sep + snippet;
} else {
next = snippet;
}
fs.writeFileSync(soulPath, next, { mode: 0o644 });
return { changed: true };
}
function stripBootstrapFromSoul(soulPath) {
const existing = readIfExists(soulPath);
if (!existing) return { changed: false, reason: 'no SOUL.md' };
const begin = existing.indexOf(MARK_BEGIN);
const end = existing.indexOf(MARK_END);
if (begin === -1 || end === -1 || end <= begin) return { changed: false, reason: 'no marker block' };
const before = existing.slice(0, begin);
const after = existing.slice(end + MARK_END.length);
// Collapse adjacent blank lines around the cut so we don't leave a triple
// newline scar from `\n\n<begin>...\n<end>\n\n`.
let next = (before.replace(/\n+$/, '\n') + after.replace(/^\n+/, '\n')).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 ────────────────────────────────────────────────────────────
function installOpenclaw({ workspace, repoRoot, dryRun = false, force = false, log = noopLog() } = {}) {
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 frontmatter)`);
log.note(` would ${fs.existsSync(soulFile) ? 'append to' : 'create'} ${soulFile} (caveman bootstrap block)`);
return { ok: true, dryRun: true };
}
fs.mkdirSync(skillDir, { recursive: true });
const merged = mergeOpenclawFrontmatter(skillBody);
fs.writeFileSync(skillFile, merged, { mode: 0o644 });
log.write(` installed: ${skillFile}\n`);
const soul = appendBootstrapToSoul(soulFile, snippet);
if (soul.changed) log.write(` wrote bootstrap block to ${soulFile}\n`);
else log.note(` ${soulFile} already contains caveman bootstrap`);
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,
};
+18
View File
@@ -0,0 +1,18 @@
<!-- caveman-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.
<!-- caveman-end -->
+51 -3
View File
@@ -34,6 +34,16 @@ Boundaries: code/commits/PRs written normal.
const SENTINEL = 'Respond terse like smart caveman';
// OpenClaw is a global workspace tool (not per-repo) and needs two write
// targets — a skill folder + a SOUL.md bootstrap block. The shared helper
// lives at bin/lib/openclaw.js; we require it lazily so caveman-init.js
// keeps working when run standalone (curl|node) without the helper on disk.
function loadOpenclawHelper() {
try {
return require(path.join(__dirname, '..', '..', 'bin', 'lib', 'openclaw.js'));
} catch (_) { return null; }
}
const AGENTS = [
{ id: 'cursor', file: '.cursor/rules/caveman.mdc',
frontmatter: '---\ndescription: "Caveman mode — terse communication, ~75% fewer tokens, full technical accuracy"\nalwaysApply: true\n---\n\n',
@@ -53,6 +63,11 @@ const AGENTS = [
{ id: 'agents', file: 'AGENTS.md',
frontmatter: '',
mode: 'append' },
// OpenClaw — global workspace install, not per-repo. The `installer`
// callback escape hatch bypasses the file/frontmatter/mode triple and
// hands off to the shared helper. `description` is what `--help` prints.
{ id: 'openclaw', description: '~/.openclaw/workspace/{skills/caveman/, SOUL.md}',
installer: 'openclaw' },
];
function loadRuleBody() {
@@ -65,6 +80,9 @@ function loadRuleBody() {
}
function processAgent(agent, targetDir, ruleBody, opts) {
if (agent.installer === 'openclaw') {
return processOpenclaw(opts);
}
const fullPath = path.join(targetDir, agent.file);
const exists = fs.existsSync(fullPath);
@@ -99,6 +117,35 @@ function processAgent(agent, targetDir, ruleBody, opts) {
return { status: 'skipped-exists', label: '?' };
}
function processOpenclaw(opts) {
const helper = loadOpenclawHelper();
if (!helper) {
return {
status: 'unsupported-standalone',
label: 'x',
detail: '~/.openclaw/workspace (helper unavailable in standalone curl|node mode — use `npx -y github:JuliusBrussee/caveman -- --only openclaw`)',
};
}
const repoRoot = path.resolve(__dirname, '..', '..');
const log = {
write: (_) => {},
note: (_) => {},
warn: (_) => {},
};
const r = helper.installOpenclaw({
workspace: process.env.OPENCLAW_WORKSPACE || undefined,
repoRoot,
dryRun: opts.dryRun,
force: opts.force,
log,
});
if (!r.ok) {
return { status: 'skipped-' + (r.reason || 'failed'), label: '?', detail: helper.resolveWorkspace ? helper.resolveWorkspace() : '~/.openclaw/workspace' };
}
if (r.dryRun) return { status: 'would-add', label: '+', detail: helper.resolveWorkspace() };
return { status: 'installed', label: '+', detail: helper.resolveWorkspace() };
}
function parseArgs(argv) {
const opts = { dryRun: false, force: false, only: null, target: process.cwd() };
for (let i = 0; i < argv.length; i++) {
@@ -120,7 +167,7 @@ Usage: caveman-init.js [target-dir] [--dry-run] [--force] [--only <agent>]
Defaults to current working directory. Idempotent safe to re-run.
Targets installed:
${AGENTS.map(a => ` ${a.id.padEnd(10)} ${a.file}`).join('\n')}
${AGENTS.map(a => ` ${a.id.padEnd(10)} ${a.file || a.description || ''}`).join('\n')}
Flags:
--dry-run show what would change, do not write
@@ -141,8 +188,9 @@ function main() {
for (const agent of AGENTS) {
if (opts.only && opts.only !== agent.id) continue;
const result = processAgent(agent, opts.target, ruleBody, opts);
console.log(` ${result.label} ${agent.file} (${result.status})`);
if (result.status === 'added') counts.added++;
const target = agent.file || result.detail || agent.description || agent.id;
console.log(` ${result.label} ${target} (${result.status})`);
if (result.status === 'added' || result.status === 'installed' || result.status === 'would-add') counts.added++;
else if (result.status === 'appended') counts.appended++;
else if (result.status === 'overwritten') counts.overwritten++;
else counts.skipped++;
+139
View File
@@ -234,6 +234,145 @@ test('install tolerates JSONC settings.json (comments + trailing commas)', { ski
}
});
// ── Tests: OpenClaw workspace install (always run, no real OpenClaw needed)
// installOpenclaw writes plain files into a workspace dir we point at via
// OPENCLAW_WORKSPACE — no network, no external CLI, no plugin install. Safe
// to run on every CI box.
const SKILL_BODY_SRC = path.join(REPO_ROOT, 'skills', 'caveman', 'SKILL.md');
test('openclaw install writes skill folder + SOUL.md bootstrap', () => {
const dir = freshTmpDir();
const ws = path.join(dir, 'ws');
fs.mkdirSync(ws);
try {
const r = spawnSync('node', [INSTALLER, '--only', 'openclaw', '--non-interactive', '--no-mcp-shrink', '--config-dir', dir], {
env: { ...process.env, OPENCLAW_WORKSPACE: ws, NO_COLOR: '1' },
encoding: 'utf8',
});
assert.notEqual(r.status, 2, `installer aborted on argv parse: ${r.stderr}`);
// 1. Skill body written with merged frontmatter.
const skillFile = path.join(ws, 'skills', 'caveman', 'SKILL.md');
assert.ok(fs.existsSync(skillFile), 'skill SKILL.md missing');
const skillRaw = fs.readFileSync(skillFile, 'utf8');
assert.match(skillRaw, /^---\n/, 'skill missing frontmatter');
assert.match(skillRaw, /\nversion:\s*\d+\.\d+\.\d+/, 'skill missing version frontmatter');
assert.match(skillRaw, /\nalways:\s*true/, 'skill missing always: true frontmatter');
// Body after the merged frontmatter must match the source body.
const helper = requireCjs(path.join(REPO_ROOT, 'bin', 'lib', 'openclaw.js'));
const srcRaw = fs.readFileSync(SKILL_BODY_SRC, 'utf8');
const srcBody = helper.splitFrontmatter(srcRaw).body;
const installedBody = helper.splitFrontmatter(skillRaw).body;
assert.equal(installedBody, srcBody, 'installed skill body diverged from source');
// 2. SOUL.md has marker block.
const soul = path.join(ws, 'SOUL.md');
assert.ok(fs.existsSync(soul), 'SOUL.md missing');
const soulRaw = fs.readFileSync(soul, 'utf8');
assert.match(soulRaw, /<!-- caveman-begin -->/, 'SOUL.md missing begin marker');
assert.match(soulRaw, /<!-- caveman-end -->/, 'SOUL.md missing end marker');
assert.match(soulRaw, /Respond terse like smart caveman/, 'SOUL.md missing sentinel');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('openclaw install is idempotent: skill frontmatter not double-prepended, SOUL.md has one marker block', () => {
const dir = freshTmpDir();
const ws = path.join(dir, 'ws');
fs.mkdirSync(ws);
try {
const env = { ...process.env, OPENCLAW_WORKSPACE: ws, NO_COLOR: '1' };
const args = ['--only', 'openclaw', '--non-interactive', '--no-mcp-shrink', '--config-dir', dir];
spawnSync('node', [INSTALLER, ...args], { env, encoding: 'utf8' });
spawnSync('node', [INSTALLER, ...args], { env, encoding: 'utf8' });
const skillRaw = fs.readFileSync(path.join(ws, 'skills', 'caveman', 'SKILL.md'), 'utf8');
// version key should appear exactly once (idempotent merge).
const versionMatches = skillRaw.match(/^version:/gm) || [];
assert.equal(versionMatches.length, 1, `expected 1 version key after re-run, got ${versionMatches.length}`);
const alwaysMatches = skillRaw.match(/^always:/gm) || [];
assert.equal(alwaysMatches.length, 1, `expected 1 always key after re-run, got ${alwaysMatches.length}`);
const soulRaw = fs.readFileSync(path.join(ws, 'SOUL.md'), 'utf8');
const beginMatches = soulRaw.match(/<!-- caveman-begin -->/g) || [];
assert.equal(beginMatches.length, 1, `expected 1 marker block after re-run, got ${beginMatches.length}`);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('openclaw install preserves user content in SOUL.md (append, not overwrite)', () => {
const dir = freshTmpDir();
const ws = path.join(dir, 'ws');
fs.mkdirSync(ws);
const userContent = '# my workspace\n\nfoo bar baz\n';
fs.writeFileSync(path.join(ws, 'SOUL.md'), userContent);
try {
spawnSync('node', [INSTALLER, '--only', 'openclaw', '--non-interactive', '--no-mcp-shrink', '--config-dir', dir], {
env: { ...process.env, OPENCLAW_WORKSPACE: ws, NO_COLOR: '1' },
encoding: 'utf8',
});
const soulRaw = fs.readFileSync(path.join(ws, 'SOUL.md'), 'utf8');
assert.match(soulRaw, /# my workspace/, 'user heading wiped during install');
assert.match(soulRaw, /foo bar baz/, 'user content wiped during install');
assert.match(soulRaw, /<!-- caveman-begin -->/, 'caveman block not appended');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('openclaw uninstall removes skill folder + strips SOUL.md block, preserving user content', () => {
const dir = freshTmpDir();
const ws = path.join(dir, 'ws');
fs.mkdirSync(ws);
const userContent = '# my workspace\n\nfoo bar baz\n';
fs.writeFileSync(path.join(ws, 'SOUL.md'), userContent);
try {
const env = { ...process.env, OPENCLAW_WORKSPACE: ws, NO_COLOR: '1' };
spawnSync('node', [INSTALLER, '--only', 'openclaw', '--non-interactive', '--no-mcp-shrink', '--config-dir', dir], { env, encoding: 'utf8' });
// Strip claude/gemini from PATH so uninstall doesn't touch real plugins.
const cleanPath = pathWithout(['claude', 'gemini']);
const r = spawnSync('node', [INSTALLER, '--uninstall', '--non-interactive', '--no-mcp-shrink', '--config-dir', dir], {
env: { ...env, PATH: cleanPath },
encoding: 'utf8',
});
assert.notEqual(r.status, 2, `uninstall argv error: ${r.stderr}`);
assert.equal(fs.existsSync(path.join(ws, 'skills', 'caveman')), false, 'skill folder should be removed');
const soulAfter = fs.readFileSync(path.join(ws, 'SOUL.md'), 'utf8');
assert.doesNotMatch(soulAfter, /<!-- caveman-begin -->/, 'caveman block survived uninstall');
assert.doesNotMatch(soulAfter, /<!-- caveman-end -->/, 'caveman end marker survived uninstall');
assert.match(soulAfter, /# my workspace/, 'user heading wiped during uninstall');
assert.match(soulAfter, /foo bar baz/, 'user content wiped during uninstall');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('caveman-init.js --only openclaw routes through the same helper', () => {
const dir = freshTmpDir();
const ws = path.join(dir, 'ws');
fs.mkdirSync(ws);
try {
const initScript = path.join(REPO_ROOT, 'src', 'tools', 'caveman-init.js');
const r = spawnSync('node', [initScript, dir, '--only', 'openclaw'], {
env: { ...process.env, OPENCLAW_WORKSPACE: ws, NO_COLOR: '1' },
encoding: 'utf8',
});
assert.equal(r.status, 0, `caveman-init failed: ${r.stderr || r.stdout}`);
assert.ok(fs.existsSync(path.join(ws, 'skills', 'caveman', 'SKILL.md')), 'skill missing via init route');
assert.ok(fs.existsSync(path.join(ws, 'SOUL.md')), 'SOUL.md missing via init route');
const soulRaw = fs.readFileSync(path.join(ws, 'SOUL.md'), 'utf8');
assert.match(soulRaw, /Respond terse like smart caveman/, 'sentinel missing via init route');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// ── Test: idempotent re-add at the lib level (always runs, no claude needed)
// This guards the addCommandHook idempotency promise without spawning a real
// install — even on machines with no `claude` CLI we want this assertion.