@event4u/agent-config 2.19.0 → 2.20.1
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 +29 -0
- package/.agent-src/commands/onboard.md +221 -81
- package/.agent-src/packs/README.md +49 -0
- package/.agent-src/packs/agency-delivery.yml +63 -0
- package/.agent-src/packs/content-engine.yml +53 -0
- package/.agent-src/packs/founder-mvp.yml +51 -0
- package/.agent-src/presets/README.md +26 -0
- package/.agent-src/presets/balanced.yml +34 -0
- package/.agent-src/presets/fast.yml +31 -0
- package/.agent-src/presets/strict.yml +38 -0
- package/.agent-src/profiles/README.md +29 -0
- package/.agent-src/profiles/agency.yml +27 -0
- package/.agent-src/profiles/content_creator.yml +25 -0
- package/.agent-src/profiles/developer.yml +26 -0
- package/.agent-src/profiles/finance.yml +24 -0
- package/.agent-src/profiles/founder.yml +25 -0
- package/.agent-src/profiles/ops.yml +25 -0
- package/.agent-src/rules/no-cheap-questions.md +25 -17
- package/.agent-src/skills/adr-create/SKILL.md +78 -68
- package/.agent-src/skills/subagent-orchestration/SKILL.md +33 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/skill-archive-note.md +101 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +73 -70
- package/README.md +68 -72
- package/config/agent-settings.template.yml +22 -0
- package/docs/adrs/caveman/0001-default-off-until-bench.md +93 -0
- package/docs/adrs/caveman/README.md +9 -0
- package/docs/adrs/cost/0001-hard-stop-hook.md +114 -0
- package/docs/adrs/cost/README.md +9 -0
- package/docs/adrs/memory/0001-consumer-side-snapshot.md +111 -0
- package/docs/adrs/memory/README.md +9 -0
- package/docs/adrs/router/0001-three-tier-routing.md +119 -0
- package/docs/adrs/router/README.md +9 -0
- package/docs/adrs/schema/0001-json-schema-frontmatter.md +102 -0
- package/docs/adrs/schema/README.md +9 -0
- package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +99 -0
- package/docs/adrs/smoke/README.md +9 -0
- package/docs/architecture/current-onboard-baseline.md +126 -0
- package/docs/architecture/current-safety-behavior.md +137 -0
- package/docs/archive/CHANGELOG-pre-2.16.0.md +48 -0
- package/docs/archive/CHANGELOG-pre-2.17.0.md +63 -0
- package/docs/contracts/adr-layout.md +108 -0
- package/docs/contracts/benchmark-corpus-spec.md +97 -0
- package/docs/contracts/benchmark-report-schema.md +111 -0
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/command-taxonomy.md +137 -0
- package/docs/contracts/compression-default-kill-criterion.md +69 -0
- package/docs/contracts/config-presets.md +144 -0
- package/docs/contracts/cost-dashboard.md +143 -0
- package/docs/contracts/cost-enforcement.md +134 -0
- package/docs/contracts/file-ownership-matrix.json +0 -7
- package/docs/contracts/mcp-tool-inventory.md +53 -0
- package/docs/contracts/measurement-baseline.md +102 -0
- package/docs/contracts/namespace.md +125 -0
- package/docs/contracts/profile-system.md +142 -0
- package/docs/contracts/safety-model.md +129 -0
- package/docs/contracts/smoke-contracts.md +144 -0
- package/docs/contracts/workflow-packs.md +121 -0
- package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +132 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/featured-commands.md +27 -0
- package/docs/parity/bench-ruflo.json +58 -0
- package/docs/parity/bench.json +41 -0
- package/docs/parity/ruflo.md +46 -0
- package/docs/profiles.md +91 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_explain.py +250 -0
- package/scripts/_lib/bench_cost.py +138 -0
- package/scripts/_lib/bench_quality.py +118 -0
- package/scripts/_lib/bench_report.py +150 -0
- package/scripts/agent-config +13 -0
- package/scripts/audit_adr_coverage.py +175 -0
- package/scripts/audit_mcp_tools.py +146 -0
- package/scripts/bench_baseline_ready.py +108 -0
- package/scripts/bench_drift_check.py +151 -0
- package/scripts/bench_per_tool.py +216 -0
- package/scripts/bench_run.py +155 -0
- package/scripts/config/__init__.py +9 -0
- package/scripts/config/presets.py +206 -0
- package/scripts/config/profiles.py +173 -0
- package/scripts/cost/budget.mjs +73 -12
- package/scripts/cost/preflight.mjs +89 -0
- package/scripts/lint_archived_skills.py +143 -0
- package/scripts/lint_bench_corpus.py +161 -0
- package/scripts/lint_namespace.py +135 -0
- package/scripts/lint_roadmap_complexity.py +3 -2
- package/scripts/skill_overlap.py +204 -0
- package/scripts/skill_usage_collect.py +191 -0
- package/scripts/skill_usage_report.py +162 -0
- package/scripts/smoke/kernel.sh +101 -0
- package/scripts/smoke/router.sh +129 -0
- package/scripts/smoke/schema.sh +71 -0
- package/scripts/smoke/skills.sh +101 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Collect skill-activation signal from Claude Code session jsonl.
|
|
3
|
+
|
|
4
|
+
Implements step-2-skill-inventory-rationalization.md Phase 1 Step 2.
|
|
5
|
+
Reads `~/.claude/projects/<project-slug>/*.jsonl` for the current repo,
|
|
6
|
+
parses each turn for two signals:
|
|
7
|
+
|
|
8
|
+
- exposure: the skill slug appeared in an `attachment.type=skill_listing`
|
|
9
|
+
payload (catalog presented to the agent that turn).
|
|
10
|
+
- mention: the assistant-text response in the same or following turn
|
|
11
|
+
referenced the slug in backticks with one of the anchor verbs
|
|
12
|
+
(using, via, per, route, dispatch, invoke, call) OR cited a SKILL.md
|
|
13
|
+
path under `.augment/skills/<slug>/`, `.claude/skills/<slug>/`, or
|
|
14
|
+
`.agent-src/skills/<slug>/`.
|
|
15
|
+
|
|
16
|
+
Emits one JSONL record per (session, turn, slug, kind) to
|
|
17
|
+
`agents/metrics/skill-usage.jsonl` (append-only, deduped on the
|
|
18
|
+
(session_id, turn_idx, slug, kind) tuple).
|
|
19
|
+
|
|
20
|
+
Privacy: `prompt_excerpt_hash` = SHA-256 of the first 200 chars of the
|
|
21
|
+
user prompt that opened the turn. No raw user or assistant bodies are
|
|
22
|
+
persisted. See `agents/audit-2026-05-14-north-star/skill-usage-sources.md`.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import hashlib
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Iterable, Iterator
|
|
33
|
+
|
|
34
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
35
|
+
OUT = REPO / "agents" / "metrics" / "skill-usage.jsonl"
|
|
36
|
+
|
|
37
|
+
LISTING_LINE_RE = re.compile(r"^-\s+([a-z0-9][a-z0-9_-]+):\s", re.MULTILINE)
|
|
38
|
+
ANCHOR_VERBS = ("using", "via", "per", "route", "routing", "dispatch", "dispatched", "invoke", "call")
|
|
39
|
+
PATH_RE = re.compile(r"\.(?:augment|claude|agent-src)/skills/([a-z0-9][a-z0-9_-]+)/SKILL\.md")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def project_slug(repo: Path) -> str:
|
|
43
|
+
return str(repo).replace("/", "-")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def session_files(slug: str) -> list[Path]:
|
|
47
|
+
base = Path.home() / ".claude" / "projects" / slug
|
|
48
|
+
if not base.is_dir():
|
|
49
|
+
return []
|
|
50
|
+
return sorted(base.glob("*.jsonl"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def iter_turns(jsonl: Path) -> Iterator[dict]:
|
|
54
|
+
with jsonl.open("r", encoding="utf-8", errors="replace") as fh:
|
|
55
|
+
for line in fh:
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if not line:
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
yield json.loads(line)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def extract_listing(entry: dict) -> set[str]:
|
|
66
|
+
att = entry.get("attachment") or {}
|
|
67
|
+
if att.get("type") != "skill_listing":
|
|
68
|
+
return set()
|
|
69
|
+
content = att.get("content", "") or ""
|
|
70
|
+
return set(LISTING_LINE_RE.findall(content))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_text(entry: dict) -> str:
|
|
74
|
+
if entry.get("type") != "assistant":
|
|
75
|
+
return ""
|
|
76
|
+
msg = entry.get("message") or {}
|
|
77
|
+
content = msg.get("content")
|
|
78
|
+
if isinstance(content, str):
|
|
79
|
+
return content
|
|
80
|
+
if isinstance(content, list):
|
|
81
|
+
return "\n".join(p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text")
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_mentions(text: str, known_slugs: Iterable[str]) -> set[str]:
|
|
86
|
+
hits: set[str] = set()
|
|
87
|
+
if not text:
|
|
88
|
+
return hits
|
|
89
|
+
hits.update(PATH_RE.findall(text))
|
|
90
|
+
for slug in known_slugs:
|
|
91
|
+
token = f"`{slug}`"
|
|
92
|
+
if token not in text:
|
|
93
|
+
continue
|
|
94
|
+
lower = text.lower()
|
|
95
|
+
for verb in ANCHOR_VERBS:
|
|
96
|
+
if f"{verb} {token}".lower() in lower or f"{verb} the {token}".lower() in lower:
|
|
97
|
+
hits.add(slug)
|
|
98
|
+
break
|
|
99
|
+
return hits
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def hash_prompt(text: str) -> str:
|
|
103
|
+
if not text:
|
|
104
|
+
return ""
|
|
105
|
+
return hashlib.sha256(text[:200].encode("utf-8", errors="replace")).hexdigest()[:16]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def collect_session(jsonl: Path, all_known: set[str]) -> list[dict]:
|
|
109
|
+
session_id = jsonl.stem
|
|
110
|
+
records: list[dict] = []
|
|
111
|
+
last_prompt_hash = ""
|
|
112
|
+
listed: set[str] = set()
|
|
113
|
+
turn_idx = -1
|
|
114
|
+
for entry in iter_turns(jsonl):
|
|
115
|
+
etype = entry.get("type")
|
|
116
|
+
if etype == "user":
|
|
117
|
+
turn_idx += 1
|
|
118
|
+
msg = entry.get("message") or {}
|
|
119
|
+
body = msg.get("content") if isinstance(msg.get("content"), str) else ""
|
|
120
|
+
last_prompt_hash = hash_prompt(body or "")
|
|
121
|
+
continue
|
|
122
|
+
if etype == "attachment":
|
|
123
|
+
listed |= extract_listing(entry)
|
|
124
|
+
continue
|
|
125
|
+
if etype == "assistant":
|
|
126
|
+
text = extract_text(entry)
|
|
127
|
+
mentions = find_mentions(text, listed | all_known)
|
|
128
|
+
ts = entry.get("timestamp") or ""
|
|
129
|
+
for slug in sorted(listed):
|
|
130
|
+
records.append({"session_id": session_id, "turn_idx": turn_idx, "slug": slug,
|
|
131
|
+
"kind": "exposure", "ts": ts, "prompt_excerpt_hash": last_prompt_hash})
|
|
132
|
+
for slug in sorted(mentions):
|
|
133
|
+
records.append({"session_id": session_id, "turn_idx": turn_idx, "slug": slug,
|
|
134
|
+
"kind": "mention", "ts": ts, "prompt_excerpt_hash": last_prompt_hash})
|
|
135
|
+
listed = set()
|
|
136
|
+
return records
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_known_slugs(repo: Path) -> set[str]:
|
|
140
|
+
slugs: set[str] = set()
|
|
141
|
+
for root in (repo / ".augment" / "skills", repo / ".claude" / "skills", repo / ".agent-src" / "skills"):
|
|
142
|
+
if not root.is_dir():
|
|
143
|
+
continue
|
|
144
|
+
for skill_md in root.glob("*/SKILL.md"):
|
|
145
|
+
slugs.add(skill_md.parent.name)
|
|
146
|
+
return slugs
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def dedup_key(rec: dict) -> tuple:
|
|
150
|
+
return (rec["session_id"], rec["turn_idx"], rec["slug"], rec["kind"])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main() -> int:
|
|
154
|
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
155
|
+
ap.add_argument("--project-slug", help="Override the ~/.claude/projects slug (defaults to current repo)")
|
|
156
|
+
ap.add_argument("--out", type=Path, default=OUT, help="Output jsonl (default: agents/metrics/skill-usage.jsonl)")
|
|
157
|
+
ap.add_argument("--quiet", action="store_true", help="Suppress non-error output")
|
|
158
|
+
args = ap.parse_args()
|
|
159
|
+
|
|
160
|
+
slug = args.project_slug or project_slug(REPO)
|
|
161
|
+
files = session_files(slug)
|
|
162
|
+
if not files:
|
|
163
|
+
if not args.quiet:
|
|
164
|
+
print(f"no session files for slug {slug}", file=sys.stderr)
|
|
165
|
+
return 0
|
|
166
|
+
known = load_known_slugs(REPO)
|
|
167
|
+
seen: set[tuple] = set()
|
|
168
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
if args.out.exists():
|
|
170
|
+
for line in args.out.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
171
|
+
try:
|
|
172
|
+
seen.add(dedup_key(json.loads(line)))
|
|
173
|
+
except (json.JSONDecodeError, KeyError):
|
|
174
|
+
continue
|
|
175
|
+
appended = 0
|
|
176
|
+
with args.out.open("a", encoding="utf-8") as fh:
|
|
177
|
+
for jsonl in files:
|
|
178
|
+
for rec in collect_session(jsonl, known):
|
|
179
|
+
k = dedup_key(rec)
|
|
180
|
+
if k in seen:
|
|
181
|
+
continue
|
|
182
|
+
seen.add(k)
|
|
183
|
+
fh.write(json.dumps(rec, separators=(",", ":")) + "\n")
|
|
184
|
+
appended += 1
|
|
185
|
+
if not args.quiet:
|
|
186
|
+
print(f"✅ Wrote {appended} new record(s) to {args.out.relative_to(REPO)} ({len(seen)} total)")
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Aggregate `agents/metrics/skill-usage.jsonl` into a per-skill report.
|
|
3
|
+
|
|
4
|
+
Implements step-2-skill-inventory-rationalization.md Phase 1 Step 3.
|
|
5
|
+
Groups records by slug; emits `agents/metrics/skill-usage-report.md`
|
|
6
|
+
with columns:
|
|
7
|
+
|
|
8
|
+
slug · exposures_total · mentions_total · exposures_30d · mentions_30d
|
|
9
|
+
· last_seen · status
|
|
10
|
+
|
|
11
|
+
`status` ∈ { active, exposed-only, dead } per:
|
|
12
|
+
|
|
13
|
+
active = mentions_30d ≥ 1
|
|
14
|
+
exposed-only = exposures_30d ≥ 1 ∧ mentions_30d == 0
|
|
15
|
+
dead = exposures_30d == 0
|
|
16
|
+
|
|
17
|
+
The report is **a baseline, not a verdict**. Rationalization decisions
|
|
18
|
+
live in Phase 2 (`skill-rationalization-candidates.md`).
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
from datetime import datetime, timedelta, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
29
|
+
IN = REPO / "agents" / "metrics" / "skill-usage.jsonl"
|
|
30
|
+
OUT = REPO / "agents" / "metrics" / "skill-usage-report.md"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_ts(raw: str) -> datetime | None:
|
|
34
|
+
if not raw:
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
return datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
38
|
+
except ValueError:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_records(path: Path) -> list[dict]:
|
|
43
|
+
if not path.exists():
|
|
44
|
+
return []
|
|
45
|
+
records: list[dict] = []
|
|
46
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
47
|
+
if not line.strip():
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
records.append(json.loads(line))
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
continue
|
|
53
|
+
return records
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def aggregate(records: list[dict], now: datetime, window_days: int = 30) -> dict[str, dict]:
|
|
57
|
+
cutoff = now - timedelta(days=window_days)
|
|
58
|
+
per: dict[str, dict] = defaultdict(lambda: {
|
|
59
|
+
"exposures_total": 0, "mentions_total": 0,
|
|
60
|
+
"exposures_30d": 0, "mentions_30d": 0,
|
|
61
|
+
"last_seen": None,
|
|
62
|
+
})
|
|
63
|
+
for rec in records:
|
|
64
|
+
slug = rec.get("slug")
|
|
65
|
+
kind = rec.get("kind")
|
|
66
|
+
if not slug or kind not in ("exposure", "mention"):
|
|
67
|
+
continue
|
|
68
|
+
ts = parse_ts(rec.get("ts") or "")
|
|
69
|
+
bucket = per[slug]
|
|
70
|
+
bucket[f"{kind}s_total"] += 1
|
|
71
|
+
if ts and ts >= cutoff:
|
|
72
|
+
bucket[f"{kind}s_30d"] += 1
|
|
73
|
+
if ts and (bucket["last_seen"] is None or ts > bucket["last_seen"]):
|
|
74
|
+
bucket["last_seen"] = ts
|
|
75
|
+
return per
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def status_for(row: dict) -> str:
|
|
79
|
+
if row["mentions_30d"] >= 1:
|
|
80
|
+
return "active"
|
|
81
|
+
if row["exposures_30d"] >= 1:
|
|
82
|
+
return "exposed-only"
|
|
83
|
+
return "dead"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def all_known_slugs(repo: Path) -> set[str]:
|
|
87
|
+
slugs: set[str] = set()
|
|
88
|
+
for root in (repo / ".augment" / "skills", repo / ".claude" / "skills", repo / ".agent-src" / "skills"):
|
|
89
|
+
if not root.is_dir():
|
|
90
|
+
continue
|
|
91
|
+
for skill_md in root.glob("*/SKILL.md"):
|
|
92
|
+
slugs.add(skill_md.parent.name)
|
|
93
|
+
return slugs
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def render(per: dict[str, dict], known: set[str]) -> str:
|
|
97
|
+
rows = []
|
|
98
|
+
for slug in sorted(known | set(per)):
|
|
99
|
+
data = per.get(slug, {
|
|
100
|
+
"exposures_total": 0, "mentions_total": 0,
|
|
101
|
+
"exposures_30d": 0, "mentions_30d": 0, "last_seen": None,
|
|
102
|
+
})
|
|
103
|
+
rows.append({"slug": slug, **data, "status": status_for(data)})
|
|
104
|
+
rows.sort(key=lambda r: (r["status"] != "dead", -r["exposures_total"], r["slug"]))
|
|
105
|
+
|
|
106
|
+
counts = {"active": 0, "exposed-only": 0, "dead": 0}
|
|
107
|
+
for r in rows:
|
|
108
|
+
counts[r["status"]] += 1
|
|
109
|
+
total = len(rows)
|
|
110
|
+
|
|
111
|
+
lines = [
|
|
112
|
+
"# Skill Usage Report (baseline)",
|
|
113
|
+
"",
|
|
114
|
+
"> Generated by `scripts/skill_usage_report.py`. Source:",
|
|
115
|
+
"> `agents/metrics/skill-usage.jsonl` (collector emits per-turn",
|
|
116
|
+
"> exposure/mention records). See",
|
|
117
|
+
"> [`step-2-skill-inventory-rationalization.md`](../roadmaps/step-2-skill-inventory-rationalization.md)",
|
|
118
|
+
"> Phase 1.",
|
|
119
|
+
"",
|
|
120
|
+
f"**Window:** 30-day rolling \u00b7 **Skills tracked:** {total} \u00b7 "
|
|
121
|
+
f"**Active:** {counts['active']} \u00b7 **Exposed-only:** {counts['exposed-only']} \u00b7 "
|
|
122
|
+
f"**Dead:** {counts['dead']}",
|
|
123
|
+
"",
|
|
124
|
+
"| # | slug | status | exposures_30d | mentions_30d | exposures_total | mentions_total | last_seen |",
|
|
125
|
+
"|---|---|---|---|---|---|---|---|",
|
|
126
|
+
]
|
|
127
|
+
for i, r in enumerate(rows, 1):
|
|
128
|
+
last = r["last_seen"].date().isoformat() if r["last_seen"] else "\u2014"
|
|
129
|
+
lines.append(
|
|
130
|
+
f"| {i} | `{r['slug']}` | {r['status']} | {r['exposures_30d']} | "
|
|
131
|
+
f"{r['mentions_30d']} | {r['exposures_total']} | {r['mentions_total']} | {last} |"
|
|
132
|
+
)
|
|
133
|
+
lines.append("")
|
|
134
|
+
lines.append("**Read-out:** rows tagged `dead` are first-cut archive candidates; "
|
|
135
|
+
"rows tagged `exposed-only` are first-cut merge / rename candidates "
|
|
136
|
+
"(catalog noise, agent never invokes). Phase 2 confirms with "
|
|
137
|
+
"structural overlap before any deletion.")
|
|
138
|
+
lines.append("")
|
|
139
|
+
return "\n".join(lines)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> int:
|
|
143
|
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
144
|
+
ap.add_argument("--in", dest="inp", type=Path, default=IN)
|
|
145
|
+
ap.add_argument("--out", type=Path, default=OUT)
|
|
146
|
+
ap.add_argument("--window", type=int, default=30, help="Rolling window in days")
|
|
147
|
+
ap.add_argument("--quiet", action="store_true")
|
|
148
|
+
args = ap.parse_args()
|
|
149
|
+
|
|
150
|
+
records = load_records(args.inp)
|
|
151
|
+
now = datetime.now(timezone.utc)
|
|
152
|
+
per = aggregate(records, now, args.window)
|
|
153
|
+
known = all_known_slugs(REPO)
|
|
154
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
args.out.write_text(render(per, known), encoding="utf-8")
|
|
156
|
+
if not args.quiet:
|
|
157
|
+
print(f"\u2705 Wrote {args.out.relative_to(REPO)} ({len(known | set(per))} skill(s))")
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/smoke/kernel.sh — kernel-tier smoke (step-11 Phase 3 Step 2).
|
|
3
|
+
#
|
|
4
|
+
# Asserts:
|
|
5
|
+
# 1. router.json lists exactly 9 kernel rules.
|
|
6
|
+
# 2. Every kernel rule file exists at .agent-src/rules/<id>.md.
|
|
7
|
+
# 3. 8 of 9 carry at least one Iron-Law fenced block.
|
|
8
|
+
# agent-authority is the dispatch index, exempt from the fence
|
|
9
|
+
# requirement (docs/contracts/smoke-contracts.md § 3.1).
|
|
10
|
+
# 4. Kernel-bucket char budget breaches ≤ EXPECTED_BREACHES.
|
|
11
|
+
#
|
|
12
|
+
# Runtime ceiling: 30 s.
|
|
13
|
+
# Output: table by default, baseline line on stdout last; SMOKE_QUIET=1
|
|
14
|
+
# suppresses the table.
|
|
15
|
+
# Contract: docs/contracts/smoke-contracts.md
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
EXPECTED_KERNEL_COUNT=9
|
|
20
|
+
EXPECTED_FENCE_CARRIERS=8
|
|
21
|
+
EXPECTED_BREACHES=2
|
|
22
|
+
EXEMPT_FROM_FENCE="agent-authority"
|
|
23
|
+
|
|
24
|
+
quiet="${SMOKE_QUIET:-0}"
|
|
25
|
+
fail=0
|
|
26
|
+
|
|
27
|
+
log() { [ "$quiet" = "1" ] || printf '%s\n' "$*"; }
|
|
28
|
+
|
|
29
|
+
# 1. kernel ids from router.json
|
|
30
|
+
kernel_ids=$(python3 -c '
|
|
31
|
+
import json
|
|
32
|
+
d = json.load(open("router.json"))
|
|
33
|
+
print("\n".join(d.get("kernel", [])))
|
|
34
|
+
')
|
|
35
|
+
kernel_count=$(printf '%s\n' "$kernel_ids" | grep -c .)
|
|
36
|
+
|
|
37
|
+
log "## Kernel smoke"
|
|
38
|
+
log ""
|
|
39
|
+
log "| Check | Value |"
|
|
40
|
+
log "|---|---:|"
|
|
41
|
+
log "| router.json kernel count | $kernel_count |"
|
|
42
|
+
|
|
43
|
+
if [ "$kernel_count" -ne "$EXPECTED_KERNEL_COUNT" ]; then
|
|
44
|
+
echo "❌ kernel count: $kernel_count (expected $EXPECTED_KERNEL_COUNT)"
|
|
45
|
+
fail=1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# 2. every kernel rule has a file
|
|
49
|
+
missing=0
|
|
50
|
+
for id in $kernel_ids; do
|
|
51
|
+
if [ ! -f ".agent-src/rules/$id.md" ]; then
|
|
52
|
+
echo "❌ missing rule file: .agent-src/rules/$id.md"
|
|
53
|
+
missing=$((missing + 1))
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
log "| Rule files present | $((kernel_count - missing))/$kernel_count |"
|
|
57
|
+
if [ "$missing" -gt 0 ]; then fail=1; fi
|
|
58
|
+
|
|
59
|
+
# 3. count Iron-Law fences per rule
|
|
60
|
+
fence_carriers=0
|
|
61
|
+
for id in $kernel_ids; do
|
|
62
|
+
if printf ' %s ' "$EXEMPT_FROM_FENCE" | grep -q " $id "; then
|
|
63
|
+
continue
|
|
64
|
+
fi
|
|
65
|
+
if [ -f ".agent-src/rules/$id.md" ]; then
|
|
66
|
+
fences=$(awk 'BEGIN{c=0;open=0} /^```/{ if(open==0){c++;open=1}else{open=0} } END{print c}' ".agent-src/rules/$id.md")
|
|
67
|
+
if [ "$fences" -ge 1 ]; then
|
|
68
|
+
fence_carriers=$((fence_carriers + 1))
|
|
69
|
+
else
|
|
70
|
+
echo "❌ no Iron-Law fence in .agent-src/rules/$id.md"
|
|
71
|
+
fail=1
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
log "| Iron-Law fence carriers | $fence_carriers/$((kernel_count - 1)) |"
|
|
76
|
+
|
|
77
|
+
if [ "$fence_carriers" -lt "$EXPECTED_FENCE_CARRIERS" ]; then
|
|
78
|
+
echo "❌ fence carriers: $fence_carriers (expected $EXPECTED_FENCE_CARRIERS)"
|
|
79
|
+
fail=1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# 4. kernel char-budget breach count (advisory: locked at current measured)
|
|
83
|
+
breach_count=0
|
|
84
|
+
if python3 scripts/measure_rule_budget.py --kernel-budget-check >/tmp/kernel-budget.$$ 2>&1; then
|
|
85
|
+
breach_count=0
|
|
86
|
+
else
|
|
87
|
+
breach_count=$(grep -c "^ - " /tmp/kernel-budget.$$ || true)
|
|
88
|
+
fi
|
|
89
|
+
rm -f /tmp/kernel-budget.$$
|
|
90
|
+
log "| Kernel-budget breaches | $breach_count (locked ≤ $EXPECTED_BREACHES) |"
|
|
91
|
+
|
|
92
|
+
if [ "$breach_count" -gt "$EXPECTED_BREACHES" ]; then
|
|
93
|
+
echo "❌ kernel budget breaches: $breach_count > $EXPECTED_BREACHES (regression)"
|
|
94
|
+
fail=1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Baseline line — last line of stdout for CI summary parsing.
|
|
98
|
+
log ""
|
|
99
|
+
echo "BASELINE: $kernel_count kernel rules · $fence_carriers carry Iron-Law fences · 1 dispatch index · $breach_count budget breach(es)"
|
|
100
|
+
|
|
101
|
+
exit $fail
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/smoke/router.sh — router-tier smoke (step-11 Phase 3 Step 3).
|
|
3
|
+
#
|
|
4
|
+
# Asserts router.json structural integrity:
|
|
5
|
+
# 1. 75 ids = 9 kernel + 24 tier_1 + 42 tier_2 (locked count).
|
|
6
|
+
# 2. Every id resolves to .agent-src/rules/<id>.md (0 broken).
|
|
7
|
+
# 3. Every routes_to ref resolves through its prefix
|
|
8
|
+
# (skill:, command:, guideline:, contract:); missing-contract
|
|
9
|
+
# count locked at ≤ EXPECTED_MISSING_CONTRACTS.
|
|
10
|
+
#
|
|
11
|
+
# Runtime ceiling: 30 s.
|
|
12
|
+
# Output: table by default, baseline line on stdout last; SMOKE_QUIET=1
|
|
13
|
+
# suppresses the table.
|
|
14
|
+
# Contract: docs/contracts/smoke-contracts.md § 3.2
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
EXPECTED_TOTAL_IDS=75
|
|
19
|
+
EXPECTED_MISSING_CONTRACTS=2
|
|
20
|
+
|
|
21
|
+
quiet="${SMOKE_QUIET:-0}"
|
|
22
|
+
log() { [ "$quiet" = "1" ] || printf '%s\n' "$*"; }
|
|
23
|
+
|
|
24
|
+
result=$(python3 <<'PY'
|
|
25
|
+
import json, os, sys, pathlib
|
|
26
|
+
|
|
27
|
+
d = json.load(open("router.json"))
|
|
28
|
+
kernel = d.get("kernel", [])
|
|
29
|
+
tier1 = d.get("tier_1", [])
|
|
30
|
+
tier2 = d.get("tier_2", [])
|
|
31
|
+
ids = list(kernel) + [r["id"] for r in tier1] + [r["id"] for r in tier2]
|
|
32
|
+
total = len(ids)
|
|
33
|
+
|
|
34
|
+
# Rule-file resolution
|
|
35
|
+
missing_rules = [i for i in ids if not os.path.exists(f".agent-src/rules/{i}.md")]
|
|
36
|
+
|
|
37
|
+
# routes_to resolution
|
|
38
|
+
def resolve(ref):
|
|
39
|
+
if ":" not in ref:
|
|
40
|
+
return f".agent-src.uncompressed/skills/{ref}/SKILL.md", "skill"
|
|
41
|
+
kind, rest = ref.split(":", 1)
|
|
42
|
+
if kind == "skill":
|
|
43
|
+
return f".agent-src.uncompressed/skills/{rest}/SKILL.md", "skill"
|
|
44
|
+
if kind == "command":
|
|
45
|
+
for p in (
|
|
46
|
+
f".agent-src.uncompressed/commands/{rest}.md",
|
|
47
|
+
f".agent-src.uncompressed/commands/{rest}/INDEX.md",
|
|
48
|
+
):
|
|
49
|
+
if os.path.exists(p):
|
|
50
|
+
return p, "command"
|
|
51
|
+
return f".agent-src.uncompressed/commands/{rest}.md", "command"
|
|
52
|
+
if kind == "guideline":
|
|
53
|
+
return f"docs/guidelines/{rest}.md", "guideline"
|
|
54
|
+
if kind == "contract":
|
|
55
|
+
return f"docs/contracts/{rest}.md", "contract"
|
|
56
|
+
return None, kind
|
|
57
|
+
|
|
58
|
+
refs = set()
|
|
59
|
+
for r in tier1 + tier2:
|
|
60
|
+
for ref in r.get("routes_to", []):
|
|
61
|
+
refs.add(ref)
|
|
62
|
+
|
|
63
|
+
missing_by_kind = {"skill": [], "command": [], "guideline": [], "contract": []}
|
|
64
|
+
for ref in refs:
|
|
65
|
+
path, kind = resolve(ref)
|
|
66
|
+
if path is None or not os.path.exists(path):
|
|
67
|
+
missing_by_kind.setdefault(kind, []).append(ref)
|
|
68
|
+
|
|
69
|
+
print(f"TOTAL_IDS={total}")
|
|
70
|
+
print(f"KERNEL={len(kernel)}")
|
|
71
|
+
print(f"TIER1={len(tier1)}")
|
|
72
|
+
print(f"TIER2={len(tier2)}")
|
|
73
|
+
print(f"MISSING_RULES={len(missing_rules)}")
|
|
74
|
+
print(f"ROUTES_TO_REFS={len(refs)}")
|
|
75
|
+
for kind, items in missing_by_kind.items():
|
|
76
|
+
print(f"MISSING_{kind.upper()}={len(items)}")
|
|
77
|
+
for r in items:
|
|
78
|
+
print(f" - {kind}: {r}")
|
|
79
|
+
PY
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Parse out the counters
|
|
83
|
+
TOTAL_IDS=$(echo "$result" | grep '^TOTAL_IDS=' | cut -d= -f2)
|
|
84
|
+
KERNEL=$(echo "$result" | grep '^KERNEL=' | cut -d= -f2)
|
|
85
|
+
TIER1=$(echo "$result" | grep '^TIER1=' | cut -d= -f2)
|
|
86
|
+
TIER2=$(echo "$result" | grep '^TIER2=' | cut -d= -f2)
|
|
87
|
+
MISSING_RULES=$(echo "$result" | grep '^MISSING_RULES=' | cut -d= -f2)
|
|
88
|
+
ROUTES_TO_REFS=$(echo "$result" | grep '^ROUTES_TO_REFS=' | cut -d= -f2)
|
|
89
|
+
MISSING_SKILL=$(echo "$result" | grep '^MISSING_SKILL=' | cut -d= -f2)
|
|
90
|
+
MISSING_COMMAND=$(echo "$result" | grep '^MISSING_COMMAND=' | cut -d= -f2)
|
|
91
|
+
MISSING_GUIDELINE=$(echo "$result" | grep '^MISSING_GUIDELINE=' | cut -d= -f2)
|
|
92
|
+
MISSING_CONTRACT=$(echo "$result" | grep '^MISSING_CONTRACT=' | cut -d= -f2)
|
|
93
|
+
|
|
94
|
+
log "## Router smoke"
|
|
95
|
+
log ""
|
|
96
|
+
log "| Check | Value |"
|
|
97
|
+
log "|---|---:|"
|
|
98
|
+
log "| Total router ids | $TOTAL_IDS (kernel $KERNEL · tier_1 $TIER1 · tier_2 $TIER2) |"
|
|
99
|
+
log "| Broken rule pointers | $MISSING_RULES |"
|
|
100
|
+
log "| routes_to refs | $ROUTES_TO_REFS |"
|
|
101
|
+
log "| missing skill targets | $MISSING_SKILL |"
|
|
102
|
+
log "| missing command targets | $MISSING_COMMAND |"
|
|
103
|
+
log "| missing guideline targets | $MISSING_GUIDELINE |"
|
|
104
|
+
log "| missing contract targets | $MISSING_CONTRACT (locked ≤ $EXPECTED_MISSING_CONTRACTS) |"
|
|
105
|
+
|
|
106
|
+
fail=0
|
|
107
|
+
if [ "$TOTAL_IDS" -ne "$EXPECTED_TOTAL_IDS" ]; then
|
|
108
|
+
echo "ℹ️ router id count drifted: $TOTAL_IDS (was $EXPECTED_TOTAL_IDS)"
|
|
109
|
+
fi
|
|
110
|
+
if [ "$MISSING_RULES" -gt 0 ]; then
|
|
111
|
+
echo "❌ broken rule pointers: $MISSING_RULES"
|
|
112
|
+
echo "$result" | grep '^ - skill:\|^ - guideline:' || true
|
|
113
|
+
fail=1
|
|
114
|
+
fi
|
|
115
|
+
if [ "$MISSING_SKILL" -gt 0 ] || [ "$MISSING_COMMAND" -gt 0 ] || [ "$MISSING_GUIDELINE" -gt 0 ]; then
|
|
116
|
+
echo "❌ broken routes_to targets:"
|
|
117
|
+
echo "$result" | grep -E '^ - (skill|command|guideline):' || true
|
|
118
|
+
fail=1
|
|
119
|
+
fi
|
|
120
|
+
if [ "$MISSING_CONTRACT" -gt "$EXPECTED_MISSING_CONTRACTS" ]; then
|
|
121
|
+
echo "❌ missing contracts: $MISSING_CONTRACT > $EXPECTED_MISSING_CONTRACTS (regression)"
|
|
122
|
+
echo "$result" | grep '^ - contract:' || true
|
|
123
|
+
fail=1
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
log ""
|
|
127
|
+
echo "BASELINE: $TOTAL_IDS router ids · $MISSING_RULES broken rule pointers · $ROUTES_TO_REFS routes_to refs · $MISSING_CONTRACT missing contracts"
|
|
128
|
+
|
|
129
|
+
exit $fail
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/smoke/schema.sh — schema-tier smoke (step-11 Phase 3 Step 4).
|
|
3
|
+
#
|
|
4
|
+
# Runs scripts/skill_linter.py --all over every lintable artefact and
|
|
5
|
+
# asserts:
|
|
6
|
+
# 1. 0 schema FAILs (hard).
|
|
7
|
+
# 2. Warns ≤ EXPECTED_WARNS (regression lock).
|
|
8
|
+
# 3. Total ≥ EXPECTED_MIN_TOTAL (catches accidental skill deletion).
|
|
9
|
+
#
|
|
10
|
+
# v2 schema (step-5) fields are deferred — when step-5 Phase 1 closes,
|
|
11
|
+
# this smoke gains a `model_tier` presence check; Phase 3 adds
|
|
12
|
+
# `schema_version: "2"`. See docs/contracts/smoke-contracts.md § 3.3.
|
|
13
|
+
#
|
|
14
|
+
# Runtime ceiling: 30 s.
|
|
15
|
+
# Output: table by default, baseline line on stdout last; SMOKE_QUIET=1
|
|
16
|
+
# suppresses the table.
|
|
17
|
+
# Contract: docs/contracts/smoke-contracts.md § 3.3
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
EXPECTED_WARNS=92
|
|
22
|
+
EXPECTED_MIN_TOTAL=438
|
|
23
|
+
|
|
24
|
+
quiet="${SMOKE_QUIET:-0}"
|
|
25
|
+
log() { [ "$quiet" = "1" ] || printf '%s\n' "$*"; }
|
|
26
|
+
|
|
27
|
+
# Run the linter and capture summary
|
|
28
|
+
out=$(python3 scripts/skill_linter.py --all --quiet 2>&1 || true)
|
|
29
|
+
summary=$(printf '%s\n' "$out" | grep -E '^Summary: ' | tail -1)
|
|
30
|
+
|
|
31
|
+
if [ -z "$summary" ]; then
|
|
32
|
+
echo "❌ skill_linter.py produced no summary line"
|
|
33
|
+
printf '%s\n' "$out" | tail -5
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Parse: "Summary: 346 pass, 92 warn, 0 fail, 438 total"
|
|
38
|
+
pass=$(echo "$summary" | sed -E 's/.*Summary: ([0-9]+) pass.*/\1/')
|
|
39
|
+
warn=$(echo "$summary" | sed -E 's/.*, ([0-9]+) warn.*/\1/')
|
|
40
|
+
fail=$(echo "$summary" | sed -E 's/.*, ([0-9]+) fail.*/\1/')
|
|
41
|
+
total=$(echo "$summary" | sed -E 's/.*, ([0-9]+) total.*/\1/')
|
|
42
|
+
|
|
43
|
+
log "## Schema smoke"
|
|
44
|
+
log ""
|
|
45
|
+
log "| Check | Value |"
|
|
46
|
+
log "|---|---:|"
|
|
47
|
+
log "| Total artefacts | $total (≥ $EXPECTED_MIN_TOTAL) |"
|
|
48
|
+
log "| Pass | $pass |"
|
|
49
|
+
log "| Warn | $warn (locked ≤ $EXPECTED_WARNS) |"
|
|
50
|
+
log "| Fail | $fail (hard 0) |"
|
|
51
|
+
log "| v2 schema enforcement | deferred (see step-5-schema-rigor.md) |"
|
|
52
|
+
|
|
53
|
+
exit_code=0
|
|
54
|
+
if [ "$fail" -gt 0 ]; then
|
|
55
|
+
echo "❌ schema FAILs: $fail (must be 0)"
|
|
56
|
+
printf '%s\n' "$out" | grep -E '^\[FAIL\]' | head -10 || true
|
|
57
|
+
exit_code=1
|
|
58
|
+
fi
|
|
59
|
+
if [ "$warn" -gt "$EXPECTED_WARNS" ]; then
|
|
60
|
+
echo "❌ schema warns: $warn > $EXPECTED_WARNS (regression)"
|
|
61
|
+
exit_code=1
|
|
62
|
+
fi
|
|
63
|
+
if [ "$total" -lt "$EXPECTED_MIN_TOTAL" ]; then
|
|
64
|
+
echo "❌ artefact total $total < $EXPECTED_MIN_TOTAL (unexpected deletion?)"
|
|
65
|
+
exit_code=1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
log ""
|
|
69
|
+
echo "BASELINE: $total lintable artefacts · $fail schema FAIL(s) · $warn warn(s)"
|
|
70
|
+
|
|
71
|
+
exit $exit_code
|