@abarcenas/ai-workflow-template 1.2.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/instructions/learned-knowledge.instructions.md +46 -0
- package/{.github → .agents}/skills/architecture-blueprint-generator/SKILL.md +57 -1
- package/.agents/skills/cavecrew/SKILL.md +82 -0
- package/.agents/skills/caveman/SKILL.md +74 -0
- package/.agents/skills/caveman-commit/SKILL.md +65 -0
- package/.agents/skills/caveman-compress/README.md +163 -0
- package/.agents/skills/caveman-compress/SECURITY.md +31 -0
- package/.agents/skills/caveman-compress/SKILL.md +111 -0
- package/.agents/skills/caveman-compress/scripts/__init__.py +9 -0
- package/.agents/skills/caveman-compress/scripts/__main__.py +3 -0
- package/.agents/skills/caveman-compress/scripts/benchmark.py +78 -0
- package/.agents/skills/caveman-compress/scripts/cli.py +85 -0
- package/.agents/skills/caveman-compress/scripts/compress.py +254 -0
- package/.agents/skills/caveman-compress/scripts/detect.py +121 -0
- package/.agents/skills/caveman-compress/scripts/validate.py +213 -0
- package/.agents/skills/caveman-help/SKILL.md +59 -0
- package/.agents/skills/caveman-review/SKILL.md +55 -0
- package/.agents/skills/caveman-stats/SKILL.md +10 -0
- package/.agents/skills/compress/SKILL.md +111 -0
- package/.agents/skills/compress/scripts/__init__.py +9 -0
- package/.agents/skills/compress/scripts/__main__.py +3 -0
- package/.agents/skills/compress/scripts/benchmark.py +78 -0
- package/.agents/skills/compress/scripts/cli.py +85 -0
- package/.agents/skills/compress/scripts/compress.py +254 -0
- package/.agents/skills/compress/scripts/detect.py +121 -0
- package/.agents/skills/compress/scripts/validate.py +213 -0
- package/AGENTS.md +149 -0
- package/README.md +197 -60
- package/opencode.mcp.example.json +54 -0
- package/package.json +13 -5
- package/scripts/mcp/playwright-mcp-launcher.js +46 -0
- package/scripts/sync.js +50 -12
- package/.github/agents/implementation-plan.agent.md +0 -192
- package/.github/agents/plan.agent.md +0 -135
- package/.github/agents/research-spike.agent.md +0 -204
- package/.github/prompts/copilot-instructions-blueprint-generator.prompt.md +0 -317
- /package/{.github → .agents}/instructions/agent-safety.instructions.md +0 -0
- /package/{.github → .agents}/instructions/agent-skill.instructions.md +0 -0
- /package/{.github → .agents}/instructions/agent.instructions.md +0 -0
- /package/{.github → .agents}/instructions/ai-prompt-engineering-safety.instructions.md +0 -0
- /package/{.github → .agents}/instructions/angular.instructions.md +0 -0
- /package/{.github → .agents}/instructions/context7.instructions.md +0 -0
- /package/{.github → .agents}/instructions/go.instructions.md +0 -0
- /package/{.github → .agents}/instructions/html-to-css-guide.instructions.md +0 -0
- /package/{.github → .agents}/instructions/instructions.instructions.md +0 -0
- /package/{.github → .agents}/instructions/markdown.instructions.md +0 -0
- /package/{.github → .agents}/instructions/memory-bank.instructions.md +0 -0
- /package/{.github → .agents}/instructions/nodejs-vitest.instructions.md +0 -0
- /package/{.github → .agents}/instructions/playwright-typescript.instructions.md +0 -0
- /package/{.github → .agents}/instructions/prompt.instructions.md +0 -0
- /package/{.github → .agents}/instructions/security-owasp.instructions.md +0 -0
- /package/{.github → .agents}/instructions/shell.instructions.md +0 -0
- /package/{.github → .agents}/instructions/task-implementation.instructions.md +0 -0
- /package/{.github → .agents}/instructions/typescript.instructions.md +0 -0
- /package/{.github → .agents}/prompts/documentation-writer.prompt.md +0 -0
- /package/{.github → .agents}/prompts/editconfig.prompt.md +0 -0
- /package/{.github → .agents}/prompts/playwright-explore.prompt.md +0 -0
- /package/{.github → .agents}/prompts/playwright-generate-test.prompt.md +0 -0
- /package/{.github → .agents}/skills/agent-governance/SKILL.md +0 -0
- /package/{.github → .agents}/skills/boost-prompt/SKILL.md +0 -0
- /package/{.github → .agents}/skills/chrome-devtools/SKILL.md +0 -0
- /package/{.github → .agents}/skills/context-map/SKILL.md +0 -0
- /package/{.github → .agents}/skills/conventional-commit/SKILL.md +0 -0
- /package/{.github → .agents}/skills/create-implementation-plan/SKILL.md +0 -0
- /package/{.github → .agents}/skills/create-readme/SKILL.md +0 -0
- /package/{.github → .agents}/skills/create-specification/SKILL.md +0 -0
- /package/{.github → .agents}/skills/editorconfig/SKILL.md +0 -0
- /package/{.github → .agents}/skills/gh-cli/SKILL.md +0 -0
- /package/{.github → .agents}/skills/git-commit/SKILL.md +0 -0
- /package/{.github → .agents}/skills/github-issues/SKILL.md +0 -0
- /package/{.github → .agents}/skills/github-issues/references/templates.md +0 -0
- /package/{.github → .agents}/skills/microsoft-docs/SKILL.md +0 -0
- /package/{.github → .agents}/skills/playwright-explore-website/SKILL.md +0 -0
- /package/{.github → .agents}/skills/playwright-generate-test/SKILL.md +0 -0
- /package/{.github → .agents}/skills/polygot-test-agent/SKILL.md +0 -0
- /package/{.github → .agents}/skills/polygot-test-agent/unit-test-genearation.prompt.md +0 -0
- /package/{.github → .agents}/skills/prompt-builder/SKILL.md +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import re
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
URL_REGEX = re.compile(r"https?://[^\s)]+")
|
|
7
|
+
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
|
|
8
|
+
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
|
|
9
|
+
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
|
|
10
|
+
|
|
11
|
+
# crude but effective path detection
|
|
12
|
+
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
|
|
13
|
+
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidationResult:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.is_valid = True
|
|
19
|
+
self.errors = []
|
|
20
|
+
self.warnings = []
|
|
21
|
+
|
|
22
|
+
def add_error(self, msg):
|
|
23
|
+
self.is_valid = False
|
|
24
|
+
self.errors.append(msg)
|
|
25
|
+
|
|
26
|
+
def add_warning(self, msg):
|
|
27
|
+
self.warnings.append(msg)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_file(path: Path) -> str:
|
|
31
|
+
return path.read_text(errors="ignore")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------- Extractors ----------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_headings(text):
|
|
38
|
+
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_code_blocks(text):
|
|
42
|
+
"""Line-based fenced code block extractor.
|
|
43
|
+
|
|
44
|
+
Handles ``` and ~~~ fences with variable length (CommonMark: closing
|
|
45
|
+
fence must use same char and be at least as long as opening). Supports
|
|
46
|
+
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
|
|
47
|
+
content).
|
|
48
|
+
"""
|
|
49
|
+
blocks = []
|
|
50
|
+
lines = text.split("\n")
|
|
51
|
+
i = 0
|
|
52
|
+
n = len(lines)
|
|
53
|
+
while i < n:
|
|
54
|
+
m = FENCE_OPEN_REGEX.match(lines[i])
|
|
55
|
+
if not m:
|
|
56
|
+
i += 1
|
|
57
|
+
continue
|
|
58
|
+
fence_char = m.group(2)[0]
|
|
59
|
+
fence_len = len(m.group(2))
|
|
60
|
+
open_line = lines[i]
|
|
61
|
+
block_lines = [open_line]
|
|
62
|
+
i += 1
|
|
63
|
+
closed = False
|
|
64
|
+
while i < n:
|
|
65
|
+
close_m = FENCE_OPEN_REGEX.match(lines[i])
|
|
66
|
+
if (
|
|
67
|
+
close_m
|
|
68
|
+
and close_m.group(2)[0] == fence_char
|
|
69
|
+
and len(close_m.group(2)) >= fence_len
|
|
70
|
+
and close_m.group(3).strip() == ""
|
|
71
|
+
):
|
|
72
|
+
block_lines.append(lines[i])
|
|
73
|
+
closed = True
|
|
74
|
+
i += 1
|
|
75
|
+
break
|
|
76
|
+
block_lines.append(lines[i])
|
|
77
|
+
i += 1
|
|
78
|
+
if closed:
|
|
79
|
+
blocks.append("\n".join(block_lines))
|
|
80
|
+
# Unclosed fences are silently skipped — they indicate malformed markdown
|
|
81
|
+
# and including them would cause false-positive validation failures.
|
|
82
|
+
return blocks
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def extract_urls(text):
|
|
86
|
+
return set(URL_REGEX.findall(text))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_paths(text):
|
|
90
|
+
return set(PATH_REGEX.findall(text))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def count_bullets(text):
|
|
94
|
+
return len(BULLET_REGEX.findall(text))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_inline_codes(text):
|
|
98
|
+
text_without_fences = re.sub(r"^```[\s\S]*?^```", "", text, flags=re.MULTILINE)
|
|
99
|
+
text_without_fences = re.sub(r"^~~~[\s\S]*?^~~~", "", text_without_fences, flags=re.MULTILINE)
|
|
100
|
+
return re.findall(r"`([^`]+)`", text_without_fences)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------- Validators ----------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_headings(orig, comp, result):
|
|
107
|
+
h1 = extract_headings(orig)
|
|
108
|
+
h2 = extract_headings(comp)
|
|
109
|
+
|
|
110
|
+
if len(h1) != len(h2):
|
|
111
|
+
result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
|
|
112
|
+
|
|
113
|
+
if h1 != h2:
|
|
114
|
+
result.add_warning("Heading text/order changed")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def validate_code_blocks(orig, comp, result):
|
|
118
|
+
c1 = extract_code_blocks(orig)
|
|
119
|
+
c2 = extract_code_blocks(comp)
|
|
120
|
+
|
|
121
|
+
if c1 != c2:
|
|
122
|
+
result.add_error("Code blocks not preserved exactly")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_urls(orig, comp, result):
|
|
126
|
+
u1 = extract_urls(orig)
|
|
127
|
+
u2 = extract_urls(comp)
|
|
128
|
+
|
|
129
|
+
if u1 != u2:
|
|
130
|
+
result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_paths(orig, comp, result):
|
|
134
|
+
p1 = extract_paths(orig)
|
|
135
|
+
p2 = extract_paths(comp)
|
|
136
|
+
|
|
137
|
+
if p1 != p2:
|
|
138
|
+
result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def validate_bullets(orig, comp, result):
|
|
142
|
+
b1 = count_bullets(orig)
|
|
143
|
+
b2 = count_bullets(comp)
|
|
144
|
+
|
|
145
|
+
if b1 == 0:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
diff = abs(b1 - b2) / b1
|
|
149
|
+
|
|
150
|
+
if diff > 0.15:
|
|
151
|
+
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def validate_inline_codes(orig, comp, result):
|
|
155
|
+
c1 = Counter(extract_inline_codes(orig))
|
|
156
|
+
c2 = Counter(extract_inline_codes(comp))
|
|
157
|
+
|
|
158
|
+
if c1 != c2:
|
|
159
|
+
lost = set(c1.keys()) - set(c2.keys())
|
|
160
|
+
added = set(c2.keys()) - set(c1.keys())
|
|
161
|
+
for code, count in c1.items():
|
|
162
|
+
if code in c2 and c2[code] < count:
|
|
163
|
+
lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)")
|
|
164
|
+
if lost:
|
|
165
|
+
result.add_error(f"Inline code lost: {lost}")
|
|
166
|
+
if added:
|
|
167
|
+
result.add_warning(f"Inline code added: {added}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------- Main ----------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
|
|
174
|
+
result = ValidationResult()
|
|
175
|
+
|
|
176
|
+
orig = read_file(original_path)
|
|
177
|
+
comp = read_file(compressed_path)
|
|
178
|
+
|
|
179
|
+
validate_headings(orig, comp, result)
|
|
180
|
+
validate_code_blocks(orig, comp, result)
|
|
181
|
+
validate_urls(orig, comp, result)
|
|
182
|
+
validate_paths(orig, comp, result)
|
|
183
|
+
validate_bullets(orig, comp, result)
|
|
184
|
+
validate_inline_codes(orig, comp, result)
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------- CLI ----------
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
import sys
|
|
193
|
+
|
|
194
|
+
if len(sys.argv) != 3:
|
|
195
|
+
print("Usage: python validate.py <original> <compressed>")
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
|
|
198
|
+
orig = Path(sys.argv[1]).resolve()
|
|
199
|
+
comp = Path(sys.argv[2]).resolve()
|
|
200
|
+
|
|
201
|
+
res = validate(orig, comp)
|
|
202
|
+
|
|
203
|
+
print(f"\nValid: {res.is_valid}")
|
|
204
|
+
|
|
205
|
+
if res.errors:
|
|
206
|
+
print("\nErrors:")
|
|
207
|
+
for e in res.errors:
|
|
208
|
+
print(f" - {e}")
|
|
209
|
+
|
|
210
|
+
if res.warnings:
|
|
211
|
+
print("\nWarnings:")
|
|
212
|
+
for w in res.warnings:
|
|
213
|
+
print(f" - {w}")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman-help
|
|
3
|
+
description: >
|
|
4
|
+
Quick-reference card for all caveman modes, skills, and commands.
|
|
5
|
+
One-shot display, not a persistent mode. Trigger: /caveman-help,
|
|
6
|
+
"caveman help", "what caveman commands", "how do I use caveman".
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Caveman Help
|
|
10
|
+
|
|
11
|
+
Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.
|
|
12
|
+
|
|
13
|
+
## Modes
|
|
14
|
+
|
|
15
|
+
| Mode | Trigger | What change |
|
|
16
|
+
|------|---------|-------------|
|
|
17
|
+
| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. |
|
|
18
|
+
| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. |
|
|
19
|
+
| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. |
|
|
20
|
+
| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. |
|
|
21
|
+
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. |
|
|
22
|
+
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. |
|
|
23
|
+
|
|
24
|
+
Mode stick until changed or session end.
|
|
25
|
+
|
|
26
|
+
## Skills
|
|
27
|
+
|
|
28
|
+
| Skill | Trigger | What it do |
|
|
29
|
+
|-------|---------|-----------|
|
|
30
|
+
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
|
|
31
|
+
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
|
|
32
|
+
| **caveman-compress** | `/caveman:compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
|
|
33
|
+
| **caveman-help** | `/caveman-help` | This card. |
|
|
34
|
+
|
|
35
|
+
## Deactivate
|
|
36
|
+
|
|
37
|
+
Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
|
|
38
|
+
|
|
39
|
+
## Configure Default Mode
|
|
40
|
+
|
|
41
|
+
Default mode = `full`. Change it:
|
|
42
|
+
|
|
43
|
+
**Environment variable** (highest priority):
|
|
44
|
+
```bash
|
|
45
|
+
export CAVEMAN_DEFAULT_MODE=ultra
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Config file** (`~/.config/caveman/config.json`):
|
|
49
|
+
```json
|
|
50
|
+
{ "defaultMode": "lite" }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`.
|
|
54
|
+
|
|
55
|
+
Resolution: env var > config file > `full`.
|
|
56
|
+
|
|
57
|
+
## More
|
|
58
|
+
|
|
59
|
+
Full docs: https://github.com/JuliusBrussee/caveman
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman-review
|
|
3
|
+
description: >
|
|
4
|
+
Ultra-compressed code review comments. Cuts noise from PR feedback while preserving
|
|
5
|
+
the actionable signal. Each comment is one line: location, problem, fix. Use when user
|
|
6
|
+
says "review this PR", "code review", "review the diff", "/review", or invokes
|
|
7
|
+
/caveman-review. Auto-triggers when reviewing pull requests.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing.
|
|
11
|
+
|
|
12
|
+
## Rules
|
|
13
|
+
|
|
14
|
+
**Format:** `L<line>: <problem>. <fix>.` — or `<file>:L<line>: ...` when reviewing multi-file diffs.
|
|
15
|
+
|
|
16
|
+
**Severity prefix (optional, when mixed):**
|
|
17
|
+
- `🔴 bug:` — broken behavior, will cause incident
|
|
18
|
+
- `🟡 risk:` — works but fragile (race, missing null check, swallowed error)
|
|
19
|
+
- `🔵 nit:` — style, naming, micro-optim. Author can ignore
|
|
20
|
+
- `❓ q:` — genuine question, not a suggestion
|
|
21
|
+
|
|
22
|
+
**Drop:**
|
|
23
|
+
- "I noticed that...", "It seems like...", "You might want to consider..."
|
|
24
|
+
- "This is just a suggestion but..." — use `nit:` instead
|
|
25
|
+
- "Great work!", "Looks good overall but..." — say it once at the top, not per comment
|
|
26
|
+
- Restating what the line does — the reviewer can read the diff
|
|
27
|
+
- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:`
|
|
28
|
+
|
|
29
|
+
**Keep:**
|
|
30
|
+
- Exact line numbers
|
|
31
|
+
- Exact symbol/function/variable names in backticks
|
|
32
|
+
- Concrete fix, not "consider refactoring this"
|
|
33
|
+
- The *why* if the fix isn't obvious from the problem statement
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here."
|
|
38
|
+
|
|
39
|
+
✅ `L42: 🔴 bug: user can be null after .find(). Add guard before .email.`
|
|
40
|
+
|
|
41
|
+
❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability."
|
|
42
|
+
|
|
43
|
+
✅ `L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.`
|
|
44
|
+
|
|
45
|
+
❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case."
|
|
46
|
+
|
|
47
|
+
✅ `L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).`
|
|
48
|
+
|
|
49
|
+
## Auto-Clarity
|
|
50
|
+
|
|
51
|
+
Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest.
|
|
52
|
+
|
|
53
|
+
## Boundaries
|
|
54
|
+
|
|
55
|
+
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.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman-stats
|
|
3
|
+
description: >
|
|
4
|
+
Show real token usage and estimated savings for the current session.
|
|
5
|
+
Reads directly from the Claude Code session log — no AI estimation.
|
|
6
|
+
Triggers on /caveman-stats. Output is injected by the mode-tracker hook;
|
|
7
|
+
the model itself does not compute the numbers.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
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.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compress
|
|
3
|
+
description: >
|
|
4
|
+
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
|
|
5
|
+
to save input tokens. Preserves all technical substance, code, URLs, and structure.
|
|
6
|
+
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
|
|
7
|
+
Trigger: /caveman:compress FILEPATH or "compress memory file"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Caveman Compress
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
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`.
|
|
15
|
+
|
|
16
|
+
## Trigger
|
|
17
|
+
|
|
18
|
+
`/caveman:compress <filepath>` or when user asks to compress a memory file.
|
|
19
|
+
|
|
20
|
+
## Process
|
|
21
|
+
|
|
22
|
+
1. This SKILL.md lives alongside `scripts/` in the same directory. Find that directory.
|
|
23
|
+
|
|
24
|
+
2. Run:
|
|
25
|
+
|
|
26
|
+
cd <directory_containing_this_SKILL.md> && python3 -m scripts <absolute_filepath>
|
|
27
|
+
|
|
28
|
+
3. The CLI will:
|
|
29
|
+
- detect file type (no tokens)
|
|
30
|
+
- call Claude to compress
|
|
31
|
+
- validate output (no tokens)
|
|
32
|
+
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
|
|
33
|
+
- retry up to 2 times
|
|
34
|
+
- if still failing after 2 retries: report error to user, leave original file untouched
|
|
35
|
+
|
|
36
|
+
4. Return result to user
|
|
37
|
+
|
|
38
|
+
## Compression Rules
|
|
39
|
+
|
|
40
|
+
### Remove
|
|
41
|
+
- Articles: a, an, the
|
|
42
|
+
- Filler: just, really, basically, actually, simply, essentially, generally
|
|
43
|
+
- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend"
|
|
44
|
+
- Hedging: "it might be worth", "you could consider", "it would be good to"
|
|
45
|
+
- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because"
|
|
46
|
+
- Connective fluff: "however", "furthermore", "additionally", "in addition"
|
|
47
|
+
|
|
48
|
+
### Preserve EXACTLY (never modify)
|
|
49
|
+
- Code blocks (fenced ``` and indented)
|
|
50
|
+
- Inline code (`backtick content`)
|
|
51
|
+
- URLs and links (full URLs, markdown links)
|
|
52
|
+
- File paths (`/src/components/...`, `./config.yaml`)
|
|
53
|
+
- Commands (`npm install`, `git commit`, `docker build`)
|
|
54
|
+
- Technical terms (library names, API names, protocols, algorithms)
|
|
55
|
+
- Proper nouns (project names, people, companies)
|
|
56
|
+
- Dates, version numbers, numeric values
|
|
57
|
+
- Environment variables (`$HOME`, `NODE_ENV`)
|
|
58
|
+
|
|
59
|
+
### Preserve Structure
|
|
60
|
+
- All markdown headings (keep exact heading text, compress body below)
|
|
61
|
+
- Bullet point hierarchy (keep nesting level)
|
|
62
|
+
- Numbered lists (keep numbering)
|
|
63
|
+
- Tables (compress cell text, keep structure)
|
|
64
|
+
- Frontmatter/YAML headers in markdown files
|
|
65
|
+
|
|
66
|
+
### Compress
|
|
67
|
+
- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"
|
|
68
|
+
- Fragments OK: "Run tests before commit" not "You should always run tests before committing"
|
|
69
|
+
- Drop "you should", "make sure to", "remember to" — just state the action
|
|
70
|
+
- Merge redundant bullets that say the same thing differently
|
|
71
|
+
- Keep one example where multiple examples show the same pattern
|
|
72
|
+
|
|
73
|
+
CRITICAL RULE:
|
|
74
|
+
Anything inside ``` ... ``` must be copied EXACTLY.
|
|
75
|
+
Do not:
|
|
76
|
+
- remove comments
|
|
77
|
+
- remove spacing
|
|
78
|
+
- reorder lines
|
|
79
|
+
- shorten commands
|
|
80
|
+
- simplify anything
|
|
81
|
+
|
|
82
|
+
Inline code (`...`) must be preserved EXACTLY.
|
|
83
|
+
Do not modify anything inside backticks.
|
|
84
|
+
|
|
85
|
+
If file contains code blocks:
|
|
86
|
+
- Treat code blocks as read-only regions
|
|
87
|
+
- Only compress text outside them
|
|
88
|
+
- Do not merge sections around code
|
|
89
|
+
|
|
90
|
+
## Pattern
|
|
91
|
+
|
|
92
|
+
Original:
|
|
93
|
+
> 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.
|
|
94
|
+
|
|
95
|
+
Compressed:
|
|
96
|
+
> Run tests before push to main. Catch bugs early, prevent broken prod deploys.
|
|
97
|
+
|
|
98
|
+
Original:
|
|
99
|
+
> 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.
|
|
100
|
+
|
|
101
|
+
Compressed:
|
|
102
|
+
> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.
|
|
103
|
+
|
|
104
|
+
## Boundaries
|
|
105
|
+
|
|
106
|
+
- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)
|
|
107
|
+
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
|
|
108
|
+
- If file has mixed content (prose + code), compress ONLY the prose sections
|
|
109
|
+
- If unsure whether something is code or prose, leave it unchanged
|
|
110
|
+
- Original file is backed up as FILE.original.md before overwriting
|
|
111
|
+
- Never compress FILE.original.md (skip it)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Support both direct execution and module import
|
|
6
|
+
try:
|
|
7
|
+
from .validate import validate
|
|
8
|
+
except ImportError:
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
10
|
+
from validate import validate
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import tiktoken
|
|
14
|
+
_enc = tiktoken.get_encoding("o200k_base")
|
|
15
|
+
except ImportError:
|
|
16
|
+
_enc = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def count_tokens(text):
|
|
20
|
+
if _enc is None:
|
|
21
|
+
return len(text.split()) # fallback: word count
|
|
22
|
+
return len(_enc.encode(text))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def benchmark_pair(orig_path: Path, comp_path: Path):
|
|
26
|
+
orig_text = orig_path.read_text()
|
|
27
|
+
comp_text = comp_path.read_text()
|
|
28
|
+
|
|
29
|
+
orig_tokens = count_tokens(orig_text)
|
|
30
|
+
comp_tokens = count_tokens(comp_text)
|
|
31
|
+
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
|
|
32
|
+
result = validate(orig_path, comp_path)
|
|
33
|
+
|
|
34
|
+
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_table(rows):
|
|
38
|
+
print("\n| File | Original | Compressed | Saved % | Valid |")
|
|
39
|
+
print("|------|----------|------------|---------|-------|")
|
|
40
|
+
for r in rows:
|
|
41
|
+
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'✅' if r[4] else '❌'} |")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
# Direct file pair: python3 benchmark.py original.md compressed.md
|
|
46
|
+
if len(sys.argv) == 3:
|
|
47
|
+
orig = Path(sys.argv[1]).resolve()
|
|
48
|
+
comp = Path(sys.argv[2]).resolve()
|
|
49
|
+
if not orig.exists():
|
|
50
|
+
print(f"❌ Not found: {orig}")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
if not comp.exists():
|
|
53
|
+
print(f"❌ Not found: {comp}")
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
print_table([benchmark_pair(orig, comp)])
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Glob mode: repo_root/tests/caveman-compress/
|
|
59
|
+
tests_dir = Path(__file__).parent.parent.parent / "tests" / "caveman-compress"
|
|
60
|
+
if not tests_dir.exists():
|
|
61
|
+
print(f"❌ Tests dir not found: {tests_dir}")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
rows = []
|
|
65
|
+
for orig in sorted(tests_dir.glob("*.original.md")):
|
|
66
|
+
comp = orig.with_name(orig.stem.removesuffix(".original") + ".md")
|
|
67
|
+
if comp.exists():
|
|
68
|
+
rows.append(benchmark_pair(orig, comp))
|
|
69
|
+
|
|
70
|
+
if not rows:
|
|
71
|
+
print("No compressed file pairs found.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
print_table(rows)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Caveman Compress CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
caveman <filepath>
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# Force UTF-8 on stdout/stderr before any code can print. Windows consoles
|
|
12
|
+
# default to cp1252 and crash on the ❌ glyphs in error/validation branches,
|
|
13
|
+
# masking the real error and leaving the user with a half-compressed file.
|
|
14
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
15
|
+
reconfigure = getattr(_stream, "reconfigure", None)
|
|
16
|
+
if callable(reconfigure):
|
|
17
|
+
try:
|
|
18
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from .compress import compress_file
|
|
25
|
+
from .detect import detect_file_type, should_compress
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_usage():
|
|
29
|
+
print("Usage: caveman <filepath>")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
if len(sys.argv) != 2:
|
|
34
|
+
print_usage()
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
filepath = Path(sys.argv[1])
|
|
38
|
+
|
|
39
|
+
# Check file exists
|
|
40
|
+
if not filepath.exists():
|
|
41
|
+
print(f"❌ File not found: {filepath}")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
if not filepath.is_file():
|
|
45
|
+
print(f"❌ Not a file: {filepath}")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
filepath = filepath.resolve()
|
|
49
|
+
|
|
50
|
+
# Detect file type
|
|
51
|
+
file_type = detect_file_type(filepath)
|
|
52
|
+
|
|
53
|
+
print(f"Detected: {file_type}")
|
|
54
|
+
|
|
55
|
+
# Check if compressible
|
|
56
|
+
if not should_compress(filepath):
|
|
57
|
+
print("Skipping: file is not natural language (code/config)")
|
|
58
|
+
sys.exit(0)
|
|
59
|
+
|
|
60
|
+
print("Starting caveman compression...\n")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
success = compress_file(filepath)
|
|
64
|
+
|
|
65
|
+
if success:
|
|
66
|
+
print("\nCompression completed successfully")
|
|
67
|
+
backup_path = filepath.with_name(filepath.stem + ".original.md")
|
|
68
|
+
print(f"Compressed: {filepath}")
|
|
69
|
+
print(f"Original: {backup_path}")
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
else:
|
|
72
|
+
print("\n❌ Compression failed after retries")
|
|
73
|
+
sys.exit(2)
|
|
74
|
+
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
print("\nInterrupted by user")
|
|
77
|
+
sys.exit(130)
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"\n❌ Error: {e}")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|