@event4u/agent-config 1.22.0 → 1.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 (91) hide show
  1. package/.agent-src/commands/agents/cleanup.md +31 -17
  2. package/.agent-src/commands/commit/in-chunks.md +30 -10
  3. package/.agent-src/commands/commit.md +46 -6
  4. package/.agent-src/commands/compress.md +19 -13
  5. package/.agent-src/commands/cost-report.md +120 -0
  6. package/.agent-src/commands/create-pr/description-only.md +8 -0
  7. package/.agent-src/commands/create-pr.md +95 -80
  8. package/.agent-src/commands/feature/plan.md +13 -7
  9. package/.agent-src/commands/memory/add.md +16 -8
  10. package/.agent-src/commands/memory/promote.md +17 -9
  11. package/.agent-src/commands/optimize/rtk.md +16 -11
  12. package/.agent-src/commands/prepare-for-review.md +12 -6
  13. package/.agent-src/commands/project-analyze.md +31 -20
  14. package/.agent-src/commands/review-changes.md +24 -15
  15. package/.agent-src/commands/roadmap/create.md +14 -9
  16. package/.agent-src/contexts/contracts/frugality-charter.md +57 -0
  17. package/.agent-src/rules/architecture.md +9 -0
  18. package/.agent-src/rules/ask-when-uncertain.md +3 -13
  19. package/.agent-src/rules/caveman-speak.md +78 -0
  20. package/.agent-src/rules/direct-answers.md +5 -14
  21. package/.agent-src/rules/markdown-safe-codeblocks.md +6 -7
  22. package/.agent-src/rules/no-cheap-questions.md +4 -14
  23. package/.agent-src/rules/token-efficiency.md +5 -7
  24. package/.agent-src/skills/adr-create/SKILL.md +197 -0
  25. package/.agent-src/skills/agent-docs-writing/SKILL.md +23 -1
  26. package/.agent-src/skills/command-writing/SKILL.md +23 -0
  27. package/.agent-src/skills/context-authoring/SKILL.md +23 -0
  28. package/.agent-src/skills/conventional-commits-writing/SKILL.md +23 -0
  29. package/.agent-src/skills/guideline-writing/SKILL.md +22 -0
  30. package/.agent-src/skills/persona-writing/SKILL.md +153 -0
  31. package/.agent-src/skills/readme-writing/SKILL.md +20 -0
  32. package/.agent-src/skills/readme-writing-package/SKILL.md +19 -0
  33. package/.agent-src/skills/roadmap-writing/SKILL.md +157 -0
  34. package/.agent-src/skills/rule-writing/SKILL.md +22 -0
  35. package/.agent-src/skills/script-writing/SKILL.md +226 -0
  36. package/.agent-src/skills/skill-writing/SKILL.md +23 -0
  37. package/.agent-src/skills/test-driven-development/SKILL.md +24 -0
  38. package/.agent-src/templates/agent-settings.md +73 -0
  39. package/.agent-src/templates/command.md +15 -10
  40. package/.agent-src/templates/rule.md +6 -0
  41. package/.agent-src/templates/skill.md +32 -0
  42. package/.claude-plugin/marketplace.json +6 -1
  43. package/AGENTS.md +3 -3
  44. package/CHANGELOG.md +35 -0
  45. package/README.md +5 -5
  46. package/docs/architecture.md +4 -4
  47. package/docs/customization.md +72 -0
  48. package/docs/decisions/INDEX.md +15 -0
  49. package/docs/getting-started.md +2 -2
  50. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -19
  51. package/docs/guidelines/agent-infra/carve-out-predicates.md +17 -0
  52. package/docs/guidelines/agent-infra/mcp-request-signing.md +199 -0
  53. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +11 -4
  54. package/package.json +1 -1
  55. package/scripts/_lib/__init__.py +5 -0
  56. package/scripts/_lib/script_output.py +140 -0
  57. package/scripts/adr/regenerate_index.py +79 -0
  58. package/scripts/ai_council/one_off_archive/2026-05/_one_off_add_quiet.py +149 -0
  59. package/scripts/ai_council/one_off_archive/2026-05/_one_off_inject_quiet_flag.py +33 -0
  60. package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_v2.sh +36 -0
  61. package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_verbosity.sh +26 -0
  62. package/scripts/ai_council/one_off_archive/2026-05/_one_off_per_task.sh +41 -0
  63. package/scripts/ai_council/one_off_archive/2026-05/_one_off_silent_taskfiles.py +98 -0
  64. package/scripts/check_augmentignore.py +4 -1
  65. package/scripts/check_command_count_messaging.py +4 -1
  66. package/scripts/check_compressed_paths.py +4 -1
  67. package/scripts/check_council_layout.py +4 -1
  68. package/scripts/check_council_references.py +4 -1
  69. package/scripts/check_iron_law_prominence.py +3 -1
  70. package/scripts/check_md_language.py +3 -1
  71. package/scripts/check_memory_proposal.py +3 -1
  72. package/scripts/check_public_catalog_links.py +4 -1
  73. package/scripts/check_reply_consistency.py +8 -2
  74. package/scripts/check_roadmap_trackable.py +4 -1
  75. package/scripts/compile_router.py +27 -0
  76. package/scripts/compress.py +33 -19
  77. package/scripts/cost/budget.mjs +152 -0
  78. package/scripts/cost/track.mjs +144 -0
  79. package/scripts/first-run.sh +3 -9
  80. package/scripts/install-hooks.sh +19 -1
  81. package/scripts/install.py +17 -12
  82. package/scripts/install.sh +19 -8
  83. package/scripts/lint_examples.py +6 -2
  84. package/scripts/lint_handoffs.py +4 -1
  85. package/scripts/lint_load_context.py +4 -1
  86. package/scripts/lint_roadmap_complexity.py +6 -2
  87. package/scripts/lint_rule_interactions.py +4 -1
  88. package/scripts/lint_rule_tiers.py +4 -1
  89. package/scripts/measure_frugality_savings.py +164 -0
  90. package/scripts/runtime_dispatcher.py +11 -0
  91. package/scripts/skill_linter.py +207 -2
@@ -34,6 +34,8 @@ import sys
34
34
  from pathlib import Path
35
35
  from typing import Iterable
36
36
 
37
+ QUIET = "--quiet" in sys.argv
38
+
37
39
  ROOT = Path(".")
38
40
 
39
41
  # A specific file inside a council dir: must end with .md or .json,
@@ -124,7 +126,8 @@ def main() -> int:
124
126
  violations.append((path, ln, ref))
125
127
 
126
128
  if not violations:
127
- print("✅ No forbidden council references in durable artefacts.")
129
+ if not QUIET:
130
+ print("✅ No forbidden council references in durable artefacts.")
128
131
  return 0
129
132
 
130
133
  print(f"❌ {len(violations)} forbidden council reference(s):\n")
@@ -111,6 +111,7 @@ def main() -> int:
111
111
  help="Files or directories to scan (default: .agent-src.uncompressed/rules)",
112
112
  )
113
113
  parser.add_argument("--format", choices=["text", "json"], default="text")
114
+ parser.add_argument("--quiet", action="store_true", help="Only print on failure")
114
115
  args = parser.parse_args()
115
116
 
116
117
  targets = _resolve_targets(args.paths)
@@ -125,7 +126,8 @@ def main() -> int:
125
126
  print(json.dumps([asdict(v) for v in all_violations], indent=2, ensure_ascii=False))
126
127
  else:
127
128
  if not all_violations:
128
- print(f"✅ Iron Law prominence clean ({len(targets)} file(s) scanned).")
129
+ if not args.quiet:
130
+ print(f"✅ Iron Law prominence clean ({len(targets)} file(s) scanned).")
129
131
  else:
130
132
  print(f"❌ {len(all_violations)} Iron-Law prominence violation(s):\n")
131
133
  for v in all_violations:
@@ -124,6 +124,7 @@ def main() -> int:
124
124
  parser = argparse.ArgumentParser(description=__doc__)
125
125
  parser.add_argument("paths", nargs="+", help="One or more .md files to scan")
126
126
  parser.add_argument("--format", choices=["text", "json"], default="text")
127
+ parser.add_argument("--quiet", action="store_true", help="Only print on failure")
127
128
  args = parser.parse_args()
128
129
 
129
130
  all_violations: list[Violation] = []
@@ -141,7 +142,8 @@ def main() -> int:
141
142
  print(json.dumps([asdict(v) for v in all_violations], indent=2, ensure_ascii=False))
142
143
  else:
143
144
  if not all_violations:
144
- print("✅ No German content detected.")
145
+ if not args.quiet:
146
+ print("✅ No German content detected.")
145
147
  else:
146
148
  print(f"❌ {len(all_violations)} violation(s) found:\n")
147
149
  for v in all_violations:
@@ -152,6 +152,7 @@ def main() -> int:
152
152
  grp.add_argument("--intake-id", help="Promote an intake record by id")
153
153
  grp.add_argument("--proposal", help="Promote a proposal YAML file")
154
154
  ap.add_argument("--format", choices=["text", "json"], default="text")
155
+ ap.add_argument("--quiet", action="store_true", help="Only print on failure")
155
156
  args = ap.parse_args()
156
157
  if args.intake_id:
157
158
  record = _find_intake(args.intake_id)
@@ -172,7 +173,8 @@ def main() -> int:
172
173
  for f in failures:
173
174
  print(f" 🔴 {f}")
174
175
  else:
175
- print(f"✅ {source} gate passed")
176
+ if not args.quiet:
177
+ print(f"✅ {source} — gate passed")
176
178
  return 1 if failures else 0
177
179
 
178
180
 
@@ -27,6 +27,8 @@ import re
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
+ QUIET = "--quiet" in sys.argv
31
+
30
32
  ROOT = Path(__file__).resolve().parent.parent
31
33
  CATALOG = ROOT / "docs" / "catalog.md"
32
34
  PACKAGE_JSON = ROOT / "package.json"
@@ -95,7 +97,8 @@ def main() -> int:
95
97
 
96
98
  total_violations = len(forbidden) + len(missing) + len(unshipped)
97
99
  if not total_violations:
98
- print(f"✅ docs/catalog.md all links resolve to shipped surfaces.")
100
+ if not QUIET:
101
+ print(f"✅ docs/catalog.md — all links resolve to shipped surfaces.")
99
102
  return 0
100
103
 
101
104
  print(f"❌ docs/catalog.md — {total_violations} violation(s):")
@@ -21,6 +21,8 @@ import re
21
21
  import sys
22
22
  from pathlib import Path
23
23
 
24
+ QUIET = "--quiet" in sys.argv
25
+
24
26
  OPTION_LINE_RE = re.compile(r"^\s*>?\s*(\d+)\.\s+\S")
25
27
  REC_LINE_RE = re.compile(
26
28
  r"(?:Recommendation|Empfehlung)\s*:\s*(\d+)\b", re.IGNORECASE
@@ -108,7 +110,8 @@ def cmd_scan_dir(root: Path) -> int:
108
110
  print(f" 🔴 {path}:{line} — inline-tag — {snippet}", file=sys.stderr)
109
111
  print(f"\n❌ {len(violations)} legacy-pattern violation(s)", file=sys.stderr)
110
112
  return 6
111
- print(f"✅ No legacy (recommended) tags found under {root}")
113
+ if not QUIET:
114
+ print(f"✅ No legacy (recommended) tags found under {root}")
112
115
  return 0
113
116
 
114
117
 
@@ -121,6 +124,8 @@ def main(argv: list[str] | None = None) -> int:
121
124
  p.add_argument("--strict", action="store_true",
122
125
  help="numbered options REQUIRE recommendation line (rule 5)")
123
126
  p.add_argument("-v", "--verbose", action="store_true")
127
+ p.add_argument("--quiet", action="store_true",
128
+ help="suppress success messages (P10.5)")
124
129
  args = p.parse_args(argv)
125
130
 
126
131
  if args.scan_dir:
@@ -130,7 +135,8 @@ def main(argv: list[str] | None = None) -> int:
130
135
  code, msg = validate(text, strict=args.strict)
131
136
  if code == 0:
132
137
  if args.verbose:
133
- print(f"✅ {msg}")
138
+ if not QUIET:
139
+ print(f"✅ {msg}")
134
140
  return 0
135
141
  print(f"❌ [exit {code}] {msg}", file=sys.stderr)
136
142
  return code
@@ -43,6 +43,8 @@ from update_roadmap_progress import ( # noqa: E402
43
43
  parse_frontmatter,
44
44
  )
45
45
 
46
+ QUIET = "--quiet" in sys.argv
47
+
46
48
  ROADMAP_ROOT = Path("agents/roadmaps")
47
49
 
48
50
 
@@ -103,7 +105,8 @@ def main() -> int:
103
105
  )
104
106
  return 1
105
107
  count = len(find_active_roadmaps(ROADMAP_ROOT))
106
- print(f"✅ {count} active roadmap(s) — all parseable, all phases have checkboxes.")
108
+ if not QUIET:
109
+ print(f"✅ {count} active roadmap(s) — all parseable, all phases have checkboxes.")
107
110
  return 0
108
111
 
109
112
 
@@ -17,8 +17,18 @@ from pathlib import Path
17
17
  ROOT = Path(__file__).resolve().parent.parent
18
18
  RULES_DIR = ROOT / ".agent-src.uncompressed" / "rules"
19
19
  OUT_PATH = ROOT / "router.json"
20
+ SETTINGS_PATH = ROOT / ".agent-settings.yml"
20
21
  SCHEMA_VERSION = 1
21
22
 
23
+ # Compile-time rule toggles. Maps rule-id → settings predicate.
24
+ # Rule omitted from router.json when predicate returns False.
25
+ # Per road-to-token-frugality § Phase 8.2 — caveman.speak compile-time toggle.
26
+ COMPILE_TIME_TOGGLES = {
27
+ "caveman-speak": lambda s: bool(
28
+ s.get("caveman", {}).get("enabled", True)
29
+ ) and bool(s.get("caveman", {}).get("speak", True)),
30
+ }
31
+
22
32
  # Maps legacy tier values to the router-canonical names. See
23
33
  # docs/contracts/rule-router.md § Backward compatibility.
24
34
  LEGACY_TIER_MAP = {
@@ -89,7 +99,21 @@ def _normalize_trigger(item) -> dict | None:
89
99
  return {keys[0]: str(item[keys[0]])}
90
100
 
91
101
 
102
+ def _load_settings() -> dict:
103
+ """Read .agent-settings.yml for compile-time toggles. Stdlib-only fallback."""
104
+ if not SETTINGS_PATH.exists():
105
+ return {}
106
+ text = SETTINGS_PATH.read_text(encoding="utf-8")
107
+ try:
108
+ import yaml # type: ignore
109
+ data = yaml.safe_load(text) or {}
110
+ return data if isinstance(data, dict) else {}
111
+ except ImportError:
112
+ return {}
113
+
114
+
92
115
  def _collect(rules_dir: Path) -> dict:
116
+ settings = _load_settings()
93
117
  kernel: list[str] = []
94
118
  tiered: dict[str, list[dict]] = {"tier-1": [], "tier-2": []}
95
119
  for path in sorted(rules_dir.glob("*.md")):
@@ -97,6 +121,9 @@ def _collect(rules_dir: Path) -> dict:
97
121
  if not fm:
98
122
  continue
99
123
  rule_id = path.stem
124
+ if rule_id in COMPILE_TIME_TOGGLES:
125
+ if not COMPILE_TIME_TOGGLES[rule_id](settings):
126
+ continue
100
127
  rule_type = str(fm.get("type", "auto"))
101
128
  tier = _resolve_tier(rule_type, fm.get("tier", ""))
102
129
  if tier not in ALLOWED_TIERS:
@@ -26,6 +26,9 @@ import shutil
26
26
  import sys
27
27
  from pathlib import Path
28
28
 
29
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
30
+ from _lib.script_output import info, success, flush_summary, resolve_level # noqa: E402
31
+
29
32
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
30
33
  SOURCE_DIR = PROJECT_ROOT / ".agent-src.uncompressed"
31
34
  TARGET_DIR = PROJECT_ROOT / ".agent-src"
@@ -479,11 +482,11 @@ def generate_rule_symlinks() -> int:
479
482
  if tool_count != source_count:
480
483
  print(f" ⚠️ {tool_dir}: {tool_count} rules (expected {source_count})")
481
484
 
482
- print(f" ✅ Created {total} rule symlinks across {len(TOOL_DIRS)} tool directories ({source_count} rules each)")
485
+ info(f" ✅ Created {total} rule symlinks across {len(TOOL_DIRS)} tool directories ({source_count} rules each)")
483
486
  return total
484
487
 
485
488
 
486
- def generate_windsurfrules() -> None:
489
+ def generate_windsurfrules() -> int:
487
490
  """Generate .windsurfrules by concatenating all rules (no frontmatter).
488
491
  """
489
492
  rules = sorted([f.name for f in RULES_SOURCE.glob("*.md")])
@@ -496,7 +499,8 @@ def generate_windsurfrules() -> None:
496
499
 
497
500
  output = PROJECT_ROOT / ".windsurfrules"
498
501
  output.write_text("\n".join(parts) + "\n")
499
- print(f" ✅ Generated .windsurfrules ({len(rules)} rules)")
502
+ info(f" ✅ Generated .windsurfrules ({len(rules)} rules)")
503
+ return len(rules)
500
504
 
501
505
 
502
506
  def generate_gemini_md() -> None:
@@ -505,7 +509,7 @@ def generate_gemini_md() -> None:
505
509
  if link.exists() or link.is_symlink():
506
510
  link.unlink()
507
511
  link.symlink_to("AGENTS.md")
508
- print(" ✅ Created GEMINI.md → AGENTS.md symlink")
512
+ info(" ✅ Created GEMINI.md → AGENTS.md symlink")
509
513
 
510
514
 
511
515
  def _command_slug(source_file: Path) -> str:
@@ -531,12 +535,12 @@ def _iter_commands():
531
535
  yield source_file, _command_slug(source_file)
532
536
 
533
537
 
534
- def generate_claude_skills() -> None:
538
+ def generate_claude_skills() -> int:
535
539
  """Create .claude/skills/ symlinks for ALL skills in .agent-src/skills/.
536
540
  """
537
541
  if not SKILLS_SOURCE.exists():
538
- print(" ⚠️ .agent-src/skills/ not found — skipping skills")
539
- return
542
+ print(" ⚠️ .agent-src/skills/ not found — skipping skills", file=sys.stderr)
543
+ return 0
540
544
 
541
545
  # All skill directories in .agent-src/skills/
542
546
  skills = sorted([d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()])
@@ -559,7 +563,8 @@ def generate_claude_skills() -> None:
559
563
  link.symlink_to(rel_target)
560
564
  count += 1
561
565
 
562
- print(f" ✅ Created {count} skill symlinks in .claude/skills/")
566
+ info(f" ✅ Created {count} skill symlinks in .claude/skills/")
567
+ return count
563
568
 
564
569
 
565
570
  def extract_description_from_md(content: str) -> str:
@@ -573,7 +578,7 @@ def extract_description_from_md(content: str) -> str:
573
578
  return ""
574
579
 
575
580
 
576
- def generate_claude_commands() -> None:
581
+ def generate_claude_commands() -> int:
577
582
  """Create .claude/skills/{slug}/SKILL.md symlinks for ALL Augment commands.
578
583
 
579
584
  Commands in .agent-src/commands/ are the single source of truth.
@@ -585,8 +590,8 @@ def generate_claude_commands() -> None:
585
590
  to `council-default` so directories never collide in `.claude/skills/`.
586
591
  """
587
592
  if not COMMANDS_SOURCE.exists():
588
- print(" ⚠️ .agent-src/commands/ not found — skipping commands")
589
- return
593
+ print(" ⚠️ .agent-src/commands/ not found — skipping commands", file=sys.stderr)
594
+ return 0
590
595
 
591
596
  CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
592
597
 
@@ -642,7 +647,8 @@ def generate_claude_commands() -> None:
642
647
  msg += f" ({skipped} skipped — same-name skill exists)"
643
648
  if removed_dirs:
644
649
  msg += f" ({removed_dirs} stale dirs removed)"
645
- print(msg)
650
+ info(msg)
651
+ return count
646
652
 
647
653
 
648
654
  def generate_persona_symlinks() -> int:
@@ -676,20 +682,28 @@ def generate_persona_symlinks() -> int:
676
682
  link.symlink_to(target)
677
683
  total += 1
678
684
 
679
- print(f" ✅ Created {total} persona symlinks across {len(PERSONA_TOOL_DIRS)} tool directories ({len(personas)} personas each)")
685
+ info(f" ✅ Created {total} persona symlinks across {len(PERSONA_TOOL_DIRS)} tool directories ({len(personas)} personas each)")
680
686
  return total
681
687
 
682
688
 
683
689
  def generate_tools() -> None:
684
690
  """Generate all tool-specific directories and files."""
685
- print("🔧 Generating multi-agent tool directories...\n")
686
- generate_rule_symlinks()
691
+ info("🔧 Generating multi-agent tool directories...\n")
692
+ rules = generate_rule_symlinks()
687
693
  generate_windsurfrules()
688
694
  generate_gemini_md()
689
- generate_claude_skills()
690
- generate_claude_commands()
691
- generate_persona_symlinks()
692
- print("\n✅ All tool directories generated")
695
+ skills = generate_claude_skills()
696
+ commands = generate_claude_commands()
697
+ personas = generate_persona_symlinks()
698
+ summary = (
699
+ f"✅ generate-tools — rules={rules} skills={skills} "
700
+ f"commands={commands} personas={personas}"
701
+ )
702
+ if resolve_level() == "verbose":
703
+ print(f"\n{summary}")
704
+ else:
705
+ success(summary)
706
+ flush_summary()
693
707
 
694
708
 
695
709
  # ── .augment/ projection ──────────────────────────────────────────────
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ // cost-budget — set / get / check the project's cost budget against
3
+ // accumulated session spend in agents/cost-tracking/sessions.jsonl.
4
+ //
5
+ // Forked from ruvnet/ruflo plugins/ruflo-cost-tracker/scripts/budget.mjs.
6
+ // Local-JSONL swap replaces the upstream `mcp__claude-flow__memory_store`
7
+ // dependency. Budget config lives next to the sessions store as budget.json.
8
+ //
9
+ // Usage: node scripts/cost/budget.mjs {set <usd>|get|check}
10
+ // Env: BUDGET_STORE, BUDGET_CONFIG, BUDGET_PERIOD={today|week|month|all}, BUDGET_QUIET=1
11
+
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
13
+ import { dirname } from 'node:path';
14
+
15
+ const STORE = process.env.BUDGET_STORE || 'agents/cost-tracking/sessions.jsonl';
16
+ const CONFIG = process.env.BUDGET_CONFIG || 'agents/cost-tracking/budget.json';
17
+
18
+ function loadConfig() {
19
+ if (!existsSync(CONFIG)) return null;
20
+ try { return JSON.parse(readFileSync(CONFIG, 'utf-8')); } catch { return null; }
21
+ }
22
+
23
+ function saveConfig(cfg) {
24
+ mkdirSync(dirname(CONFIG), { recursive: true });
25
+ writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
26
+ }
27
+
28
+ function loadSessions() {
29
+ if (!existsSync(STORE)) return [];
30
+ const out = [];
31
+ for (const line of readFileSync(STORE, 'utf-8').split('\n')) {
32
+ if (!line.trim()) continue;
33
+ try { out.push(JSON.parse(line)); } catch { /* skip malformed line */ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function periodFilter(period) {
39
+ const now = Date.now();
40
+ const day = 24 * 3600 * 1000;
41
+ if (period === 'today') return (ts) => ts && new Date(ts).toDateString() === new Date().toDateString();
42
+ if (period === 'week') return (ts) => ts && (now - new Date(ts).getTime()) < 7 * day;
43
+ if (period === 'month') return (ts) => ts && (now - new Date(ts).getTime()) < 30 * day;
44
+ return () => true;
45
+ }
46
+
47
+ function alertLevel(u) {
48
+ if (u >= 1.00) return { level: 'HARD_STOP', emoji: '🛑', threshold: 100 };
49
+ if (u >= 0.90) return { level: 'CRITICAL', emoji: '🔴', threshold: 90 };
50
+ if (u >= 0.75) return { level: 'WARNING', emoji: '🟠', threshold: 75 };
51
+ if (u >= 0.50) return { level: 'INFO', emoji: '🟡', threshold: 50 };
52
+ return { level: 'OK', emoji: '🟢', threshold: 0 };
53
+ }
54
+
55
+ function recommendedAction(level) {
56
+ return ({
57
+ OK: 'within budget — no action.',
58
+ INFO: '50% consumed — log notification, no UX disruption.',
59
+ WARNING: '75% consumed — suggest /set-cost-profile balanced→minimal.',
60
+ CRITICAL: '90% consumed — recommend model downgrades, consider /set-cost-profile minimal.',
61
+ HARD_STOP: '100% consumed — halt non-essential work; review /cost:report before continuing.',
62
+ }[level]);
63
+ }
64
+
65
+ function cmdSet(args) {
66
+ const amount = parseFloat(args[0]);
67
+ if (!Number.isFinite(amount) || amount <= 0) {
68
+ console.error('usage: budget.mjs set <usd-amount> (positive number)');
69
+ process.exit(2);
70
+ }
71
+ const config = {
72
+ budget_usd: amount,
73
+ setAt: new Date().toISOString(),
74
+ thresholds: { info: 0.50, warning: 0.75, critical: 0.90, hard_stop: 1.00 },
75
+ };
76
+ saveConfig(config);
77
+ if (process.env.BUDGET_QUIET === '1') {
78
+ console.log(JSON.stringify(config));
79
+ } else {
80
+ console.log(`✓ Budget set: $${amount.toFixed(2)} (config: ${CONFIG})`);
81
+ console.log(' Alerts: 50% INFO · 75% WARNING · 90% CRITICAL · 100% HARD_STOP');
82
+ }
83
+ }
84
+
85
+ function cmdGet() {
86
+ const cfg = loadConfig();
87
+ if (process.env.BUDGET_QUIET === '1') {
88
+ console.log(JSON.stringify(cfg || { error: 'no budget configured' }));
89
+ return;
90
+ }
91
+ if (!cfg) {
92
+ console.log(`No budget configured (config: ${CONFIG}).`);
93
+ console.log('Set one with: node scripts/cost/budget.mjs set <usd>');
94
+ return;
95
+ }
96
+ console.log(`Budget: $${cfg.budget_usd?.toFixed(2)} (set ${cfg.setAt})`);
97
+ console.log('Thresholds: 50/75/90/100%');
98
+ }
99
+
100
+ function cmdCheck() {
101
+ const cfg = loadConfig();
102
+ const period = process.env.BUDGET_PERIOD || 'all';
103
+ const filt = periodFilter(period);
104
+ const filtered = loadSessions().filter((r) => filt(r.capturedAt || r.endedAt));
105
+ const totalSpend = filtered.reduce((s, r) => s + (r.total_cost_usd || 0), 0);
106
+ if (!cfg || !Number.isFinite(cfg.budget_usd)) {
107
+ const out = { period, totalSpend, recordCount: filtered.length, error: 'no budget configured' };
108
+ if (process.env.BUDGET_QUIET === '1') return console.log(JSON.stringify(out));
109
+ console.log(`Period: ${period}`);
110
+ console.log(`Spent so far: $${totalSpend.toFixed(2)} across ${filtered.length} sessions`);
111
+ console.log('No budget set — run `node scripts/cost/budget.mjs set <usd>` to enable alerts.');
112
+ return;
113
+ }
114
+ const utilization = totalSpend / cfg.budget_usd;
115
+ const alert = alertLevel(utilization);
116
+ const out = {
117
+ period,
118
+ budget_usd: cfg.budget_usd,
119
+ spent_usd: totalSpend,
120
+ remaining_usd: Math.max(0, cfg.budget_usd - totalSpend),
121
+ utilization_pct: utilization * 100,
122
+ level: alert.level,
123
+ threshold: alert.threshold,
124
+ recommended_action: recommendedAction(alert.level),
125
+ sessionCount: filtered.length,
126
+ };
127
+ if (process.env.BUDGET_QUIET === '1') return console.log(JSON.stringify(out));
128
+ console.log(`# Budget check (period: ${period})\n`);
129
+ console.log('| Metric | Value |\n|---|---:|');
130
+ console.log(`| Budget | $${cfg.budget_usd.toFixed(2)} |`);
131
+ console.log(`| Spent | $${totalSpend.toFixed(2)} |`);
132
+ console.log(`| Remaining | $${out.remaining_usd.toFixed(2)} |`);
133
+ console.log(`| Utilization | ${out.utilization_pct.toFixed(1)}% |`);
134
+ console.log(`| Sessions counted | ${filtered.length} |`);
135
+ console.log(`| **Alert** | **${alert.emoji} ${alert.level}** |`);
136
+ console.log(`\nAction: ${out.recommended_action}`);
137
+ if (alert.level === 'HARD_STOP') process.exit(1);
138
+ }
139
+
140
+ function main() {
141
+ const [cmd, ...rest] = process.argv.slice(2);
142
+ switch (cmd) {
143
+ case 'set': return cmdSet(rest);
144
+ case 'get': return cmdGet();
145
+ case 'check': return cmdCheck();
146
+ default:
147
+ console.error('usage: budget.mjs {set <usd>|get|check}');
148
+ process.exit(2);
149
+ }
150
+ }
151
+
152
+ main();
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // cost-track — auto-capture token usage from a Claude Code session jsonl
3
+ // and append a structured record to agents/cost-tracking/sessions.jsonl.
4
+ //
5
+ // Forked from ruvnet/ruflo plugins/ruflo-cost-tracker/scripts/track.mjs.
6
+ // Local-JSONL swap replaces the upstream `mcp__claude-flow__memory_store`
7
+ // dependency. Pricing constants are kept in sync with REFERENCE.md.
8
+ //
9
+ // Env:
10
+ // TRACK_CWD=<path> override which project's sessions to scan
11
+ // TRACK_SESSION=<file> pin to a specific session jsonl
12
+ // TRACK_OUT=<path> also write the JSON summary to this path
13
+ // TRACK_DRY_RUN=1 skip the JSONL append
14
+ // TRACK_QUIET=1 suppress markdown summary
15
+ // TRACK_STORE=<path> override (default: agents/cost-tracking/sessions.jsonl)
16
+
17
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
18
+ import { join, dirname } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+
21
+ const PROJECTS_DIR = join(homedir(), '.claude', 'projects');
22
+ const DEFAULT_STORE = 'agents/cost-tracking/sessions.jsonl';
23
+
24
+ // USD per 1M tokens.
25
+ const PRICING = {
26
+ haiku: { input: 0.25, output: 1.25, cache_write: 0.30, cache_read: 0.03 },
27
+ sonnet: { input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30 },
28
+ opus: { input: 15.00, output: 75.00, cache_write: 18.75, cache_read: 1.50 },
29
+ };
30
+
31
+ function modelTier(model) {
32
+ if (!model) return 'unknown';
33
+ const m = String(model).toLowerCase();
34
+ if (m.includes('haiku')) return 'haiku';
35
+ if (m.includes('sonnet')) return 'sonnet';
36
+ if (m.includes('opus')) return 'opus';
37
+ return 'unknown';
38
+ }
39
+
40
+ function costForUsage(tier, u) {
41
+ const p = PRICING[tier];
42
+ if (!p || !u) return 0;
43
+ return (u.input_tokens || 0) / 1e6 * p.input
44
+ + (u.output_tokens || 0) / 1e6 * p.output
45
+ + (u.cache_creation_input_tokens || 0) / 1e6 * p.cache_write
46
+ + (u.cache_read_input_tokens || 0) / 1e6 * p.cache_read;
47
+ }
48
+
49
+ function encodeProjectPath(cwd) { return cwd.replace(/\//g, '-'); }
50
+
51
+ function findProjectDir(cwd) {
52
+ const c = join(PROJECTS_DIR, encodeProjectPath(cwd));
53
+ return existsSync(c) ? c : null;
54
+ }
55
+
56
+ function findActiveSession(dir) {
57
+ const e = readdirSync(dir).filter((f) => f.endsWith('.jsonl'))
58
+ .map((f) => ({ f, mtime: statSync(join(dir, f)).mtimeMs }))
59
+ .sort((a, b) => b.mtime - a.mtime);
60
+ return e[0] ? join(dir, e[0].f) : null;
61
+ }
62
+
63
+ function summarizeSession(jsonlPath) {
64
+ const lines = readFileSync(jsonlPath, 'utf-8').split('\n').filter(Boolean);
65
+ const byModel = {};
66
+ const byTier = { haiku: 0, sonnet: 0, opus: 0, unknown: 0 };
67
+ let messageCount = 0, totalCost = 0, firstTs = null, lastTs = null;
68
+ let sessionId = null, cwd = null;
69
+ for (const line of lines) {
70
+ let m; try { m = JSON.parse(line); } catch { continue; }
71
+ if (!sessionId && m.sessionId) sessionId = m.sessionId;
72
+ if (!cwd && m.cwd) cwd = m.cwd;
73
+ if (m.timestamp) {
74
+ if (!firstTs || m.timestamp < firstTs) firstTs = m.timestamp;
75
+ if (!lastTs || m.timestamp > lastTs) lastTs = m.timestamp;
76
+ }
77
+ if (m.type !== 'assistant' || !m.message?.usage) continue;
78
+ messageCount++;
79
+ const model = m.message.model || 'unknown';
80
+ const tier = modelTier(model);
81
+ const u = m.message.usage;
82
+ const cost = costForUsage(tier, u);
83
+ const slot = byModel[model] || { tier, input_tokens: 0, output_tokens: 0,
84
+ cache_creation_input_tokens: 0, cache_read_input_tokens: 0, messages: 0, cost_usd: 0 };
85
+ slot.input_tokens += u.input_tokens || 0;
86
+ slot.output_tokens += u.output_tokens || 0;
87
+ slot.cache_creation_input_tokens += u.cache_creation_input_tokens || 0;
88
+ slot.cache_read_input_tokens += u.cache_read_input_tokens || 0;
89
+ slot.messages++; slot.cost_usd += cost;
90
+ byModel[model] = slot; byTier[tier] += cost; totalCost += cost;
91
+ }
92
+ return { sessionId, cwd, startedAt: firstTs, endedAt: lastTs, messageCount,
93
+ byModel, byTier, total_cost_usd: totalCost, capturedAt: new Date().toISOString() };
94
+ }
95
+
96
+ function persistJsonl(summary, store) {
97
+ mkdirSync(dirname(store), { recursive: true });
98
+ appendFileSync(store, JSON.stringify(summary) + '\n');
99
+ return { ok: true, path: store };
100
+ }
101
+
102
+ function main() {
103
+ const targetCwd = process.env.TRACK_CWD || process.cwd();
104
+ const projectDir = findProjectDir(targetCwd);
105
+ if (!projectDir) {
106
+ console.error(`cost-track: no Claude Code project dir for cwd=${targetCwd}`);
107
+ console.error(`looked under ${PROJECTS_DIR}/${encodeProjectPath(targetCwd)}`);
108
+ process.exit(2);
109
+ }
110
+ const sessionPath = process.env.TRACK_SESSION || findActiveSession(projectDir);
111
+ if (!sessionPath || !existsSync(sessionPath)) {
112
+ console.error(`cost-track: no session jsonl in ${projectDir}`); process.exit(2);
113
+ }
114
+ const summary = summarizeSession(sessionPath);
115
+ if (process.env.TRACK_OUT) writeFileSync(process.env.TRACK_OUT, JSON.stringify(summary, null, 2));
116
+ const store = process.env.TRACK_STORE || DEFAULT_STORE;
117
+ let res = { ok: false, reason: 'dry-run' };
118
+ if (process.env.TRACK_DRY_RUN !== '1') res = persistJsonl(summary, store);
119
+ if (process.env.TRACK_QUIET === '1') return;
120
+
121
+ console.log(`# cost-track — session ${(summary.sessionId || '').slice(0, 8) || 'unknown'}`);
122
+ console.log('');
123
+ console.log('| Metric | Value |\n|---|---:|');
124
+ console.log(`| Session ID | \`${summary.sessionId}\` |`);
125
+ console.log(`| Project | \`${summary.cwd}\` |`);
126
+ console.log(`| First message | ${summary.startedAt} |`);
127
+ console.log(`| Last message | ${summary.endedAt} |`);
128
+ console.log(`| Assistant messages | ${summary.messageCount} |`);
129
+ console.log(`| **Total cost** | **$${summary.total_cost_usd.toFixed(6)}** |`);
130
+ console.log(`| Persisted | ${res.ok ? `\`${res.path}\`` : `**FAILED** (${res.reason})`} |`);
131
+ console.log('\n## Per-model breakdown\n');
132
+ console.log('| Model | Tier | Messages | Input | Output | Cache write | Cache read | Cost |');
133
+ console.log('|---|---|---:|---:|---:|---:|---:|---:|');
134
+ for (const [m, s] of Object.entries(summary.byModel).sort((a, b) => b[1].cost_usd - a[1].cost_usd)) {
135
+ console.log(`| \`${m}\` | ${s.tier} | ${s.messages} | ${s.input_tokens} | ${s.output_tokens} | ${s.cache_creation_input_tokens} | ${s.cache_read_input_tokens} | $${s.cost_usd.toFixed(6)} |`);
136
+ }
137
+ console.log('\n## Per-tier breakdown\n');
138
+ console.log('| Tier | Cost |\n|---|---:|');
139
+ for (const [t, c] of Object.entries(summary.byTier).sort((a, b) => b[1] - a[1])) {
140
+ if (c > 0) console.log(`| ${t} | $${c.toFixed(6)} |`);
141
+ }
142
+ }
143
+
144
+ main();
@@ -5,9 +5,7 @@ SETTINGS_FILE=".agent-settings.yml"
5
5
  LEGACY_SETTINGS_FILE=".agent-settings"
6
6
 
7
7
  echo ""
8
- echo "========================================"
9
- echo " Agent Config — First Run"
10
- echo "========================================"
8
+ echo "Agent Config — First Run"
11
9
  echo ""
12
10
 
13
11
  # --- Profile detection ---
@@ -63,9 +61,7 @@ echo " ✅ Zero token overhead in minimal mode"
63
61
  echo ""
64
62
 
65
63
  # --- 3 test prompts ---
66
- echo "========================================"
67
- echo " Try these 3 prompts now"
68
- echo "========================================"
64
+ echo "Try these 3 prompts now:"
69
65
  echo ""
70
66
 
71
67
  echo "1️⃣ Refactoring check"
@@ -94,9 +90,7 @@ echo " → Agent challenges weak requirements"
94
90
  echo ""
95
91
 
96
92
  # --- Next steps ---
97
- echo "========================================"
98
- echo " Next steps"
99
- echo "========================================"
93
+ echo "Next steps:"
100
94
  echo ""
101
95
  echo "Cost profiles:"
102
96
  echo " minimal rules, skills, commands only"