@event4u/agent-config 2.20.1 → 2.23.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/adversarial-review/SKILL.md +2 -1
- package/.agent-src/skills/canvas-design/SKILL.md +11 -6
- package/.agent-src/skills/compress-memory/SKILL.md +119 -0
- package/.agent-src/skills/fe-design/SKILL.md +8 -0
- package/.agent-src/skills/prompt-optimizer/SKILL.md +29 -5
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +9 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +57 -0
- package/.agent-src/skills/tailwind-engineer/SKILL.md +14 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +53 -1
- package/.claude-plugin/marketplace.json +2 -1
- package/CHANGELOG.md +101 -138
- package/README.md +5 -5
- package/docs/architecture.md +2 -2
- package/docs/archive/CHANGELOG-pre-2.20.0.md +159 -0
- package/docs/benchmarks.md +74 -0
- package/docs/catalog.md +5 -3
- 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 +48 -0
- package/docs/guidelines/prompt-templates.md +166 -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
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:]))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Group cost-tracking sessions by conversation_id (Ruflo `conversation.mjs` `5b71c7a` ref)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import argparse, json, sys
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
9
|
+
DEFAULT_JSONL = REPO_ROOT / "agents" / "cost-tracking" / "sessions.jsonl"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load(path: Path) -> list[dict]:
|
|
13
|
+
if not path.is_file():
|
|
14
|
+
return []
|
|
15
|
+
out = []
|
|
16
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
17
|
+
s = line.strip()
|
|
18
|
+
if not s or s.startswith("#"):
|
|
19
|
+
continue
|
|
20
|
+
try:
|
|
21
|
+
out.append(json.loads(s))
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
continue
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def group(rows: list[dict]) -> dict:
|
|
28
|
+
by_conv: dict = defaultdict(lambda: {
|
|
29
|
+
"sessions": 0, "total_cost_usd": 0.0, "input_tokens": 0,
|
|
30
|
+
"output_tokens": 0, "caveman_delta_tokens": 0,
|
|
31
|
+
"by_model": defaultdict(lambda: {"sessions": 0, "cost_usd": 0.0}),
|
|
32
|
+
})
|
|
33
|
+
for row in rows:
|
|
34
|
+
cid = str(row.get("conversation_id") or "unknown")
|
|
35
|
+
b = by_conv[cid]
|
|
36
|
+
cost = float(row.get("total_cost_usd") or 0)
|
|
37
|
+
b["sessions"] += 1
|
|
38
|
+
b["total_cost_usd"] += cost
|
|
39
|
+
b["input_tokens"] += int(row.get("input_tokens") or 0)
|
|
40
|
+
b["output_tokens"] += int(row.get("output_tokens") or 0)
|
|
41
|
+
b["caveman_delta_tokens"] += int(row.get("caveman_delta_tokens") or 0)
|
|
42
|
+
m = b["by_model"][str(row.get("model") or "unknown")]
|
|
43
|
+
m["sessions"] += 1
|
|
44
|
+
m["cost_usd"] += cost
|
|
45
|
+
return {cid: {**b, "by_model": dict(b["by_model"])} for cid, b in by_conv.items()}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render_text(report: dict) -> str:
|
|
49
|
+
if not report:
|
|
50
|
+
return "cost-by-conversation: no rows.\n"
|
|
51
|
+
lines = ["cost-by-conversation lens · grouped by conversation_id", ""]
|
|
52
|
+
for cid, b in sorted(report.items()):
|
|
53
|
+
lines.append(
|
|
54
|
+
f" {cid}: {b['sessions']} sessions · ${b['total_cost_usd']:.4f} · "
|
|
55
|
+
f"in {b['input_tokens']:,} · out {b['output_tokens']:,} · "
|
|
56
|
+
f"caveman_delta {b['caveman_delta_tokens']:+,}"
|
|
57
|
+
)
|
|
58
|
+
for model, m in sorted(b["by_model"].items()):
|
|
59
|
+
lines.append(f" {model}: {m['sessions']} sessions · ${m['cost_usd']:.4f}")
|
|
60
|
+
return "\n".join(lines) + "\n"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main(argv: list[str] | None = None) -> int:
|
|
64
|
+
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
65
|
+
p.add_argument("--input", type=Path, default=DEFAULT_JSONL)
|
|
66
|
+
p.add_argument("--format", choices=["text", "json"], default="text")
|
|
67
|
+
args = p.parse_args(argv)
|
|
68
|
+
report = group(_load(args.input))
|
|
69
|
+
if args.format == "json":
|
|
70
|
+
print(json.dumps({"schema_version": "cost-by-conversation/v1",
|
|
71
|
+
"by_conversation": report}, indent=2))
|
|
72
|
+
else:
|
|
73
|
+
print(render_text(report))
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
sys.exit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Emit `cost-summary/v1` JSON per `docs/contracts/cost-summary-schema.md`.
|
|
3
|
+
|
|
4
|
+
Reads `agents/cost-tracking/sessions.jsonl` (or `--input`), aggregates by
|
|
5
|
+
session, conversation, and model. Honors the caveman suspended-multiplier
|
|
6
|
+
contract (delta = 0 while suspended; see `caveman-telemetry.md`).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import argparse, json, sys
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
15
|
+
DEFAULT_JSONL = REPO_ROOT / "agents" / "cost-tracking" / "sessions.jsonl"
|
|
16
|
+
SCHEMA = "cost-summary/v1"
|
|
17
|
+
MULTIPLIER_VERSION = "v1"
|
|
18
|
+
MULTIPLIER_ACTIVE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load(path: Path) -> list[dict]:
|
|
22
|
+
if not path.is_file():
|
|
23
|
+
return []
|
|
24
|
+
out = []
|
|
25
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
26
|
+
s = line.strip()
|
|
27
|
+
if not s or s.startswith("#"):
|
|
28
|
+
continue
|
|
29
|
+
try:
|
|
30
|
+
out.append(json.loads(s))
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
continue
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _delta(row: dict) -> int:
|
|
37
|
+
if not MULTIPLIER_ACTIVE:
|
|
38
|
+
return 0
|
|
39
|
+
return int(row.get("caveman_delta_tokens") or 0)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _zero_kv() -> dict:
|
|
43
|
+
return {"sessions": 0, "total_cost_usd": 0.0, "input_tokens": 0,
|
|
44
|
+
"output_tokens": 0, "caveman_delta_tokens": 0}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _zero_model() -> dict:
|
|
48
|
+
return {"sessions": 0, "total_cost_usd": 0.0, "input_tokens": 0, "output_tokens": 0}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def aggregate(rows: list[dict]) -> dict:
|
|
52
|
+
by_sess: dict = defaultdict(_zero_kv)
|
|
53
|
+
by_conv: dict = defaultdict(_zero_kv)
|
|
54
|
+
by_model: dict = defaultdict(_zero_model)
|
|
55
|
+
totals = _zero_kv()
|
|
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
|
+
model = str(row.get("model") or "unknown")
|
|
60
|
+
cost = float(row.get("total_cost_usd") or 0)
|
|
61
|
+
itok = int(row.get("input_tokens") or 0)
|
|
62
|
+
otok = int(row.get("output_tokens") or 0)
|
|
63
|
+
delta = _delta(row)
|
|
64
|
+
for bucket in (by_sess[sid], by_conv[cid], totals):
|
|
65
|
+
bucket["sessions"] += 1
|
|
66
|
+
bucket["total_cost_usd"] += cost
|
|
67
|
+
bucket["input_tokens"] += itok
|
|
68
|
+
bucket["output_tokens"] += otok
|
|
69
|
+
bucket["caveman_delta_tokens"] += delta
|
|
70
|
+
m = by_model[model]
|
|
71
|
+
m["sessions"] += 1
|
|
72
|
+
m["total_cost_usd"] += cost
|
|
73
|
+
m["input_tokens"] += itok
|
|
74
|
+
m["output_tokens"] += otok
|
|
75
|
+
totals["caveman_multiplier_version"] = MULTIPLIER_VERSION
|
|
76
|
+
totals["caveman_multiplier_active"] = MULTIPLIER_ACTIVE
|
|
77
|
+
return {
|
|
78
|
+
"schema_version": SCHEMA,
|
|
79
|
+
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
80
|
+
"totals": totals,
|
|
81
|
+
"by_session": [{"key": k, **v} for k, v in sorted(by_sess.items())],
|
|
82
|
+
"by_conversation": [{"key": k, **v} for k, v in sorted(by_conv.items())],
|
|
83
|
+
"by_model": [{"model": k, **v} for k, v in sorted(by_model.items())],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main(argv: list[str] | None = None) -> int:
|
|
88
|
+
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
89
|
+
p.add_argument("--input", type=Path, default=DEFAULT_JSONL)
|
|
90
|
+
p.add_argument("--format", choices=["json"], default="json")
|
|
91
|
+
args = p.parse_args(argv)
|
|
92
|
+
print(json.dumps(aggregate(_load(args.input)), indent=2))
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
sys.exit(main())
|
package/scripts/update_counts.py
CHANGED
|
@@ -55,11 +55,13 @@ TARGETS: list[tuple[str, list[tuple[str, str]]]] = [
|
|
|
55
55
|
[
|
|
56
56
|
(r"(Browse all )(\d+)( commands\])", "commands"),
|
|
57
57
|
(r"(package \(rules \+ )(\d+)( skills)", "skills"),
|
|
58
|
-
# Hero
|
|
59
|
-
|
|
60
|
-
(r"(
|
|
61
|
-
(r"(
|
|
62
|
-
|
|
58
|
+
# Hero badges: shields.io URLs `Skills-NNN-<color>` etc.
|
|
59
|
+
# Format: https://img.shields.io/badge/<Label>-<N>-<hex>?style=flat-square
|
|
60
|
+
(r"(/badge/Skills-)(\d+)(-)", "skills"),
|
|
61
|
+
(r"(/badge/Rules-)(\d+)(-)", "rules"),
|
|
62
|
+
(r"(/badge/Guidelines-)(\d+)(-)", "guidelines"),
|
|
63
|
+
(r"(/badge/Personas-)(\d+)(-)", "personas"),
|
|
64
|
+
# NOTE: hero `Commands-N` badge and tools-blurb
|
|
63
65
|
# `skills + N native commands` are owned by
|
|
64
66
|
# `check_command_count_messaging.py` (Phase-1.2 of
|
|
65
67
|
# road-to-pr-34-followups). Those surfaces advertise the
|