@event4u/agent-config 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent-src/commands/review-routing.md +7 -10
- package/.agent-src/contexts/authority/kernel-rule-edits.md +48 -0
- package/.agent-src/contexts/authority/scope-mechanics.md +15 -0
- package/.agent-src/contexts/contracts/consumer-agents-md-guide.md +127 -0
- package/.agent-src/contexts/contracts/emergency-triage-block.md +53 -0
- package/.agent-src/rules/analysis-skill-routing.md +1 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -1
- package/.agent-src/rules/artifact-engagement-recording.md +1 -1
- package/.agent-src/rules/augment-source-of-truth.md +1 -1
- package/.agent-src/rules/autonomous-execution.md +1 -1
- package/.agent-src/rules/caveman-speak.md +1 -1
- package/.agent-src/rules/cli-output-handling.md +1 -1
- package/.agent-src/rules/command-suggestion-policy.md +1 -1
- package/.agent-src/rules/docs-sync.md +1 -1
- package/.agent-src/rules/guidelines.md +1 -1
- package/.agent-src/rules/improve-before-implement.md +1 -1
- package/.agent-src/rules/invite-challenge.md +1 -1
- package/.agent-src/rules/minimal-safe-diff.md +1 -1
- package/.agent-src/rules/model-recommendation.md +1 -1
- package/.agent-src/rules/no-attribution-footers.md +1 -1
- package/.agent-src/rules/no-roadmap-references.md +56 -20
- package/.agent-src/rules/onboarding-gate.md +1 -1
- package/.agent-src/rules/package-ci-checks.md +1 -1
- package/.agent-src/rules/reviewer-awareness.md +9 -2
- package/.agent-src/rules/roadmap-progress-sync.md +1 -1
- package/.agent-src/rules/scope-control.md +6 -0
- package/.agent-src/rules/security-sensitive-stop.md +1 -1
- package/.agent-src/rules/size-enforcement.md +1 -1
- package/.agent-src/rules/token-optimizer-maintenance.md +1 -1
- package/.agent-src/rules/ui-audit-gate.md +1 -1
- package/.agent-src/skills/adr-create/SKILL.md +2 -1
- package/.agent-src/skills/agents-md-thin-root/SKILL.md +125 -0
- package/.agent-src/skills/ai-council/SKILL.md +9 -7
- package/.agent-src/skills/review-routing/SKILL.md +3 -4
- package/.agent-src/templates/AGENTS.md +18 -148
- package/.agent-src/templates/copilot-instructions.md +41 -17
- package/.agent-src/templates/github-workflows/pr-risk-review.yml +1 -1
- package/.agent-src/templates/scripts/pr_review_routing.py +1 -1
- package/.claude-plugin/marketplace.json +2 -1
- package/AGENTS.md +18 -216
- package/CHANGELOG.md +58 -0
- package/README.md +2 -2
- package/docs/architecture.md +13 -7
- package/docs/catalog.md +26 -27
- package/docs/contracts/agents-md-tech-stack.md +74 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +1 -1
- package/docs/contracts/linter-structural-model.md +180 -0
- package/docs/contracts/package-self-orientation.md +135 -0
- package/docs/contracts/rule-classification.md +4 -4
- package/docs/decisions/ADR-004-rule-governance-pruning.md +240 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/review-routing-data-format.md +1 -2
- package/docs/guidelines/agent-infra/size-and-scope.md +18 -12
- package/package.json +1 -1
- package/scripts/_p4_migrate.py +5 -5
- package/scripts/audit_auto_rules.py +159 -0
- package/scripts/audit_likelihood.py +148 -0
- package/scripts/audit_overlap.py +145 -0
- package/scripts/build_rule_trigger_matrix.py +3 -5
- package/scripts/check_augment_description_cap.py +79 -0
- package/scripts/check_council_references.py +3 -3
- package/scripts/check_kernel_rule_bundle.py +151 -0
- package/scripts/check_references.py +21 -1
- package/scripts/compile_router.py +3 -0
- package/scripts/install.sh +0 -1
- package/scripts/lint_agents_md.py +168 -0
- package/scripts/measure_augment_budget.py +208 -0
- package/scripts/measure_density.py +232 -0
- package/scripts/schemas/rule.schema.json +2 -1
- package/scripts/skill_linter.py +166 -31
- package/scripts/spotcheck_thin_root.py +134 -0
- package/scripts/update_counts.py +6 -10
- package/.agent-src/rules/no-council-references.md +0 -76
- package/.agent-src/rules/review-routing-awareness.md +0 -19
- package/.agent-src/templates/copilot-review-instructions.md +0 -76
package/scripts/_p4_migrate.py
CHANGED
|
@@ -52,12 +52,12 @@ SKILL_MIGRATIONS = [
|
|
|
52
52
|
("package-ci-checks", "skill:lint-skills",
|
|
53
53
|
[("phrase", "task ci"), ("phrase", "before push"), ("phrase", "before pr")],
|
|
54
54
|
"Run `task ci` locally and confirm green before pushing or opening a PR in this package."),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# review-routing-awareness was merged into reviewer-awareness on 2026-05-08
|
|
56
|
+
# (see agents/contexts/adr-auto-rule-consolidation.md) as part of the
|
|
57
|
+
# Augment literal-budget relief work — Lever D consolidation.
|
|
58
58
|
("reviewer-awareness", "skill:review-routing",
|
|
59
|
-
[("keyword", "reviewer"), ("phrase", "suggest reviewers")],
|
|
60
|
-
"Anchor reviewer choice in paths and risk, never seniority; medium / high risk requires primary + secondary role."),
|
|
59
|
+
[("keyword", "reviewer"), ("phrase", "suggest reviewers"), ("phrase", "risk hotspot"), ("phrase", "ownership map")],
|
|
60
|
+
"Anchor reviewer choice in paths and risk, never seniority; consult ownership-map + historical-bug-patterns; medium / high risk requires primary + secondary role."),
|
|
61
61
|
("skill-improvement-trigger", "skill:skill-improvement-pipeline",
|
|
62
62
|
[("phrase", "after completing"), ("keyword", "improvement"), ("keyword", "pipeline")],
|
|
63
63
|
"After a meaningful task, trigger the post-task learning capture if `pipelines.skill_improvement` is enabled."),
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Audit auto-rules for the Rule-Governance pass (Phase 5.1 of
|
|
3
|
+
road-to-augment-limit-fit).
|
|
4
|
+
|
|
5
|
+
Walk `.agent-src.uncompressed/rules/*.md`, collect per-rule frontmatter
|
|
6
|
+
(`description`, `triggers`, `routes_to`, `tier`), measure body and
|
|
7
|
+
registry-stub costs, and emit:
|
|
8
|
+
|
|
9
|
+
- `agents/reports/auto-rules-audit.json` — deterministic, machine-readable.
|
|
10
|
+
- `agents/reports/auto-rules-audit.md` — ranked summary for review.
|
|
11
|
+
|
|
12
|
+
The registry-stub cost mirrors `scripts/measure_augment_budget.py`'s
|
|
13
|
+
accounting model: only the stub line is injected into the Augment
|
|
14
|
+
workspace-guidelines budget for `type: auto` rules. The body cost is
|
|
15
|
+
informational — it ships in the projected `.augment/rules/` tree but
|
|
16
|
+
does NOT count against the 49,512-char ceiling.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
+
SRC_RULES = REPO_ROOT / ".agent-src.uncompressed" / "rules"
|
|
29
|
+
PROJECTED_RULES = REPO_ROOT / ".augment" / "rules"
|
|
30
|
+
REPORT_DIR = REPO_ROOT / "agents" / "reports"
|
|
31
|
+
JSON_OUT = REPORT_DIR / "auto-rules-audit.json"
|
|
32
|
+
MD_OUT = REPORT_DIR / "auto-rules-audit.md"
|
|
33
|
+
|
|
34
|
+
# Stub Augment injects per auto-rule. Mirrors measure_augment_budget.STUB_TEMPLATE.
|
|
35
|
+
STUB_TEMPLATE = (
|
|
36
|
+
'If the user prompt matches the description "{desc}", '
|
|
37
|
+
"read the file located in {path}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _split_frontmatter(text: str) -> tuple[dict, str]:
|
|
42
|
+
if not text.startswith("---\n"):
|
|
43
|
+
return {}, text
|
|
44
|
+
end = text.find("\n---", 4)
|
|
45
|
+
if end < 0:
|
|
46
|
+
return {}, text
|
|
47
|
+
fm = yaml.safe_load(text[4:end]) or {}
|
|
48
|
+
body = text[end + 4 :].lstrip("\n")
|
|
49
|
+
return fm, body
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _trigger_summary(triggers: list) -> dict:
|
|
53
|
+
paths: list[str] = []
|
|
54
|
+
keywords: list[str] = []
|
|
55
|
+
intents: list[str] = []
|
|
56
|
+
for entry in triggers or []:
|
|
57
|
+
if not isinstance(entry, dict):
|
|
58
|
+
continue
|
|
59
|
+
if "path_prefix" in entry:
|
|
60
|
+
paths.append(str(entry["path_prefix"]))
|
|
61
|
+
if "keyword" in entry:
|
|
62
|
+
keywords.append(str(entry["keyword"]))
|
|
63
|
+
if "intent" in entry:
|
|
64
|
+
intents.append(str(entry["intent"]))
|
|
65
|
+
return {"path_prefixes": paths, "keywords": keywords, "intents": intents}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def collect() -> list[dict]:
|
|
69
|
+
rules: list[dict] = []
|
|
70
|
+
for path in sorted(SRC_RULES.glob("*.md")):
|
|
71
|
+
text = path.read_text(encoding="utf-8")
|
|
72
|
+
fm, body = _split_frontmatter(text)
|
|
73
|
+
if fm.get("type") != "auto":
|
|
74
|
+
continue
|
|
75
|
+
desc = (fm.get("description") or "").strip()
|
|
76
|
+
rel_projected = f".augment/rules/{path.name}"
|
|
77
|
+
stub = STUB_TEMPLATE.format(desc=desc, path=rel_projected)
|
|
78
|
+
triggers = _trigger_summary(fm.get("triggers") or [])
|
|
79
|
+
routes_to = list(fm.get("routes_to") or [])
|
|
80
|
+
rules.append(
|
|
81
|
+
{
|
|
82
|
+
"name": path.stem,
|
|
83
|
+
"src_path": str(path.relative_to(REPO_ROOT)),
|
|
84
|
+
"tier": fm.get("tier"),
|
|
85
|
+
"description": desc,
|
|
86
|
+
"description_chars": len(desc),
|
|
87
|
+
"triggers": triggers,
|
|
88
|
+
"trigger_count": (
|
|
89
|
+
len(triggers["path_prefixes"])
|
|
90
|
+
+ len(triggers["keywords"])
|
|
91
|
+
+ len(triggers["intents"])
|
|
92
|
+
),
|
|
93
|
+
"routes_to": routes_to,
|
|
94
|
+
"body_chars": len(body),
|
|
95
|
+
"file_chars": len(text),
|
|
96
|
+
"stub_chars": len(stub),
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
return rules
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def render_markdown(rules: list[dict]) -> str:
|
|
103
|
+
total_stub = sum(r["stub_chars"] for r in rules)
|
|
104
|
+
total_body = sum(r["body_chars"] for r in rules)
|
|
105
|
+
total_desc = sum(r["description_chars"] for r in rules)
|
|
106
|
+
lines = [
|
|
107
|
+
"# Auto-Rule Audit",
|
|
108
|
+
"",
|
|
109
|
+
"Generated by `scripts/audit_auto_rules.py` for Phase 5 of",
|
|
110
|
+
"`agents/roadmaps/road-to-augment-limit-fit.md`. Re-run after",
|
|
111
|
+
"any rule add/merge/deprecate to refresh the baseline.",
|
|
112
|
+
"",
|
|
113
|
+
"## Totals",
|
|
114
|
+
"",
|
|
115
|
+
f"- auto-rules: **{len(rules)}**",
|
|
116
|
+
f"- registry-stub cost (counts against 49,512 cap): **{total_stub:,}** chars",
|
|
117
|
+
f"- description chars (subset of stub cost): **{total_desc:,}** chars",
|
|
118
|
+
f"- body chars (informational, NOT in budget): **{total_body:,}** chars",
|
|
119
|
+
"",
|
|
120
|
+
"## Ranked by registry-stub cost",
|
|
121
|
+
"",
|
|
122
|
+
"| # | Rule | Tier | Desc | Stub | Body | Triggers | Routes |",
|
|
123
|
+
"|---|------|------|------|------|------|----------|--------|",
|
|
124
|
+
]
|
|
125
|
+
for i, r in enumerate(sorted(rules, key=lambda x: -x["stub_chars"]), 1):
|
|
126
|
+
triggers = (
|
|
127
|
+
f"{len(r['triggers']['path_prefixes'])}p / "
|
|
128
|
+
f"{len(r['triggers']['keywords'])}k / "
|
|
129
|
+
f"{len(r['triggers']['intents'])}i"
|
|
130
|
+
)
|
|
131
|
+
routes = ", ".join(r["routes_to"]) or "—"
|
|
132
|
+
lines.append(
|
|
133
|
+
f"| {i} | `{r['name']}` | {r['tier'] or '—'} | "
|
|
134
|
+
f"{r['description_chars']} | {r['stub_chars']} | "
|
|
135
|
+
f"{r['body_chars']} | {triggers} | {routes} |"
|
|
136
|
+
)
|
|
137
|
+
lines.append("")
|
|
138
|
+
lines.append("Trigger key: `Np` = path-prefix, `Nk` = keyword, `Ni` = intent.")
|
|
139
|
+
lines.append("")
|
|
140
|
+
return "\n".join(lines)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main() -> int:
|
|
144
|
+
if not SRC_RULES.is_dir():
|
|
145
|
+
print(f"❌ Missing source dir: {SRC_RULES}", file=sys.stderr)
|
|
146
|
+
return 1
|
|
147
|
+
rules = collect()
|
|
148
|
+
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
payload = {"rule_count": len(rules), "rules": rules}
|
|
150
|
+
JSON_OUT.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
151
|
+
MD_OUT.write_text(render_markdown(rules), encoding="utf-8")
|
|
152
|
+
print(f"✅ Audited {len(rules)} auto-rules.")
|
|
153
|
+
print(f" JSON: {JSON_OUT.relative_to(REPO_ROOT)}")
|
|
154
|
+
print(f" MD: {MD_OUT.relative_to(REPO_ROOT)}")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
sys.exit(main())
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Activation-likelihood heuristic for the Rule-Governance pass
|
|
3
|
+
(Phase 5.3 of road-to-augment-limit-fit).
|
|
4
|
+
|
|
5
|
+
For every auto-rule from `agents/reports/auto-rules-audit.json`:
|
|
6
|
+
|
|
7
|
+
1. Build a token set from `description`, `triggers[].keyword`,
|
|
8
|
+
`triggers[].intent`, and the rule name itself.
|
|
9
|
+
2. Index a corpus of skills (`SKILL.md`), contexts
|
|
10
|
+
(`agents/contexts/**/*.md`), guidelines, and command files.
|
|
11
|
+
3. Score `corpus_hits = sum(1 for token in tokens if token in corpus)`.
|
|
12
|
+
4. Flag rules with `< 2` corpus hits as "low-likelihood" (their trigger
|
|
13
|
+
surface is so generic that the host LLM is unlikely to find a
|
|
14
|
+
project-local file the rule was written to bridge to).
|
|
15
|
+
|
|
16
|
+
Result is a JSON dump + Markdown section appended to
|
|
17
|
+
`agents/reports/auto-rules-audit.md`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from collections import Counter
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
REPORT_DIR = REPO_ROOT / "agents" / "reports"
|
|
30
|
+
AUDIT_JSON = REPORT_DIR / "auto-rules-audit.json"
|
|
31
|
+
AUDIT_MD = REPORT_DIR / "auto-rules-audit.md"
|
|
32
|
+
LIKELIHOOD_JSON = REPORT_DIR / "auto-rules-likelihood.json"
|
|
33
|
+
|
|
34
|
+
CORPUS_GLOBS = [
|
|
35
|
+
".agent-src.uncompressed/skills/**/SKILL.md",
|
|
36
|
+
".agent-src.uncompressed/commands/**/*.md",
|
|
37
|
+
"agents/contexts/**/*.md",
|
|
38
|
+
"docs/guidelines/**/*.md",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
LOW_LIKELIHOOD_HITS = 2
|
|
42
|
+
|
|
43
|
+
STOPWORDS = {
|
|
44
|
+
"the", "and", "for", "with", "when", "use", "or", "of", "to", "a",
|
|
45
|
+
"an", "is", "in", "on", "by", "be", "at", "as", "it", "if", "are",
|
|
46
|
+
"this", "that", "from", "but", "not", "can", "any", "all", "no",
|
|
47
|
+
"after", "before", "during", "user", "agent", "code", "project",
|
|
48
|
+
"via", "into", "onto", "even", "without", "naming", "rule", "rules",
|
|
49
|
+
"skill", "skills", "command", "commands", "files", "file", "doc",
|
|
50
|
+
"docs", "md", "txt",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def tokens(text: str) -> set[str]:
|
|
55
|
+
raw = re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", text.lower())
|
|
56
|
+
return {t for t in raw if t not in STOPWORDS and len(t) > 3}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_corpus() -> Counter:
|
|
60
|
+
counter: Counter = Counter()
|
|
61
|
+
for glob in CORPUS_GLOBS:
|
|
62
|
+
for path in REPO_ROOT.glob(glob):
|
|
63
|
+
if not path.is_file():
|
|
64
|
+
continue
|
|
65
|
+
try:
|
|
66
|
+
text = path.read_text(encoding="utf-8")
|
|
67
|
+
except UnicodeDecodeError:
|
|
68
|
+
continue
|
|
69
|
+
for tok in tokens(text):
|
|
70
|
+
counter[tok] += 1
|
|
71
|
+
return counter
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def score(rule: dict, corpus: Counter) -> dict:
|
|
75
|
+
rule_tokens = (
|
|
76
|
+
tokens(rule["description"])
|
|
77
|
+
| tokens(rule["name"].replace("-", " "))
|
|
78
|
+
| tokens(" ".join(rule["triggers"]["keywords"]))
|
|
79
|
+
| tokens(" ".join(rule["triggers"]["intents"]))
|
|
80
|
+
)
|
|
81
|
+
hits = {t: corpus[t] for t in rule_tokens if corpus[t] > 0}
|
|
82
|
+
return {
|
|
83
|
+
"name": rule["name"],
|
|
84
|
+
"tokens": sorted(rule_tokens),
|
|
85
|
+
"hits": dict(sorted(hits.items(), key=lambda x: -x[1])[:8]),
|
|
86
|
+
"hit_count": len(hits),
|
|
87
|
+
"total_hit_volume": sum(hits.values()),
|
|
88
|
+
"low_likelihood": len(hits) < LOW_LIKELIHOOD_HITS,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def render_md(scores: list[dict]) -> str:
|
|
93
|
+
flagged = [s for s in scores if s["low_likelihood"]]
|
|
94
|
+
lines = [
|
|
95
|
+
"",
|
|
96
|
+
"## Phase 5.3 — Activation likelihood (corpus-keyword)",
|
|
97
|
+
"",
|
|
98
|
+
f"Corpus: skills + commands + contexts + guidelines.",
|
|
99
|
+
f"Low-likelihood threshold: `< {LOW_LIKELIHOOD_HITS}` distinct corpus hits.",
|
|
100
|
+
"",
|
|
101
|
+
f"Rules flagged: **{len(flagged)} / {len(scores)}**.",
|
|
102
|
+
"",
|
|
103
|
+
"### Low-likelihood rules",
|
|
104
|
+
"",
|
|
105
|
+
]
|
|
106
|
+
if not flagged:
|
|
107
|
+
lines += ["_None._", ""]
|
|
108
|
+
else:
|
|
109
|
+
lines += ["| Rule | Hits | Tokens (top) |", "|------|------|--------------|"]
|
|
110
|
+
for s in sorted(flagged, key=lambda x: x["hit_count"]):
|
|
111
|
+
toks = ", ".join(f"`{t}`" for t in s["tokens"][:6]) or "—"
|
|
112
|
+
lines.append(f"| `{s['name']}` | {s['hit_count']} | {toks} |")
|
|
113
|
+
lines.append("")
|
|
114
|
+
lines += [
|
|
115
|
+
"### Full ranking (lowest hit-count first, top 20)",
|
|
116
|
+
"",
|
|
117
|
+
"| Rule | Distinct hits | Total hit volume |",
|
|
118
|
+
"|------|---------------|------------------|",
|
|
119
|
+
]
|
|
120
|
+
for s in sorted(scores, key=lambda x: (x["hit_count"], x["total_hit_volume"]))[:20]:
|
|
121
|
+
lines.append(f"| `{s['name']}` | {s['hit_count']} | {s['total_hit_volume']} |")
|
|
122
|
+
lines.append("")
|
|
123
|
+
return "\n".join(lines)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> int:
|
|
127
|
+
if not AUDIT_JSON.exists():
|
|
128
|
+
print(f"❌ Run audit_auto_rules.py first: missing {AUDIT_JSON}", file=sys.stderr)
|
|
129
|
+
return 1
|
|
130
|
+
rules = json.loads(AUDIT_JSON.read_text(encoding="utf-8"))["rules"]
|
|
131
|
+
corpus = build_corpus()
|
|
132
|
+
scores = [score(r, corpus) for r in rules]
|
|
133
|
+
LIKELIHOOD_JSON.write_text(
|
|
134
|
+
json.dumps({"corpus_size": len(corpus), "scores": scores}, indent=2),
|
|
135
|
+
encoding="utf-8",
|
|
136
|
+
)
|
|
137
|
+
md = AUDIT_MD.read_text(encoding="utf-8") if AUDIT_MD.exists() else ""
|
|
138
|
+
if "## Phase 5.3 — Activation likelihood" in md:
|
|
139
|
+
md = md.split("## Phase 5.3 — Activation likelihood")[0].rstrip() + "\n"
|
|
140
|
+
AUDIT_MD.write_text(md + render_md(scores), encoding="utf-8")
|
|
141
|
+
flagged = [s for s in scores if s["low_likelihood"]]
|
|
142
|
+
print(f"✅ Likelihood scored: {len(scores)} rules, {len(flagged)} low-likelihood.")
|
|
143
|
+
print(f" JSON: {LIKELIHOOD_JSON.relative_to(REPO_ROOT)}")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
sys.exit(main())
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Trigger-overlap analysis for the Rule-Governance pass (Phase 5.2 of
|
|
3
|
+
road-to-augment-limit-fit).
|
|
4
|
+
|
|
5
|
+
Reads `agents/reports/auto-rules-audit.json` (produced by
|
|
6
|
+
`audit_auto_rules.py`) and computes:
|
|
7
|
+
|
|
8
|
+
- path-prefix Jaccard similarity (per pair of rules);
|
|
9
|
+
- description-keyword overlap fraction (per pair of rules).
|
|
10
|
+
|
|
11
|
+
Pairs scoring `path_jaccard >= 0.5` OR `keyword_overlap >= 0.4` are
|
|
12
|
+
flagged as merge candidates. Output is appended to
|
|
13
|
+
`agents/reports/auto-rules-audit.md` and a structured JSON list is
|
|
14
|
+
written to `agents/reports/auto-rules-overlap.json` for downstream
|
|
15
|
+
consumers (Phase 5.3 likelihood, 5.4 council walk).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from itertools import combinations
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
27
|
+
REPORT_DIR = REPO_ROOT / "agents" / "reports"
|
|
28
|
+
AUDIT_JSON = REPORT_DIR / "auto-rules-audit.json"
|
|
29
|
+
AUDIT_MD = REPORT_DIR / "auto-rules-audit.md"
|
|
30
|
+
OVERLAP_JSON = REPORT_DIR / "auto-rules-overlap.json"
|
|
31
|
+
|
|
32
|
+
PATH_THRESHOLD = 0.5
|
|
33
|
+
KEYWORD_THRESHOLD = 0.4
|
|
34
|
+
|
|
35
|
+
STOPWORDS = {
|
|
36
|
+
"the", "and", "for", "with", "when", "use", "or", "of", "to", "a",
|
|
37
|
+
"an", "is", "in", "on", "by", "be", "at", "as", "it", "if", "are",
|
|
38
|
+
"this", "that", "from", "but", "not", "can", "any", "all", "no",
|
|
39
|
+
"after", "before", "during", "user", "agent", "code", "project",
|
|
40
|
+
"via", "into", "onto", "even", "without", "naming",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def keyword_set(text: str) -> set[str]:
|
|
45
|
+
tokens = re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", text.lower())
|
|
46
|
+
return {t for t in tokens if t not in STOPWORDS and not t.isdigit()}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def jaccard(a: set, b: set) -> float:
|
|
50
|
+
if not a and not b:
|
|
51
|
+
return 0.0
|
|
52
|
+
return len(a & b) / len(a | b)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def overlap_fraction(a: set, b: set) -> float:
|
|
56
|
+
"""Symmetric overlap as fraction of smaller set."""
|
|
57
|
+
if not a or not b:
|
|
58
|
+
return 0.0
|
|
59
|
+
return len(a & b) / min(len(a), len(b))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def analyse(rules: list[dict]) -> list[dict]:
|
|
63
|
+
pairs: list[dict] = []
|
|
64
|
+
for r in rules:
|
|
65
|
+
r["_paths"] = set(r["triggers"]["path_prefixes"])
|
|
66
|
+
r["_keywords"] = (
|
|
67
|
+
keyword_set(r["description"])
|
|
68
|
+
| keyword_set(" ".join(r["triggers"]["keywords"]))
|
|
69
|
+
| keyword_set(" ".join(r["triggers"]["intents"]))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for a, b in combinations(rules, 2):
|
|
73
|
+
pj = jaccard(a["_paths"], b["_paths"])
|
|
74
|
+
ko = overlap_fraction(a["_keywords"], b["_keywords"])
|
|
75
|
+
flagged = pj >= PATH_THRESHOLD or ko >= KEYWORD_THRESHOLD
|
|
76
|
+
if not flagged:
|
|
77
|
+
continue
|
|
78
|
+
pairs.append(
|
|
79
|
+
{
|
|
80
|
+
"rule_a": a["name"],
|
|
81
|
+
"rule_b": b["name"],
|
|
82
|
+
"path_jaccard": round(pj, 3),
|
|
83
|
+
"keyword_overlap": round(ko, 3),
|
|
84
|
+
"shared_paths": sorted(a["_paths"] & b["_paths"]),
|
|
85
|
+
"shared_keywords": sorted(a["_keywords"] & b["_keywords"])[:12],
|
|
86
|
+
"rule_a_desc": a["description"],
|
|
87
|
+
"rule_b_desc": b["description"],
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return sorted(
|
|
92
|
+
pairs, key=lambda p: -(p["path_jaccard"] + p["keyword_overlap"])
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def render_md(pairs: list[dict]) -> str:
|
|
97
|
+
lines = [
|
|
98
|
+
"",
|
|
99
|
+
"## Phase 5.2 — Trigger overlap (Jaccard + keyword)",
|
|
100
|
+
"",
|
|
101
|
+
f"Pairs flagged: **{len(pairs)}** "
|
|
102
|
+
f"(thresholds: path-Jaccard ≥ {PATH_THRESHOLD}, "
|
|
103
|
+
f"keyword-overlap ≥ {KEYWORD_THRESHOLD}).",
|
|
104
|
+
"",
|
|
105
|
+
]
|
|
106
|
+
if not pairs:
|
|
107
|
+
lines.append("_No pairs over threshold._")
|
|
108
|
+
lines.append("")
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
lines += [
|
|
111
|
+
"| # | Rule A | Rule B | Path-J | Keyword-O | Shared keywords |",
|
|
112
|
+
"|---|--------|--------|--------|-----------|-----------------|",
|
|
113
|
+
]
|
|
114
|
+
for i, p in enumerate(pairs, 1):
|
|
115
|
+
kw = ", ".join(f"`{k}`" for k in p["shared_keywords"][:6]) or "—"
|
|
116
|
+
lines.append(
|
|
117
|
+
f"| {i} | `{p['rule_a']}` | `{p['rule_b']}` | "
|
|
118
|
+
f"{p['path_jaccard']:.2f} | {p['keyword_overlap']:.2f} | {kw} |"
|
|
119
|
+
)
|
|
120
|
+
lines.append("")
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> int:
|
|
125
|
+
if not AUDIT_JSON.exists():
|
|
126
|
+
print(f"❌ Run audit_auto_rules.py first: missing {AUDIT_JSON}", file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
data = json.loads(AUDIT_JSON.read_text(encoding="utf-8"))
|
|
129
|
+
pairs = analyse(data["rules"])
|
|
130
|
+
OVERLAP_JSON.write_text(
|
|
131
|
+
json.dumps({"pair_count": len(pairs), "pairs": pairs}, indent=2),
|
|
132
|
+
encoding="utf-8",
|
|
133
|
+
)
|
|
134
|
+
md_existing = AUDIT_MD.read_text(encoding="utf-8") if AUDIT_MD.exists() else ""
|
|
135
|
+
if "## Phase 5.2 — Trigger overlap" in md_existing:
|
|
136
|
+
md_existing = md_existing.split("## Phase 5.2 — Trigger overlap")[0].rstrip() + "\n"
|
|
137
|
+
AUDIT_MD.write_text(md_existing + render_md(pairs), encoding="utf-8")
|
|
138
|
+
print(f"✅ Overlap analysis: {len(pairs)} pairs flagged.")
|
|
139
|
+
print(f" JSON: {OVERLAP_JSON.relative_to(REPO_ROOT)}")
|
|
140
|
+
print(f" MD appended: {AUDIT_MD.relative_to(REPO_ROOT)}")
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
sys.exit(main())
|
|
@@ -117,11 +117,9 @@ add("agent-docs.md", "file-edit on agents/docs/, AGENTS.md", "hook",
|
|
|
117
117
|
"tool-call", "medium", "2a", notes="Path-pattern based marker")
|
|
118
118
|
add("upstream-proposal.md", "skill/rule create event", "hook", "output",
|
|
119
119
|
"medium", "2a", notes="Marker after new artifact lands")
|
|
120
|
-
add("
|
|
121
|
-
"output", "medium", "2a",
|
|
122
|
-
notes="
|
|
123
|
-
add("reviewer-awareness.md", "PR-prep", "hook", "output",
|
|
124
|
-
"medium", "2a", notes="Reviewer-suggestion marker at PR creation")
|
|
120
|
+
add("reviewer-awareness.md", "PR-prep / reviewer-suggestion / risk flagging",
|
|
121
|
+
"hook", "output", "medium", "2a",
|
|
122
|
+
notes="Reviewer-suggestion + risk-tagging marker at PR creation; consolidates former review-routing-awareness")
|
|
125
123
|
add("security-sensitive-stop.md", "file-edit on auth/billing/secrets paths",
|
|
126
124
|
"hook", "tool-call", "low", "2a",
|
|
127
125
|
notes="Path-pattern based marker — strong candidate for low-cost hook")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Auto-rule description-length CI gate (Phase 1.3 of
|
|
3
|
+
road-to-augment-limit-fit).
|
|
4
|
+
|
|
5
|
+
For every `type: auto` rule under `.agent-src.uncompressed/rules/`,
|
|
6
|
+
fail CI when the frontmatter `description:` exceeds DESC_CAP chars.
|
|
7
|
+
|
|
8
|
+
Why: Augment injects each auto-rule's description into the
|
|
9
|
+
workspace-guidelines registry stub. Empirical 2026-05-08 budget
|
|
10
|
+
analysis showed this channel consuming 25 % of the 49,512-char
|
|
11
|
+
ceiling. Capping descriptions guards future drift.
|
|
12
|
+
|
|
13
|
+
Source of truth: `.agent-src.uncompressed/rules/`. The compressed
|
|
14
|
+
projection is regenerated; the source dictates what ships.
|
|
15
|
+
|
|
16
|
+
Exit codes: 0 = pass, 1 = at least one rule over cap.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
26
|
+
RULES_DIR = REPO_ROOT / ".agent-src.uncompressed" / "rules"
|
|
27
|
+
DESC_CAP = 150
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_frontmatter(text: str) -> dict[str, str]:
|
|
31
|
+
if not text.startswith("---\n"):
|
|
32
|
+
return {}
|
|
33
|
+
end = text.find("\n---", 4)
|
|
34
|
+
if end < 0:
|
|
35
|
+
return {}
|
|
36
|
+
fm: dict[str, str] = {}
|
|
37
|
+
for line in text[4:end].splitlines():
|
|
38
|
+
m = re.match(r"^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$", line)
|
|
39
|
+
if m:
|
|
40
|
+
fm[m.group(1)] = m.group(2).strip().strip('"').strip("'")
|
|
41
|
+
return fm
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> int:
|
|
45
|
+
failures: list[tuple[str, int, str]] = []
|
|
46
|
+
checked = 0
|
|
47
|
+
|
|
48
|
+
for path in sorted(RULES_DIR.glob("*.md")):
|
|
49
|
+
text = path.read_text()
|
|
50
|
+
fm = parse_frontmatter(text)
|
|
51
|
+
if fm.get("type") != "auto":
|
|
52
|
+
continue
|
|
53
|
+
desc = fm.get("description", "")
|
|
54
|
+
checked += 1
|
|
55
|
+
if len(desc) > DESC_CAP:
|
|
56
|
+
failures.append((path.name, len(desc), desc))
|
|
57
|
+
|
|
58
|
+
if failures:
|
|
59
|
+
print(
|
|
60
|
+
f"❌ {len(failures)} auto-rule description(s) exceed {DESC_CAP} chars:\n",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
for name, dlen, desc in sorted(failures, key=lambda x: -x[1]):
|
|
64
|
+
print(f" [{dlen:>3}] {name}", file=sys.stderr)
|
|
65
|
+
print(f" {desc}", file=sys.stderr)
|
|
66
|
+
print(
|
|
67
|
+
f"\n Guard rationale: each char in an auto-rule description "
|
|
68
|
+
f"costs one char in the\n Augment workspace-guidelines budget "
|
|
69
|
+
f"(cap 49,512). Trim to ≤ {DESC_CAP}.",
|
|
70
|
+
file=sys.stderr,
|
|
71
|
+
)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
print(f"✅ All {checked} auto-rule descriptions ≤ {DESC_CAP} chars.")
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
sys.exit(main())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""CI guard for the `no-
|
|
2
|
+
"""CI guard for the council clause of the `no-roadmap-references` rule.
|
|
3
3
|
|
|
4
4
|
Council artefacts under `agents/council-{questions,responses,sessions}/`
|
|
5
5
|
are gitignored, local-only, and auto-pruned. A link to a specific
|
|
@@ -68,7 +68,7 @@ ALLOWLIST_PREFIXES: tuple[str, ...] = (
|
|
|
68
68
|
# the SKIP_DIRS contract in scripts/check_references.py).
|
|
69
69
|
"agents/analysis/",
|
|
70
70
|
# The rule itself documents forbidden vs. allowed forms.
|
|
71
|
-
".agent-src.uncompressed/rules/no-
|
|
71
|
+
".agent-src.uncompressed/rules/no-roadmap-references.md",
|
|
72
72
|
# ai-council skill documents the output-path schema.
|
|
73
73
|
".agent-src.uncompressed/skills/ai-council/",
|
|
74
74
|
# Council commands document the output-path schema.
|
|
@@ -134,7 +134,7 @@ def main() -> int:
|
|
|
134
134
|
for path, ln, ref in violations:
|
|
135
135
|
print(f" - {path.as_posix()}:{ln}: {ref}")
|
|
136
136
|
print(
|
|
137
|
-
"\nRule: .agent-src/rules/no-
|
|
137
|
+
"\nRule: .agent-src/rules/no-roadmap-references.md (council clause)\n"
|
|
138
138
|
"Fix: inline the convergence summary (members + date) instead of\n"
|
|
139
139
|
"linking the file. Append "
|
|
140
140
|
"<!-- council-ref-allowed: <reason> --> on the same line to\n"
|