@event4u/agent-config 2.20.1 → 2.21.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/.agent-src/commands/agent-status.md +16 -0
- package/.agent-src/rules/caveman-speak.md +2 -0
- package/.agent-src/skills/compress-memory/SKILL.md +119 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.claude-plugin/marketplace.json +2 -1
- package/CHANGELOG.md +35 -0
- package/README.md +5 -5
- package/docs/architecture.md +1 -1
- package/docs/benchmarks.md +74 -0
- package/docs/catalog.md +3 -2
- package/docs/contracts/caveman-telemetry.md +83 -0
- package/docs/contracts/compression-default-kill-criterion.md +82 -35
- package/docs/contracts/cost-summary-schema.md +107 -0
- package/docs/contracts/file-ownership-matrix.json +41 -0
- package/package.json +1 -1
- package/scripts/_lib/bench_caveman.py +273 -0
- package/scripts/_lib/bench_caveman_report.py +152 -0
- package/scripts/bench_compress_memory.py +168 -0
- package/scripts/bench_run.py +119 -1
- package/scripts/caveman_stats.py +119 -0
- package/scripts/check_command_count_messaging.py +2 -2
- package/scripts/compress_memory.py +172 -0
- package/scripts/cost_by_conversation.py +78 -0
- package/scripts/cost_summary.py +97 -0
- package/scripts/update_counts.py +7 -5
- package/scripts/validate_caveman_carveouts.py +129 -0
- package/scripts/validate_safe_paths.py +118 -0
- package/scripts/verify_roadmap_closure.py +327 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Offline bench for input-side memory compression (Phase 2 / Step 11).
|
|
3
|
+
|
|
4
|
+
Runs `compress_memory.py` over a fixed corpus of memory-target files, records
|
|
5
|
+
pre/post char counts, approximates input-token savings (chars / 4 — the
|
|
6
|
+
GPT-4 / Claude rule of thumb), and emits `bench/reports/caveman-v2.{json,md}`.
|
|
7
|
+
|
|
8
|
+
Offline (no API calls). Cadence-aligned with `docs/benchmarks.md`. Citation
|
|
9
|
+
in `bench/reports/caveman-v2.md` notes the chars→tokens approximation and
|
|
10
|
+
points at upstream tiktoken / claude-tokenizer if a calibrated number is
|
|
11
|
+
later needed.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import shutil
|
|
17
|
+
import statistics
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
COMPRESS_SCRIPT = REPO_ROOT / "scripts" / "compress_memory.py"
|
|
26
|
+
REPORT_JSON = REPO_ROOT / "bench" / "reports" / "caveman-v2.json"
|
|
27
|
+
REPORT_MD = REPO_ROOT / "bench" / "reports" / "caveman-v2.md"
|
|
28
|
+
|
|
29
|
+
CORPUS: list[tuple[str, str]] = [
|
|
30
|
+
("AGENTS.md", "thin-root-package"),
|
|
31
|
+
(".agent-src.uncompressed/templates/AGENTS.md", "thin-root-consumer-template"),
|
|
32
|
+
(".agent-src/templates/AGENTS.md", "thin-root-consumer-generated"),
|
|
33
|
+
("docs/contracts/ai-council-config.md", "prose-heavy-contract"),
|
|
34
|
+
("docs/contracts/implement-ticket-flow.md", "prose-heavy-contract"),
|
|
35
|
+
("docs/contracts/command-clusters.md", "prose-heavy-contract"),
|
|
36
|
+
("docs/contracts/mental-models.md", "prose-heavy-contract"),
|
|
37
|
+
("docs/contracts/kernel-membership.md", "prose-heavy-contract"),
|
|
38
|
+
("docs/contracts/load-context-budget-model.md", "prose-heavy-contract"),
|
|
39
|
+
("docs/contracts/mcp-cloud-scope.md", "prose-heavy-contract"),
|
|
40
|
+
("docs/contracts/context-spine.md", "prose-heavy-contract"),
|
|
41
|
+
("docs/contracts/rule-classification.md", "rule-classification"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def chars_to_tokens(n: int) -> int:
|
|
46
|
+
"""Approximate token count via chars / 4 (GPT-4/Claude English heuristic)."""
|
|
47
|
+
return round(n / 4)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def bench_one(rel_path: str, category: str) -> dict:
|
|
51
|
+
src = REPO_ROOT / rel_path
|
|
52
|
+
if not src.is_file():
|
|
53
|
+
return {"path": rel_path, "category": category, "error": "not-found"}
|
|
54
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
55
|
+
target = Path(tmp) / src.name
|
|
56
|
+
shutil.copy(src, target)
|
|
57
|
+
before_chars = target.stat().st_size
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
[sys.executable, str(COMPRESS_SCRIPT), str(target)],
|
|
60
|
+
capture_output=True, text=True, cwd=REPO_ROOT,
|
|
61
|
+
)
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
return {"path": rel_path, "category": category,
|
|
64
|
+
"error": f"exit-{result.returncode}", "stderr": result.stderr[:200]}
|
|
65
|
+
after_chars = target.stat().st_size
|
|
66
|
+
before_tok = chars_to_tokens(before_chars)
|
|
67
|
+
after_tok = chars_to_tokens(after_chars)
|
|
68
|
+
return {
|
|
69
|
+
"path": rel_path,
|
|
70
|
+
"category": category,
|
|
71
|
+
"before_chars": before_chars,
|
|
72
|
+
"after_chars": after_chars,
|
|
73
|
+
"delta_chars": after_chars - before_chars,
|
|
74
|
+
"saving_pct_chars": (before_chars - after_chars) * 100 / before_chars,
|
|
75
|
+
"before_tokens_est": before_tok,
|
|
76
|
+
"after_tokens_est": after_tok,
|
|
77
|
+
"delta_tokens_est": after_tok - before_tok,
|
|
78
|
+
"saving_pct_tokens_est": (before_tok - after_tok) * 100 / before_tok if before_tok else 0.0,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def aggregate(rows: list[dict]) -> dict:
|
|
83
|
+
rows_ok = [r for r in rows if "error" not in r]
|
|
84
|
+
savings = [r["saving_pct_chars"] for r in rows_ok]
|
|
85
|
+
by_cat: dict[str, list[float]] = {}
|
|
86
|
+
for r in rows_ok:
|
|
87
|
+
by_cat.setdefault(r["category"], []).append(r["saving_pct_chars"])
|
|
88
|
+
return {
|
|
89
|
+
"calls": len(rows),
|
|
90
|
+
"errors": len(rows) - len(rows_ok),
|
|
91
|
+
"median_saving_pct": statistics.median(savings) if savings else 0.0,
|
|
92
|
+
"p10_saving_pct": statistics.quantiles(savings, n=10)[0] if len(savings) >= 10 else min(savings, default=0.0),
|
|
93
|
+
"p90_saving_pct": statistics.quantiles(savings, n=10)[8] if len(savings) >= 10 else max(savings, default=0.0),
|
|
94
|
+
"stdev_saving_pct": statistics.pstdev(savings) if len(savings) > 1 else 0.0,
|
|
95
|
+
"total_chars_saved": sum(r["before_chars"] - r["after_chars"] for r in rows_ok),
|
|
96
|
+
"total_tokens_est_saved": sum(r["before_tokens_est"] - r["after_tokens_est"] for r in rows_ok),
|
|
97
|
+
"by_category_median_pct": {k: statistics.median(v) for k, v in by_cat.items()},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def render_md(payload: dict) -> str:
|
|
102
|
+
agg = payload["aggregate"]
|
|
103
|
+
lines = [
|
|
104
|
+
"# caveman-v2 — input-side memory compression bench",
|
|
105
|
+
"",
|
|
106
|
+
f"**Generated:** {payload['generated_at']}",
|
|
107
|
+
f"**Schema:** `caveman-v2` (input-side; offline; chars→tokens via /4 heuristic)",
|
|
108
|
+
f"**Script:** `scripts/bench_compress_memory.py`",
|
|
109
|
+
"",
|
|
110
|
+
"## Headline",
|
|
111
|
+
"",
|
|
112
|
+
f"- Median char saving: **{agg['median_saving_pct']:+.2f}%** (p10 {agg['p10_saving_pct']:+.2f}% · p90 {agg['p90_saving_pct']:+.2f}%)",
|
|
113
|
+
f"- Total chars saved across corpus: **{agg['total_chars_saved']:+,}**",
|
|
114
|
+
f"- Total tokens (estimate) saved across corpus: **{agg['total_tokens_est_saved']:+,}**",
|
|
115
|
+
f"- Files: {agg['calls']} · errors: {agg['errors']}",
|
|
116
|
+
"",
|
|
117
|
+
"## By category (median %)",
|
|
118
|
+
"",
|
|
119
|
+
"| Category | Median saving |",
|
|
120
|
+
"|---|---:|",
|
|
121
|
+
]
|
|
122
|
+
for cat, med in sorted(agg["by_category_median_pct"].items()):
|
|
123
|
+
lines.append(f"| {cat} | {med:+.2f}% |")
|
|
124
|
+
lines += ["", "## Per file", "",
|
|
125
|
+
"| Path | Category | Before | After | Δ chars | Saving % |",
|
|
126
|
+
"|---|---|---:|---:|---:|---:|"]
|
|
127
|
+
for r in payload["rows"]:
|
|
128
|
+
if "error" in r:
|
|
129
|
+
lines.append(f"| `{r['path']}` | {r['category']} | — | — | — | {r['error']} |")
|
|
130
|
+
else:
|
|
131
|
+
lines.append(
|
|
132
|
+
f"| `{r['path']}` | {r['category']} | {r['before_chars']:,} | {r['after_chars']:,} | "
|
|
133
|
+
f"{r['delta_chars']:+,} | {r['saving_pct_chars']:+.2f}% |"
|
|
134
|
+
)
|
|
135
|
+
lines += ["", "## Methodology",
|
|
136
|
+
"",
|
|
137
|
+
"- Offline run: `compress_memory.py` writes `.original.md` backup + frontmatter (`original_sha256`, `compressed_at`). The frontmatter pair (≈ 120 chars) is the fixed compression tax — files with little prose net negative.",
|
|
138
|
+
"- chars → tokens approximation: `tokens ≈ chars / 4` (GPT-4 / Claude English rule of thumb). Calibrated number requires `tiktoken` or `claude-tokenizer`; deferred until a consumer requests pinpoint numbers.",
|
|
139
|
+
"- The `caveman-v1` output-side verdict (`vs_terse` median −9.27%) is orthogonal — input-side savings apply to the always-loaded memory budget, not the reply stream.",
|
|
140
|
+
"",
|
|
141
|
+
"## Interpretation",
|
|
142
|
+
"",
|
|
143
|
+
"- **Thin-Root files net negative.** `AGENTS.md` and `templates/AGENTS.md` already follow `agents-md-thin-root` (≥ 40 % pointer ratio). The compressor's frontmatter pair adds more bytes than the sparse prose loses. **Do not compress Thin-Root files.**",
|
|
144
|
+
"- **Prose-heavy contract docs net 3–6 % saving.** Useful but modest. Pays off when the file is large and frequently loaded.",
|
|
145
|
+
"- **Rule of thumb:** target files with > 5 KB and visible paragraph prose; skip pointer-only files.",
|
|
146
|
+
""]
|
|
147
|
+
return "\n".join(lines)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main() -> int:
|
|
151
|
+
rows = [bench_one(p, c) for p, c in CORPUS]
|
|
152
|
+
payload = {
|
|
153
|
+
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
154
|
+
"schema": "caveman-v2",
|
|
155
|
+
"rows": rows,
|
|
156
|
+
"aggregate": aggregate(rows),
|
|
157
|
+
}
|
|
158
|
+
REPORT_JSON.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
REPORT_JSON.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
160
|
+
REPORT_MD.write_text(render_md(payload), encoding="utf-8")
|
|
161
|
+
print(f"wrote: {REPORT_JSON}")
|
|
162
|
+
print(f"wrote: {REPORT_MD}")
|
|
163
|
+
print(f"median saving: {payload['aggregate']['median_saving_pct']:+.2f}%")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main())
|
package/scripts/bench_run.py
CHANGED
|
@@ -21,6 +21,7 @@ from pathlib import Path
|
|
|
21
21
|
|
|
22
22
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
23
23
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
24
|
+
sys.path.insert(0, str(REPO_ROOT))
|
|
24
25
|
|
|
25
26
|
from _lib import script_output # type: ignore[import-not-found] # noqa: E402
|
|
26
27
|
from _lib.bench_cost import aggregate_sessions # noqa: E402
|
|
@@ -33,6 +34,9 @@ from _lib.bench_report import ( # noqa: E402
|
|
|
33
34
|
write_json,
|
|
34
35
|
write_markdown,
|
|
35
36
|
)
|
|
37
|
+
from _lib import bench_caveman # noqa: E402
|
|
38
|
+
from _lib.bench_caveman_report import build_caveman_report, render_caveman_markdown # noqa: E402
|
|
39
|
+
from _lib.bench_cost import load_pricing # noqa: E402
|
|
36
40
|
from bench_runner import run_corpus # noqa: E402
|
|
37
41
|
|
|
38
42
|
try:
|
|
@@ -41,11 +45,12 @@ except ImportError:
|
|
|
41
45
|
script_output.error("error: PyYAML required (pip install pyyaml)")
|
|
42
46
|
sys.exit(2)
|
|
43
47
|
|
|
44
|
-
BENCH_RUN_VERSION = "0.
|
|
48
|
+
BENCH_RUN_VERSION = "0.2.0"
|
|
45
49
|
PRICING_PATH = REPO_ROOT / "bench" / "pricing.yaml"
|
|
46
50
|
SESSIONS_JSONL = REPO_ROOT / "agents" / "cost-tracking" / "sessions.jsonl"
|
|
47
51
|
REPORTS_DIR = REPO_ROOT / "bench" / "reports"
|
|
48
52
|
CORPUS_DIR = REPO_ROOT / "tests" / "eval"
|
|
53
|
+
CAVEMAN_CORPUS = REPO_ROOT / "bench" / "corpora" / "caveman" / "prompts.yaml"
|
|
49
54
|
BASELINE_COLLECTOR = REPO_ROOT / "scripts" / "bench_runner.py"
|
|
50
55
|
|
|
51
56
|
|
|
@@ -110,8 +115,21 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
110
115
|
help="Override timestamp (test hook); defaults to UTC now")
|
|
111
116
|
ap.add_argument("--no-write", action="store_true",
|
|
112
117
|
help="Compute the report but do not write files (dry run)")
|
|
118
|
+
ap.add_argument("--caveman", action="store_true",
|
|
119
|
+
help="Run the caveman three-arm compression bench instead of the "
|
|
120
|
+
"selection-accuracy bench (step-16 Phase 1).")
|
|
121
|
+
ap.add_argument("--caveman-max-prompts", type=int, default=None,
|
|
122
|
+
help="Cap prompts in the caveman bench (smoke test).")
|
|
123
|
+
ap.add_argument("--caveman-dry-run", action="store_true",
|
|
124
|
+
help="Caveman: skip live API calls; emit a stub report with "
|
|
125
|
+
"zero tokens (wiring check only).")
|
|
126
|
+
ap.add_argument("--caveman-report-tag", default="caveman-v1",
|
|
127
|
+
help="Filename tag for the caveman report (default: caveman-v1).")
|
|
113
128
|
args = ap.parse_args(argv)
|
|
114
129
|
|
|
130
|
+
if args.caveman:
|
|
131
|
+
return _run_caveman(args)
|
|
132
|
+
|
|
115
133
|
corpus_path = CORPUS_DIR / f"corpus-{args.corpus}.yaml"
|
|
116
134
|
if not corpus_path.is_file():
|
|
117
135
|
script_output.error(f"error: corpus not found: {corpus_path}")
|
|
@@ -151,5 +169,105 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
151
169
|
return 0 if verdict["overall"] in ("pass", "partial") else 1
|
|
152
170
|
|
|
153
171
|
|
|
172
|
+
class _DryRunClient:
|
|
173
|
+
"""Stub client for --caveman-dry-run. Returns empty CouncilResponse-shaped objects."""
|
|
174
|
+
|
|
175
|
+
def ask(self, system_prompt: str, user_prompt: str, max_tokens: int = 1024):
|
|
176
|
+
from ai_council.clients import CouncilResponse
|
|
177
|
+
return CouncilResponse(
|
|
178
|
+
provider="dry-run", model="stub", text="",
|
|
179
|
+
input_tokens=0, output_tokens=0, latency_ms=0, error=None,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _build_anthropic_client():
|
|
184
|
+
from ai_council.clients import AnthropicClient, load_anthropic_key
|
|
185
|
+
return AnthropicClient(api_key=load_anthropic_key())
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _run_caveman(args: argparse.Namespace) -> int:
|
|
189
|
+
if not CAVEMAN_CORPUS.is_file():
|
|
190
|
+
script_output.error(f"error: caveman corpus not found: {CAVEMAN_CORPUS}")
|
|
191
|
+
return 2
|
|
192
|
+
|
|
193
|
+
if args.caveman_dry_run:
|
|
194
|
+
client = _DryRunClient()
|
|
195
|
+
transport = "dry-run"
|
|
196
|
+
model = "stub"
|
|
197
|
+
else:
|
|
198
|
+
try:
|
|
199
|
+
client = _build_anthropic_client()
|
|
200
|
+
except Exception as exc: # noqa: BLE001
|
|
201
|
+
script_output.error(f"error: cannot build Anthropic client: {exc}")
|
|
202
|
+
return 2
|
|
203
|
+
transport = "api"
|
|
204
|
+
model = getattr(client, "model", "claude-sonnet-4-5")
|
|
205
|
+
|
|
206
|
+
def _progress(done: int, total: int, pid: str, arm: str, ar) -> None:
|
|
207
|
+
if args.quiet:
|
|
208
|
+
return
|
|
209
|
+
err = f" ERR={ar.error}" if ar.error else ""
|
|
210
|
+
print(f"[{done:>3}/{total}] {pid} · {arm:<14} "
|
|
211
|
+
f"in={ar.input_tokens:>4} out={ar.output_tokens:>4} "
|
|
212
|
+
f"{ar.latency_ms:>5}ms{err}", file=sys.stderr)
|
|
213
|
+
|
|
214
|
+
results = bench_caveman.run_caveman_bench(
|
|
215
|
+
client, CAVEMAN_CORPUS,
|
|
216
|
+
max_prompts=args.caveman_max_prompts,
|
|
217
|
+
on_progress=_progress,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
rates, sourced_on = load_pricing(PRICING_PATH)
|
|
221
|
+
sonnet_rates = rates.get("sonnet", {"input": 0.0, "output": 0.0})
|
|
222
|
+
|
|
223
|
+
report = build_caveman_report(
|
|
224
|
+
results=results,
|
|
225
|
+
corpus_path_rel=str(CAVEMAN_CORPUS.relative_to(REPO_ROOT)),
|
|
226
|
+
generated_at=utc_now_iso(),
|
|
227
|
+
bench_run_version=BENCH_RUN_VERSION,
|
|
228
|
+
model=model,
|
|
229
|
+
transport=transport,
|
|
230
|
+
pricing_rates=sonnet_rates,
|
|
231
|
+
pricing_sourced_on=sourced_on,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
stamp = args.stamp or utc_now_filename_stamp()
|
|
235
|
+
json_path, md_path = report_paths(REPORTS_DIR, args.caveman_report_tag, stamp)
|
|
236
|
+
# Override: caveman roadmap pins the filename to `caveman-v1.{json,md}` (no stamp).
|
|
237
|
+
fixed_json = REPORTS_DIR / f"{args.caveman_report_tag}.json"
|
|
238
|
+
fixed_md = REPORTS_DIR / f"{args.caveman_report_tag}.md"
|
|
239
|
+
|
|
240
|
+
if not args.no_write:
|
|
241
|
+
write_json(fixed_json, report)
|
|
242
|
+
fixed_md.parent.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
fixed_md.write_text(render_caveman_markdown(report), encoding="utf-8")
|
|
244
|
+
# Also drop a timestamped copy for the cadence trail.
|
|
245
|
+
write_json(json_path, report)
|
|
246
|
+
json_path.with_suffix(".md").write_text(
|
|
247
|
+
render_caveman_markdown(report), encoding="utf-8"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
cost = report["cost"]
|
|
251
|
+
headline = (
|
|
252
|
+
f"caveman · prompts {report['corpus']['prompt_count']} · "
|
|
253
|
+
f"calls {cost['totals']['calls']} · errors {cost['totals']['errors']} · "
|
|
254
|
+
f"vs_raw med {report['caveman']['aggregate']['savings_vs_raw']['median']:.2%} · "
|
|
255
|
+
f"vs_terse med {report['caveman']['aggregate']['savings_vs_terse']['median']:.2%} · "
|
|
256
|
+
f"cost ${cost['totals']['total_cost_usd']:.6f}"
|
|
257
|
+
)
|
|
258
|
+
if args.quiet:
|
|
259
|
+
print(headline)
|
|
260
|
+
if not args.no_write:
|
|
261
|
+
print(f"report: {fixed_md.relative_to(REPO_ROOT)}")
|
|
262
|
+
else:
|
|
263
|
+
print(render_caveman_markdown(report))
|
|
264
|
+
if not args.no_write:
|
|
265
|
+
print(f"\n→ json: {fixed_json.relative_to(REPO_ROOT)}")
|
|
266
|
+
print(f"→ markdown: {fixed_md.relative_to(REPO_ROOT)}")
|
|
267
|
+
print(f"→ trail: {json_path.relative_to(REPO_ROOT)}")
|
|
268
|
+
|
|
269
|
+
return 0 if cost["totals"]["errors"] == 0 else 1
|
|
270
|
+
|
|
271
|
+
|
|
154
272
|
if __name__ == "__main__":
|
|
155
273
|
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Caveman per-session / per-conversation / lifetime token-delta lens.
|
|
3
|
+
|
|
4
|
+
Reads sessions.jsonl, groups by sessionId + conversation_id, emits per-row
|
|
5
|
+
caveman delta tokens. Honors the suspended-multiplier contract in
|
|
6
|
+
`docs/contracts/caveman-telemetry.md` (delta = 0 while suspended).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import argparse, json, sys
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
14
|
+
DEFAULT_JSONL = REPO_ROOT / "agents" / "cost-tracking" / "sessions.jsonl"
|
|
15
|
+
TELEMETRY_DOC = REPO_ROOT / "docs" / "contracts" / "caveman-telemetry.md"
|
|
16
|
+
|
|
17
|
+
# Mirrors `docs/contracts/caveman-telemetry.md` `v1` constants.
|
|
18
|
+
MULTIPLIER_VERSION = "v1"
|
|
19
|
+
MULTIPLIER_VALUE = 0.9155
|
|
20
|
+
MULTIPLIER_ACTIVE = False # suspended pending v2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load(path: Path) -> list[dict]:
|
|
24
|
+
if not path.is_file():
|
|
25
|
+
return []
|
|
26
|
+
rows: list[dict] = []
|
|
27
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if not line or line.startswith("#"):
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
rows.append(json.loads(line))
|
|
33
|
+
except json.JSONDecodeError:
|
|
34
|
+
continue
|
|
35
|
+
return rows
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _delta(row: dict) -> int:
|
|
39
|
+
"""Per-row delta with suspended-multiplier guard."""
|
|
40
|
+
if not MULTIPLIER_ACTIVE:
|
|
41
|
+
return 0
|
|
42
|
+
explicit = row.get("caveman_delta_tokens")
|
|
43
|
+
if isinstance(explicit, (int, float)):
|
|
44
|
+
return int(explicit)
|
|
45
|
+
compressed = row.get("caveman_compressed_tokens")
|
|
46
|
+
if isinstance(compressed, (int, float)) and compressed > 0:
|
|
47
|
+
return int(compressed * MULTIPLIER_VALUE - compressed)
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def aggregate(rows: list[dict]) -> dict:
|
|
52
|
+
_zero = lambda: {"sessions": 0, "delta_tokens": 0, "compressed_tokens": 0}
|
|
53
|
+
by_session: dict[str, dict] = defaultdict(_zero)
|
|
54
|
+
by_conv: dict[str, dict] = defaultdict(_zero)
|
|
55
|
+
lifetime = _zero()
|
|
56
|
+
for row in rows:
|
|
57
|
+
sid = str(row.get("sessionId") or row.get("session_id") or "unknown")
|
|
58
|
+
cid = str(row.get("conversation_id") or "unknown")
|
|
59
|
+
delta = _delta(row)
|
|
60
|
+
comp = int(row.get("caveman_compressed_tokens") or 0)
|
|
61
|
+
for bucket in (by_session[sid], by_conv[cid], lifetime):
|
|
62
|
+
bucket["sessions"] += 1
|
|
63
|
+
bucket["delta_tokens"] += delta
|
|
64
|
+
bucket["compressed_tokens"] += comp
|
|
65
|
+
return {
|
|
66
|
+
"schema_version": "caveman-stats/v1",
|
|
67
|
+
"multiplier_version": MULTIPLIER_VERSION,
|
|
68
|
+
"multiplier_value": MULTIPLIER_VALUE,
|
|
69
|
+
"multiplier_active": MULTIPLIER_ACTIVE,
|
|
70
|
+
"lifetime": lifetime,
|
|
71
|
+
"by_session": dict(by_session),
|
|
72
|
+
"by_conversation": dict(by_conv),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def render_text(report: dict) -> str:
|
|
77
|
+
lines = [
|
|
78
|
+
f"caveman-stats {report['schema_version']} · multiplier {report['multiplier_version']}"
|
|
79
|
+
f" ({'ACTIVE' if report['multiplier_active'] else 'SUSPENDED'}) · "
|
|
80
|
+
f"value {report['multiplier_value']:.4f}",
|
|
81
|
+
"",
|
|
82
|
+
f" lifetime: {report['lifetime']['sessions']} sessions · "
|
|
83
|
+
f"delta_tokens = {report['lifetime']['delta_tokens']:+,} · "
|
|
84
|
+
f"compressed_tokens = {report['lifetime']['compressed_tokens']:,}",
|
|
85
|
+
"",
|
|
86
|
+
" by conversation:",
|
|
87
|
+
]
|
|
88
|
+
for cid, b in sorted(report["by_conversation"].items()):
|
|
89
|
+
lines.append(
|
|
90
|
+
f" {cid}: {b['sessions']} sessions · "
|
|
91
|
+
f"delta = {b['delta_tokens']:+,} · compressed = {b['compressed_tokens']:,}"
|
|
92
|
+
)
|
|
93
|
+
if not report["multiplier_active"]:
|
|
94
|
+
lines += [
|
|
95
|
+
"",
|
|
96
|
+
" Note: multiplier suspended — see docs/contracts/caveman-telemetry.md",
|
|
97
|
+
" (delta_tokens = 0 until kill-criterion satisfied in caveman-v2).",
|
|
98
|
+
]
|
|
99
|
+
return "\n".join(lines) + "\n"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: list[str] | None = None) -> int:
|
|
103
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
104
|
+
parser.add_argument("--input", type=Path, default=DEFAULT_JSONL)
|
|
105
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
106
|
+
args = parser.parse_args(argv)
|
|
107
|
+
|
|
108
|
+
rows = _load(args.input)
|
|
109
|
+
report = aggregate(rows)
|
|
110
|
+
|
|
111
|
+
if args.format == "json":
|
|
112
|
+
print(json.dumps(report, indent=2))
|
|
113
|
+
else:
|
|
114
|
+
print(render_text(report))
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
sys.exit(main())
|
|
@@ -17,7 +17,7 @@ Canonical counts:
|
|
|
17
17
|
Patterns checked (per file):
|
|
18
18
|
|
|
19
19
|
README.md
|
|
20
|
-
hero
|
|
20
|
+
hero badge "/badge/Commands-{N}-…" → active
|
|
21
21
|
browse line "Browse all {N} active commands" → active
|
|
22
22
|
browse meta "{N} files total" → total
|
|
23
23
|
browse meta "{N} are deprecation shims" → shims
|
|
@@ -84,7 +84,7 @@ def main() -> int:
|
|
|
84
84
|
|
|
85
85
|
checks = [
|
|
86
86
|
# README.md
|
|
87
|
-
(README, r"
|
|
87
|
+
(README, r"/badge/Commands-(\d+)-", active, "hero badge"),
|
|
88
88
|
(README, r"Browse all (\d+) active commands", active, "browse line"),
|
|
89
89
|
(README, r"\+ (\d+) native commands\)", active, "tools blurb"),
|
|
90
90
|
# docs/getting-started.md
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Input-side memory compression — Phase 2 of step-16-caveman-substance.
|
|
3
|
+
|
|
4
|
+
Rewrites memory files (AGENTS.md, CLAUDE.md, .cursorrules, ...) to caveman
|
|
5
|
+
grammar (drop articles / auxiliaries) while preserving carve-outs byte-for-byte
|
|
6
|
+
(code blocks, numbered-options, status markers, Iron-Law ALL-CAPS, backtick
|
|
7
|
+
spans). Writes `.original.md` backup before mutating. Gated by Phase 0
|
|
8
|
+
`validate_safe_paths.assert_safe`. Idempotency guard: `original_sha256:` +
|
|
9
|
+
`compressed_at:` frontmatter refuse re-compression on body-hash drift.
|
|
10
|
+
|
|
11
|
+
CLI: `compress_memory.py <path> [--check|--decompress]`. Stdlib-only.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import hashlib
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
23
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
24
|
+
|
|
25
|
+
from validate_safe_paths import SensitivePathError, assert_safe # noqa: E402
|
|
26
|
+
|
|
27
|
+
__all__ = ["compress_text", "compress_file", "decompress_file", "CompressionRefused"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CompressionRefused(RuntimeError):
|
|
31
|
+
"""Raised when the target is already compressed and body hash diverged."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Carve-out region patterns — mirrors caveman-speak.md § Carve-outs (1–7).
|
|
35
|
+
RE_FENCE = re.compile(r"^```")
|
|
36
|
+
RE_NUMBERED = re.compile(r"^>?\s*\d+\.\s")
|
|
37
|
+
RE_STATUS = re.compile(r"^\s*(?:❌|⚠️|✅)")
|
|
38
|
+
RE_IRONLAW = re.compile(r"^[A-Z][A-Z0-9 ,.\-_/']{3,}$")
|
|
39
|
+
RE_BACKTICK_SPAN = re.compile(r"`[^`\n]+`")
|
|
40
|
+
RE_FRONTMATTER = re.compile(r"^---\s*$")
|
|
41
|
+
WORD_RE = re.compile(r"\b[A-Za-z]+\b")
|
|
42
|
+
DROP_TOKENS = {"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
|
43
|
+
"being", "that", "which"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _compress_words(text: str) -> str:
|
|
47
|
+
out = WORD_RE.sub(lambda m: "" if m.group(0).lower() in DROP_TOKENS else m.group(0), text)
|
|
48
|
+
out = re.sub(r"[ \t]{2,}", " ", out)
|
|
49
|
+
return re.sub(r" +([,.;:!?])", r"\1", out)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _compress_prose_line(line: str) -> str:
|
|
53
|
+
"""Compress a prose line; preserve backtick-spans byte-for-byte."""
|
|
54
|
+
parts: list[str] = []
|
|
55
|
+
last = 0
|
|
56
|
+
for span in RE_BACKTICK_SPAN.finditer(line):
|
|
57
|
+
parts.append(_compress_words(line[last:span.start()]))
|
|
58
|
+
parts.append(span.group(0))
|
|
59
|
+
last = span.end()
|
|
60
|
+
parts.append(_compress_words(line[last:]))
|
|
61
|
+
return "".join(parts)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def compress_text(body: str) -> str:
|
|
65
|
+
"""Compress a memory-file body. Idempotent on already-caveman text."""
|
|
66
|
+
out: list[str] = []
|
|
67
|
+
in_fence = False
|
|
68
|
+
for raw in body.splitlines(keepends=True):
|
|
69
|
+
stripped = raw.rstrip("\r\n")
|
|
70
|
+
if RE_FENCE.match(stripped):
|
|
71
|
+
in_fence = not in_fence
|
|
72
|
+
out.append(raw)
|
|
73
|
+
continue
|
|
74
|
+
if in_fence or RE_NUMBERED.match(stripped) or RE_STATUS.match(stripped) \
|
|
75
|
+
or RE_IRONLAW.match(stripped.strip()):
|
|
76
|
+
out.append(raw)
|
|
77
|
+
continue
|
|
78
|
+
out.append(_compress_prose_line(raw))
|
|
79
|
+
return "".join(out)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _split_frontmatter(text: str) -> tuple[str, str]:
|
|
83
|
+
lines = text.splitlines(keepends=True)
|
|
84
|
+
if not lines or not RE_FRONTMATTER.match(lines[0].rstrip()):
|
|
85
|
+
return "", text
|
|
86
|
+
for idx in range(1, len(lines)):
|
|
87
|
+
if RE_FRONTMATTER.match(lines[idx].rstrip()):
|
|
88
|
+
return "".join(lines[: idx + 1]), "".join(lines[idx + 1:])
|
|
89
|
+
return "", text
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _sha256(text: str) -> str:
|
|
93
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _has_sha_marker(fm: str) -> bool:
|
|
97
|
+
return bool(re.search(r"^original_sha256:\s*[0-9a-f]{64}\s*$", fm, re.MULTILINE))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _inject_frontmatter(fm: str, sha: str, ts: str) -> str:
|
|
101
|
+
drop = re.compile(r"^(original_sha256|compressed_at):.*$", re.MULTILINE)
|
|
102
|
+
inner = drop.sub("", fm.strip().strip("-").strip()).strip() if fm else ""
|
|
103
|
+
body = inner + ("\n" if inner else "")
|
|
104
|
+
return f"---\n{body}original_sha256: {sha}\ncompressed_at: {ts}\n---\n"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _backup_path(target: Path) -> Path:
|
|
108
|
+
return target.parent / (target.name + ".original.md")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def compress_file(target: Path) -> Path:
|
|
112
|
+
assert_safe(target)
|
|
113
|
+
text = target.read_text(encoding="utf-8")
|
|
114
|
+
fm, body = _split_frontmatter(text)
|
|
115
|
+
if _has_sha_marker(fm):
|
|
116
|
+
if _sha256(compress_text(body)) != _sha256(body):
|
|
117
|
+
raise CompressionRefused(
|
|
118
|
+
f"{target}: body hash diverged; decompress first "
|
|
119
|
+
f"(`scripts/compress_memory.py {target} --decompress`)."
|
|
120
|
+
)
|
|
121
|
+
return target
|
|
122
|
+
backup = _backup_path(target)
|
|
123
|
+
backup.write_text(text, encoding="utf-8")
|
|
124
|
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
125
|
+
target.write_text(
|
|
126
|
+
_inject_frontmatter(fm, _sha256(body), ts) + compress_text(body),
|
|
127
|
+
encoding="utf-8",
|
|
128
|
+
)
|
|
129
|
+
return backup
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def decompress_file(target: Path) -> Path:
|
|
133
|
+
assert_safe(target)
|
|
134
|
+
backup = _backup_path(target)
|
|
135
|
+
if not backup.is_file():
|
|
136
|
+
raise FileNotFoundError(f"no backup at {backup}")
|
|
137
|
+
target.write_text(backup.read_text(encoding="utf-8"), encoding="utf-8")
|
|
138
|
+
backup.unlink()
|
|
139
|
+
return target
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _main(argv: list[str]) -> int:
|
|
143
|
+
ap = argparse.ArgumentParser(description="Compress memory files to caveman grammar.")
|
|
144
|
+
ap.add_argument("path", type=Path)
|
|
145
|
+
grp = ap.add_mutually_exclusive_group()
|
|
146
|
+
grp.add_argument("--check", action="store_true", help="exit 0 if safe; no writes")
|
|
147
|
+
grp.add_argument("--decompress", action="store_true", help="restore .original.md")
|
|
148
|
+
args = ap.parse_args(argv)
|
|
149
|
+
try:
|
|
150
|
+
if args.check:
|
|
151
|
+
assert_safe(args.path)
|
|
152
|
+
return 0
|
|
153
|
+
if args.decompress:
|
|
154
|
+
decompress_file(args.path)
|
|
155
|
+
print(f"decompressed: {args.path}")
|
|
156
|
+
return 0
|
|
157
|
+
backup = compress_file(args.path)
|
|
158
|
+
print(f"compressed: {args.path} (backup: {backup})")
|
|
159
|
+
return 0
|
|
160
|
+
except SensitivePathError as exc:
|
|
161
|
+
print(f"error: refused: {exc}", file=sys.stderr)
|
|
162
|
+
return 2
|
|
163
|
+
except CompressionRefused as exc:
|
|
164
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
165
|
+
return 3
|
|
166
|
+
except FileNotFoundError as exc:
|
|
167
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
168
|
+
return 4
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
raise SystemExit(_main(sys.argv[1:]))
|