@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.
Files changed (37) hide show
  1. package/.agent-src/commands/agent-status.md +16 -0
  2. package/.agent-src/rules/caveman-speak.md +2 -0
  3. package/.agent-src/skills/adversarial-review/SKILL.md +2 -1
  4. package/.agent-src/skills/canvas-design/SKILL.md +11 -6
  5. package/.agent-src/skills/compress-memory/SKILL.md +119 -0
  6. package/.agent-src/skills/fe-design/SKILL.md +8 -0
  7. package/.agent-src/skills/prompt-optimizer/SKILL.md +29 -5
  8. package/.agent-src/skills/react-shadcn-ui/SKILL.md +9 -0
  9. package/.agent-src/skills/refine-prompt/SKILL.md +57 -0
  10. package/.agent-src/skills/tailwind-engineer/SKILL.md +14 -0
  11. package/.agent-src/templates/agents/agent-project-settings.example.yml +53 -1
  12. package/.claude-plugin/marketplace.json +2 -1
  13. package/CHANGELOG.md +101 -138
  14. package/README.md +5 -5
  15. package/docs/architecture.md +2 -2
  16. package/docs/archive/CHANGELOG-pre-2.20.0.md +159 -0
  17. package/docs/benchmarks.md +74 -0
  18. package/docs/catalog.md +5 -3
  19. package/docs/contracts/caveman-telemetry.md +83 -0
  20. package/docs/contracts/compression-default-kill-criterion.md +82 -35
  21. package/docs/contracts/cost-summary-schema.md +107 -0
  22. package/docs/contracts/file-ownership-matrix.json +48 -0
  23. package/docs/guidelines/prompt-templates.md +166 -0
  24. package/package.json +1 -1
  25. package/scripts/_lib/bench_caveman.py +273 -0
  26. package/scripts/_lib/bench_caveman_report.py +152 -0
  27. package/scripts/bench_compress_memory.py +168 -0
  28. package/scripts/bench_run.py +119 -1
  29. package/scripts/caveman_stats.py +119 -0
  30. package/scripts/check_command_count_messaging.py +2 -2
  31. package/scripts/compress_memory.py +172 -0
  32. package/scripts/cost_by_conversation.py +78 -0
  33. package/scripts/cost_summary.py +97 -0
  34. package/scripts/update_counts.py +7 -5
  35. package/scripts/validate_caveman_carveouts.py +129 -0
  36. package/scripts/validate_safe_paths.py +118 -0
  37. package/scripts/verify_roadmap_closure.py +327 -0
@@ -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.1.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 row "<strong>{N} Commands</strong>" → active
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"<strong>(\d+) Commands</strong>", active, "hero row"),
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())
@@ -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 line: **NNN Skills** · **NNN Rules** · **NNN Commands** · **NNN Guidelines**
59
- (r"(<strong>)(\d+)( Skills</strong>)", "skills"),
60
- (r"(<strong>)(\d+)( Rules</strong>)", "rules"),
61
- (r"(<strong>)(\d+)( Guidelines</strong>)", "guidelines"),
62
- # NOTE: hero `<strong>N Commands</strong>` and tools-blurb
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