@event4u/agent-config 2.19.0 → 2.20.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 (92) hide show
  1. package/.agent-src/commands/agent-status.md +29 -0
  2. package/.agent-src/commands/onboard.md +221 -81
  3. package/.agent-src/packs/README.md +49 -0
  4. package/.agent-src/packs/agency-delivery.yml +63 -0
  5. package/.agent-src/packs/content-engine.yml +53 -0
  6. package/.agent-src/packs/founder-mvp.yml +51 -0
  7. package/.agent-src/presets/README.md +26 -0
  8. package/.agent-src/presets/balanced.yml +34 -0
  9. package/.agent-src/presets/fast.yml +31 -0
  10. package/.agent-src/presets/strict.yml +38 -0
  11. package/.agent-src/profiles/README.md +29 -0
  12. package/.agent-src/profiles/agency.yml +27 -0
  13. package/.agent-src/profiles/content_creator.yml +25 -0
  14. package/.agent-src/profiles/developer.yml +26 -0
  15. package/.agent-src/profiles/finance.yml +24 -0
  16. package/.agent-src/profiles/founder.yml +25 -0
  17. package/.agent-src/profiles/ops.yml +25 -0
  18. package/.agent-src/rules/no-cheap-questions.md +25 -17
  19. package/.agent-src/skills/adr-create/SKILL.md +78 -68
  20. package/.agent-src/skills/subagent-orchestration/SKILL.md +33 -0
  21. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/.agent-src/templates/skill-archive-note.md +101 -0
  23. package/.claude-plugin/marketplace.json +1 -1
  24. package/CHANGELOG.md +52 -30
  25. package/README.md +68 -72
  26. package/config/agent-settings.template.yml +22 -0
  27. package/docs/adrs/caveman/0001-default-off-until-bench.md +93 -0
  28. package/docs/adrs/caveman/README.md +9 -0
  29. package/docs/adrs/cost/0001-hard-stop-hook.md +114 -0
  30. package/docs/adrs/cost/README.md +9 -0
  31. package/docs/adrs/memory/0001-consumer-side-snapshot.md +111 -0
  32. package/docs/adrs/memory/README.md +9 -0
  33. package/docs/adrs/router/0001-three-tier-routing.md +119 -0
  34. package/docs/adrs/router/README.md +9 -0
  35. package/docs/adrs/schema/0001-json-schema-frontmatter.md +102 -0
  36. package/docs/adrs/schema/README.md +9 -0
  37. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +99 -0
  38. package/docs/adrs/smoke/README.md +9 -0
  39. package/docs/architecture/current-onboard-baseline.md +126 -0
  40. package/docs/architecture/current-safety-behavior.md +137 -0
  41. package/docs/archive/CHANGELOG-pre-2.16.0.md +48 -0
  42. package/docs/contracts/adr-layout.md +108 -0
  43. package/docs/contracts/benchmark-corpus-spec.md +97 -0
  44. package/docs/contracts/benchmark-report-schema.md +111 -0
  45. package/docs/contracts/command-clusters.md +1 -0
  46. package/docs/contracts/command-taxonomy.md +137 -0
  47. package/docs/contracts/compression-default-kill-criterion.md +69 -0
  48. package/docs/contracts/config-presets.md +144 -0
  49. package/docs/contracts/cost-dashboard.md +143 -0
  50. package/docs/contracts/cost-enforcement.md +134 -0
  51. package/docs/contracts/file-ownership-matrix.json +0 -7
  52. package/docs/contracts/mcp-tool-inventory.md +53 -0
  53. package/docs/contracts/measurement-baseline.md +102 -0
  54. package/docs/contracts/namespace.md +125 -0
  55. package/docs/contracts/profile-system.md +142 -0
  56. package/docs/contracts/safety-model.md +129 -0
  57. package/docs/contracts/smoke-contracts.md +144 -0
  58. package/docs/contracts/workflow-packs.md +121 -0
  59. package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +132 -0
  60. package/docs/decisions/INDEX.md +1 -0
  61. package/docs/featured-commands.md +27 -0
  62. package/docs/parity/bench-ruflo.json +58 -0
  63. package/docs/parity/bench.json +41 -0
  64. package/docs/parity/ruflo.md +46 -0
  65. package/docs/profiles.md +91 -0
  66. package/package.json +1 -1
  67. package/scripts/_cli/cmd_explain.py +250 -0
  68. package/scripts/_lib/bench_cost.py +138 -0
  69. package/scripts/_lib/bench_quality.py +118 -0
  70. package/scripts/_lib/bench_report.py +150 -0
  71. package/scripts/agent-config +13 -0
  72. package/scripts/audit_adr_coverage.py +175 -0
  73. package/scripts/audit_mcp_tools.py +146 -0
  74. package/scripts/bench_baseline_ready.py +108 -0
  75. package/scripts/bench_drift_check.py +151 -0
  76. package/scripts/bench_per_tool.py +216 -0
  77. package/scripts/bench_run.py +155 -0
  78. package/scripts/config/__init__.py +9 -0
  79. package/scripts/config/presets.py +206 -0
  80. package/scripts/config/profiles.py +173 -0
  81. package/scripts/cost/budget.mjs +73 -12
  82. package/scripts/cost/preflight.mjs +89 -0
  83. package/scripts/lint_archived_skills.py +143 -0
  84. package/scripts/lint_bench_corpus.py +161 -0
  85. package/scripts/lint_namespace.py +135 -0
  86. package/scripts/skill_overlap.py +204 -0
  87. package/scripts/skill_usage_collect.py +191 -0
  88. package/scripts/skill_usage_report.py +162 -0
  89. package/scripts/smoke/kernel.sh +101 -0
  90. package/scripts/smoke/router.sh +129 -0
  91. package/scripts/smoke/schema.sh +71 -0
  92. 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