@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
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure structural density across the artifact corpus.
|
|
3
|
+
|
|
4
|
+
Phase 1.1 of `agents/roadmaps/road-to-structural-linter-reform.md`.
|
|
5
|
+
|
|
6
|
+
Density score = structured_lines / total_lines, where structured_lines
|
|
7
|
+
sum lines inside fenced blocks + markdown-table rows + bullet lines +
|
|
8
|
+
numbered/ordered-list lines + section-heading lines. Higher = more
|
|
9
|
+
structured (catalogue, orchestrator, Iron-Law block); lower = prose-
|
|
10
|
+
dominant.
|
|
11
|
+
|
|
12
|
+
Companion signals collected per artifact (consumed by Phases 1.2-1.4):
|
|
13
|
+
|
|
14
|
+
- ``multi_workflow`` ≥ 2 ``## Procedure`` (or ``## Procedure: …``)
|
|
15
|
+
blocks in a skill — candidate for cluster split.
|
|
16
|
+
- ``delegation`` command frontmatter has ``cluster:`` or
|
|
17
|
+
``routes_to:``, or the body links to ≥ 3 other
|
|
18
|
+
commands/skills via ``](...md)``.
|
|
19
|
+
- ``iron_law_block`` ≥ 1 fenced block whose body is ≥ 60 % ALL-CAPS
|
|
20
|
+
across ≥ 3 non-empty lines.
|
|
21
|
+
|
|
22
|
+
Output:
|
|
23
|
+
- Default stdout: per-type distribution buckets + tail (lowest density).
|
|
24
|
+
- ``--json`` deterministic JSON of every artifact.
|
|
25
|
+
- ``--snapshot`` writes JSONL to ``agents/.density-snapshot.jsonl``.
|
|
26
|
+
|
|
27
|
+
Stdlib only; no network. Re-runnable.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import re
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, Dict, List
|
|
37
|
+
|
|
38
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
39
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
40
|
+
|
|
41
|
+
from skill_linter import ( # noqa: E402
|
|
42
|
+
detect_artifact_type,
|
|
43
|
+
extract_frontmatter,
|
|
44
|
+
gather_all_candidate_files,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
SNAPSHOT_FILE = REPO_ROOT / "agents" / ".density-snapshot.jsonl"
|
|
48
|
+
|
|
49
|
+
_TABLE_ROW = re.compile(r"^\s*\|.*\|\s*$")
|
|
50
|
+
_BULLET = re.compile(r"^\s*[-*]\s+\S")
|
|
51
|
+
_NUMBERED = re.compile(r"^\s*\d+\.\s+\S")
|
|
52
|
+
_HEADING = re.compile(r"^\s{0,3}#{1,6}\s+\S")
|
|
53
|
+
_PROCEDURE = re.compile(r"^##\s+Procedure(\s*:.*)?\s*$", re.MULTILINE)
|
|
54
|
+
_LINK_MD = re.compile(r"\]\([^)]+\.md[^)]*\)")
|
|
55
|
+
_FRONTMATTER_KEY = re.compile(r"^(cluster|routes_to)\s*:", re.MULTILINE)
|
|
56
|
+
_ALLCAPS_LINE = re.compile(r"[A-Z]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _classify_lines(text: str) -> Dict[str, int]:
|
|
60
|
+
"""Bucket every non-blank line into one structural category."""
|
|
61
|
+
inside_fence = False
|
|
62
|
+
counts = {
|
|
63
|
+
"total": 0,
|
|
64
|
+
"fenced": 0,
|
|
65
|
+
"table": 0,
|
|
66
|
+
"bullet": 0,
|
|
67
|
+
"numbered": 0,
|
|
68
|
+
"heading": 0,
|
|
69
|
+
"prose": 0,
|
|
70
|
+
}
|
|
71
|
+
for raw in text.splitlines():
|
|
72
|
+
stripped = raw.strip()
|
|
73
|
+
if stripped.startswith("```"):
|
|
74
|
+
inside_fence = not inside_fence
|
|
75
|
+
counts["total"] += 1
|
|
76
|
+
counts["fenced"] += 1
|
|
77
|
+
continue
|
|
78
|
+
if not stripped:
|
|
79
|
+
continue
|
|
80
|
+
counts["total"] += 1
|
|
81
|
+
if inside_fence:
|
|
82
|
+
counts["fenced"] += 1
|
|
83
|
+
elif _TABLE_ROW.match(raw):
|
|
84
|
+
counts["table"] += 1
|
|
85
|
+
elif _HEADING.match(raw):
|
|
86
|
+
counts["heading"] += 1
|
|
87
|
+
elif _BULLET.match(raw):
|
|
88
|
+
counts["bullet"] += 1
|
|
89
|
+
elif _NUMBERED.match(raw):
|
|
90
|
+
counts["numbered"] += 1
|
|
91
|
+
else:
|
|
92
|
+
counts["prose"] += 1
|
|
93
|
+
return counts
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _detect_iron_law_blocks(text: str) -> int:
|
|
97
|
+
"""Count fenced blocks that look like verbatim Iron-Law imperatives.
|
|
98
|
+
|
|
99
|
+
Heuristic: fenced block with ≥ 1 non-empty line whose alphabetical
|
|
100
|
+
body is ≥ 60 % uppercase AND has ≥ 30 letters total (filters single
|
|
101
|
+
short ALL-CAPS markers like ``OK``). Also matches blockquote-style
|
|
102
|
+
Iron Laws (``> NEVER COMMIT``).
|
|
103
|
+
"""
|
|
104
|
+
blocks = 0
|
|
105
|
+
inside = False
|
|
106
|
+
body: list[str] = []
|
|
107
|
+
for raw in text.splitlines():
|
|
108
|
+
if raw.strip().startswith("```"):
|
|
109
|
+
if inside and body:
|
|
110
|
+
non_empty = [b for b in body if b.strip()]
|
|
111
|
+
letters = "".join(non_empty)
|
|
112
|
+
upper = sum(1 for c in letters if c.isalpha() and c.isupper())
|
|
113
|
+
total = sum(1 for c in letters if c.isalpha())
|
|
114
|
+
if total >= 30 and upper / total >= 0.6 and non_empty:
|
|
115
|
+
blocks += 1
|
|
116
|
+
inside = not inside
|
|
117
|
+
body = []
|
|
118
|
+
continue
|
|
119
|
+
if inside:
|
|
120
|
+
body.append(raw)
|
|
121
|
+
return blocks
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _count_procedures(text: str) -> int:
|
|
125
|
+
return len(_PROCEDURE.findall(text))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _delegation_signal(text: str, frontmatter: str | None) -> Dict[str, Any]:
|
|
129
|
+
fm_keys = bool(frontmatter and _FRONTMATTER_KEY.search(frontmatter))
|
|
130
|
+
md_links = len(_LINK_MD.findall(text))
|
|
131
|
+
return {"frontmatter_routes": fm_keys, "md_links": md_links,
|
|
132
|
+
"has_signal": fm_keys or md_links >= 3}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def measure(path: Path) -> Dict[str, Any]:
|
|
136
|
+
text = path.read_text(encoding="utf-8")
|
|
137
|
+
rel = path.relative_to(REPO_ROOT) if path.is_absolute() else path
|
|
138
|
+
artifact_type = detect_artifact_type(rel, text)
|
|
139
|
+
frontmatter = extract_frontmatter(text)
|
|
140
|
+
counts = _classify_lines(text)
|
|
141
|
+
structured = counts["fenced"] + counts["table"] + counts["bullet"] + \
|
|
142
|
+
counts["numbered"] + counts["heading"]
|
|
143
|
+
density = structured / counts["total"] if counts["total"] else 0.0
|
|
144
|
+
return {
|
|
145
|
+
"file": str(rel),
|
|
146
|
+
"type": artifact_type,
|
|
147
|
+
"lines": counts["total"],
|
|
148
|
+
"words": len(text.split()),
|
|
149
|
+
"density": round(density, 3),
|
|
150
|
+
"fenced": counts["fenced"],
|
|
151
|
+
"table": counts["table"],
|
|
152
|
+
"bullet": counts["bullet"],
|
|
153
|
+
"numbered": counts["numbered"],
|
|
154
|
+
"heading": counts["heading"],
|
|
155
|
+
"prose": counts["prose"],
|
|
156
|
+
"iron_law_blocks": _detect_iron_law_blocks(text),
|
|
157
|
+
"procedures": _count_procedures(text),
|
|
158
|
+
"delegation": _delegation_signal(text, frontmatter),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def collect() -> List[Dict[str, Any]]:
|
|
163
|
+
paths = gather_all_candidate_files(REPO_ROOT)
|
|
164
|
+
return [measure(p) for p in paths]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _bucketize(values: List[float]) -> Dict[str, int]:
|
|
168
|
+
buckets = {"0.0-0.2": 0, "0.2-0.4": 0, "0.4-0.6": 0,
|
|
169
|
+
"0.6-0.8": 0, "0.8-1.0": 0}
|
|
170
|
+
for v in values:
|
|
171
|
+
if v < 0.2:
|
|
172
|
+
buckets["0.0-0.2"] += 1
|
|
173
|
+
elif v < 0.4:
|
|
174
|
+
buckets["0.2-0.4"] += 1
|
|
175
|
+
elif v < 0.6:
|
|
176
|
+
buckets["0.4-0.6"] += 1
|
|
177
|
+
elif v < 0.8:
|
|
178
|
+
buckets["0.6-0.8"] += 1
|
|
179
|
+
else:
|
|
180
|
+
buckets["0.8-1.0"] += 1
|
|
181
|
+
return buckets
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def report(results: List[Dict[str, Any]]) -> str:
|
|
185
|
+
by_type: Dict[str, List[Dict[str, Any]]] = {}
|
|
186
|
+
for r in results:
|
|
187
|
+
by_type.setdefault(r["type"], []).append(r)
|
|
188
|
+
lines: List[str] = ["# Structural Density Snapshot", "",
|
|
189
|
+
f"Total artifacts: {len(results)}", ""]
|
|
190
|
+
for t in sorted(by_type):
|
|
191
|
+
rows = by_type[t]
|
|
192
|
+
densities = [r["density"] for r in rows]
|
|
193
|
+
avg = sum(densities) / len(densities) if densities else 0.0
|
|
194
|
+
med = sorted(densities)[len(densities) // 2] if densities else 0.0
|
|
195
|
+
buckets = _bucketize(densities)
|
|
196
|
+
lines.append(f"## {t} ({len(rows)} artifacts)")
|
|
197
|
+
lines.append(f"avg density={avg:.2f} median={med:.2f}")
|
|
198
|
+
lines.append("buckets " + " ".join(
|
|
199
|
+
f"[{k}]={v}" for k, v in buckets.items()))
|
|
200
|
+
tail = sorted(rows, key=lambda r: r["density"])[:5]
|
|
201
|
+
lines.append("lowest density:")
|
|
202
|
+
for r in tail:
|
|
203
|
+
lines.append(f" {r['density']:.2f} {r['lines']:>4}L "
|
|
204
|
+
f"proc={r['procedures']} "
|
|
205
|
+
f"iron={r['iron_law_blocks']} "
|
|
206
|
+
f"deleg={int(r['delegation']['has_signal'])} "
|
|
207
|
+
f"{r['file']}")
|
|
208
|
+
lines.append("")
|
|
209
|
+
return "\n".join(lines)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main() -> int:
|
|
213
|
+
p = argparse.ArgumentParser()
|
|
214
|
+
p.add_argument("--json", action="store_true")
|
|
215
|
+
p.add_argument("--snapshot", action="store_true",
|
|
216
|
+
help=f"write JSONL to {SNAPSHOT_FILE.relative_to(REPO_ROOT)}")
|
|
217
|
+
args = p.parse_args()
|
|
218
|
+
results = collect()
|
|
219
|
+
if args.snapshot:
|
|
220
|
+
SNAPSHOT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
with SNAPSHOT_FILE.open("w", encoding="utf-8") as fh:
|
|
222
|
+
for r in sorted(results, key=lambda x: x["file"]):
|
|
223
|
+
fh.write(json.dumps(r, sort_keys=True) + "\n")
|
|
224
|
+
if args.json:
|
|
225
|
+
print(json.dumps(results, sort_keys=True, indent=2))
|
|
226
|
+
else:
|
|
227
|
+
print(report(results))
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
raise SystemExit(main())
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"properties": {
|
|
10
10
|
"type": {
|
|
11
11
|
"type": "string",
|
|
12
|
-
"enum": ["always", "auto"]
|
|
12
|
+
"enum": ["always", "auto", "manual"],
|
|
13
|
+
"description": "`always` = injected verbatim every turn (kernel). `auto` = description stub injected, body loaded on trigger match. `manual` = no auto-injection (zero workspace-budget cost); file remains as a reference document linkable from skills/contexts. Introduced by ADR-004 to demote thin pointer-rules without breaking cross-references."
|
|
13
14
|
},
|
|
14
15
|
"source": {
|
|
15
16
|
"type": "string",
|
package/scripts/skill_linter.py
CHANGED
|
@@ -115,7 +115,7 @@ ORDERED_STEP_PATTERN = re.compile(r"^(?:\s*|\#{1,4}\s*)(\d+)\.\s+", re.MULTILINE
|
|
|
115
115
|
SECTION_PATTERN = re.compile(r"^##\s+(.+?)\s*$", re.MULTILINE)
|
|
116
116
|
FRONTMATTER_PATTERN = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
|
|
117
117
|
DESCRIPTION_PATTERN = re.compile(r'^description:\s*"?(.*?)"?\s*$', re.MULTILINE)
|
|
118
|
-
TYPE_PATTERN = re.compile(r'^type:\s*"?(always|auto)"?\s*$', re.MULTILINE)
|
|
118
|
+
TYPE_PATTERN = re.compile(r'^type:\s*"?(always|auto|manual)"?\s*$', re.MULTILINE)
|
|
119
119
|
SOURCE_PATTERN = re.compile(r'^source:\s*"?(package|project)"?\s*$', re.MULTILINE)
|
|
120
120
|
STATUS_PATTERN = re.compile(r'^status:\s*"?(active|deprecated|superseded)"?\s*$', re.MULTILINE)
|
|
121
121
|
REPLACED_BY_PATTERN = re.compile(r'^replaced_by:\s*"?([\w-]+)"?\s*$', re.MULTILINE)
|
|
@@ -133,7 +133,7 @@ SENIOR_OUTPUT_PATTERN = re.compile(r"^##\s+Output\s*$", re.MULTILINE)
|
|
|
133
133
|
H1_PATTERN = re.compile(r"^# .+", re.MULTILINE)
|
|
134
134
|
DOUBLE_BLANK_PATTERN = re.compile(r"\n{3,}")
|
|
135
135
|
|
|
136
|
-
VALID_RULE_TYPES = {"always", "auto"}
|
|
136
|
+
VALID_RULE_TYPES = {"always", "auto", "manual"}
|
|
137
137
|
VALID_RULE_SOURCES = {"package", "project"}
|
|
138
138
|
VALID_STATUSES = {"active", "deprecated", "superseded"}
|
|
139
139
|
|
|
@@ -264,9 +264,9 @@ def _count_code_blocks(text: str) -> int:
|
|
|
264
264
|
def _fenced_content_ratio(text: str) -> float:
|
|
265
265
|
"""Return the fraction of non-empty lines that sit inside fenced blocks.
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
Retained as a helper for backwards compatibility; the size gates use
|
|
268
|
+
:func:`_density_score` from the structural model instead (Phase 3 of
|
|
269
|
+
road-to-structural-linter-reform).
|
|
270
270
|
"""
|
|
271
271
|
inside = False
|
|
272
272
|
fenced_lines = 0
|
|
@@ -287,6 +287,106 @@ def _fenced_content_ratio(text: str) -> float:
|
|
|
287
287
|
return fenced_lines / non_empty
|
|
288
288
|
|
|
289
289
|
|
|
290
|
+
# --- Structural-density model (docs/contracts/linter-structural-model.md) ---
|
|
291
|
+
# Replaces the raw line/word/fenced-ratio gates with four primitives that
|
|
292
|
+
# distinguish complexity from bloat. Calibrated 2026-05-08 against the full
|
|
293
|
+
# 310-artefact corpus (agents/.density-snapshot.jsonl).
|
|
294
|
+
|
|
295
|
+
PROCEDURE_HEADING_PATTERN = re.compile(
|
|
296
|
+
r"^##\s+Procedure(\s*[:\u2014\-].*)?\s*$", re.MULTILINE
|
|
297
|
+
)
|
|
298
|
+
COMMAND_FRONTMATTER_DELEGATION_KEYS = ("cluster:", "routes_to:")
|
|
299
|
+
MD_LINK_PATTERN = re.compile(r"\[[^\]]+\]\(([^)]+\.md[^)]*)\)")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _density_score(text: str) -> float:
|
|
303
|
+
"""Return structural density 0.0–1.0 — see docs/contracts/linter-structural-model.md.
|
|
304
|
+
|
|
305
|
+
density = structured_lines / non_blank_lines, where structured_lines =
|
|
306
|
+
fenced + table + bullet + numbered + heading. Higher = more structured
|
|
307
|
+
(catalogue, table, code, list); lower = prose-dominant.
|
|
308
|
+
"""
|
|
309
|
+
inside_fence = False
|
|
310
|
+
structured = 0
|
|
311
|
+
non_blank = 0
|
|
312
|
+
for raw in text.splitlines():
|
|
313
|
+
stripped = raw.strip()
|
|
314
|
+
if not stripped:
|
|
315
|
+
continue
|
|
316
|
+
non_blank += 1
|
|
317
|
+
if stripped.startswith("```"):
|
|
318
|
+
inside_fence = not inside_fence
|
|
319
|
+
structured += 1
|
|
320
|
+
continue
|
|
321
|
+
if inside_fence:
|
|
322
|
+
structured += 1
|
|
323
|
+
continue
|
|
324
|
+
if stripped.startswith("#"):
|
|
325
|
+
structured += 1
|
|
326
|
+
continue
|
|
327
|
+
if stripped.startswith("|") and stripped.endswith("|"):
|
|
328
|
+
structured += 1
|
|
329
|
+
continue
|
|
330
|
+
if stripped.startswith(("- ", "* ", "+ ")):
|
|
331
|
+
structured += 1
|
|
332
|
+
continue
|
|
333
|
+
if re.match(r"^\d+\.\s", stripped):
|
|
334
|
+
structured += 1
|
|
335
|
+
continue
|
|
336
|
+
if non_blank == 0:
|
|
337
|
+
return 0.0
|
|
338
|
+
return round(structured / non_blank, 3)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _count_procedure_sections(text: str) -> int:
|
|
342
|
+
"""Count `## Procedure` (or `## Procedure: <name>`) blocks in *text*."""
|
|
343
|
+
return len(PROCEDURE_HEADING_PATTERN.findall(text))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _command_delegation_signal(text: str, frontmatter: Optional[str]) -> bool:
|
|
347
|
+
"""Return True when a command has a delegation signal.
|
|
348
|
+
|
|
349
|
+
Signals: frontmatter declares ``cluster:`` or ``routes_to:`` — OR — the
|
|
350
|
+
body contains ≥ 3 markdown links to other ``.md`` files. Either signal
|
|
351
|
+
is sufficient (council review 2026-05-08).
|
|
352
|
+
"""
|
|
353
|
+
if frontmatter:
|
|
354
|
+
for key in COMMAND_FRONTMATTER_DELEGATION_KEYS:
|
|
355
|
+
if re.search(rf"^{re.escape(key)}", frontmatter, re.MULTILINE):
|
|
356
|
+
return True
|
|
357
|
+
if len(MD_LINK_PATTERN.findall(text)) >= 3:
|
|
358
|
+
return True
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _iron_law_blocks(text: str) -> int:
|
|
363
|
+
"""Count fenced blocks that look like verbatim Iron-Law imperatives.
|
|
364
|
+
|
|
365
|
+
Heuristic: fenced block whose body has ≥ 30 alphabetical chars and
|
|
366
|
+
≥ 60 % uppercase across ≥ 1 non-empty line. The 30-char floor filters
|
|
367
|
+
short ALL-CAPS markers (``OK``, ``WIP``); the 60 %-uppercase floor
|
|
368
|
+
catches verbatim imperatives (``NEVER COMMIT.``).
|
|
369
|
+
"""
|
|
370
|
+
blocks = 0
|
|
371
|
+
inside = False
|
|
372
|
+
body: list[str] = []
|
|
373
|
+
for raw in text.splitlines():
|
|
374
|
+
if raw.strip().startswith("```"):
|
|
375
|
+
if inside and body:
|
|
376
|
+
non_empty = [b for b in body if b.strip()]
|
|
377
|
+
letters = "".join(non_empty)
|
|
378
|
+
upper = sum(1 for c in letters if c.isalpha() and c.isupper())
|
|
379
|
+
total = sum(1 for c in letters if c.isalpha())
|
|
380
|
+
if total >= 30 and upper / total >= 0.6 and non_empty:
|
|
381
|
+
blocks += 1
|
|
382
|
+
inside = not inside
|
|
383
|
+
body = []
|
|
384
|
+
continue
|
|
385
|
+
if inside:
|
|
386
|
+
body.append(raw)
|
|
387
|
+
return blocks
|
|
388
|
+
|
|
389
|
+
|
|
290
390
|
def extract_description(text: str) -> Optional[str]:
|
|
291
391
|
frontmatter = FRONTMATTER_PATTERN.search(text)
|
|
292
392
|
if not frontmatter:
|
|
@@ -561,14 +661,28 @@ def lint_skill(path: Path, text: str) -> LintResult:
|
|
|
561
661
|
"Assisted skill has no validation/challenge step in procedure"))
|
|
562
662
|
suggestions.append("Add a requirement-checking or validation step before implementation")
|
|
563
663
|
|
|
564
|
-
# --- Size check (
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#
|
|
664
|
+
# --- Size check (docs/contracts/linter-structural-model.md) ---
|
|
665
|
+
# Structural-density gate replaces raw line count (Phase 3 of
|
|
666
|
+
# road-to-structural-linter-reform, 2026-05-08): warn only when the skill
|
|
667
|
+
# is *both* large AND prose-dominant OR ships ≥ 2 independently invocable
|
|
668
|
+
# procedures. Reference catalogues (quality-tools 411 L / density 0.83)
|
|
669
|
+
# pass; multi-procedure skills are flagged for split.
|
|
569
670
|
total_lines = len(text.splitlines())
|
|
570
671
|
if total_lines > 400:
|
|
571
|
-
|
|
672
|
+
density = _density_score(text)
|
|
673
|
+
procedures = _count_procedure_sections(text)
|
|
674
|
+
if density < 0.6 or procedures >= 2:
|
|
675
|
+
reason = (
|
|
676
|
+
f"density {density:.2f} < 0.60"
|
|
677
|
+
if density < 0.6
|
|
678
|
+
else f"{procedures} ## Procedure blocks (≥ 2)"
|
|
679
|
+
)
|
|
680
|
+
issues.append(Issue(
|
|
681
|
+
"warning",
|
|
682
|
+
"skill_too_large",
|
|
683
|
+
f"Skill has {total_lines} lines and {reason}; review for split "
|
|
684
|
+
f"(see linter-structural-model contract)",
|
|
685
|
+
))
|
|
572
686
|
|
|
573
687
|
# --- Pointer-only / guideline-dependent skill detection ---
|
|
574
688
|
if procedure_block:
|
|
@@ -683,6 +797,12 @@ def lint_router_frontmatter(rule_id: str, frontmatter: str,
|
|
|
683
797
|
triggers = _parse_yaml_list(frontmatter, "triggers")
|
|
684
798
|
routes_to = _parse_yaml_list(frontmatter, "routes_to")
|
|
685
799
|
|
|
800
|
+
# Manual rules are reference-only — not auto-injected, not router-routed
|
|
801
|
+
# (ADR-004). Skip router validation so legacy triggers/routes_to fields
|
|
802
|
+
# remain documented in the rule body without forcing maintenance.
|
|
803
|
+
if rule_type == "manual":
|
|
804
|
+
return issues
|
|
805
|
+
|
|
686
806
|
is_kernel = rule_id in KERNEL_RULE_IDS or rule_type == "always"
|
|
687
807
|
|
|
688
808
|
if is_kernel:
|
|
@@ -961,9 +1081,9 @@ def lint_rule(path: Path, text: str) -> LintResult:
|
|
|
961
1081
|
# type field
|
|
962
1082
|
rule_type = extract_frontmatter_field(frontmatter, TYPE_PATTERN)
|
|
963
1083
|
if rule_type is None:
|
|
964
|
-
issues.append(Issue("error", "missing_type", "Frontmatter missing 'type' field (must be 'always' or '
|
|
1084
|
+
issues.append(Issue("error", "missing_type", "Frontmatter missing 'type' field (must be 'always', 'auto', or 'manual')"))
|
|
965
1085
|
elif rule_type not in VALID_RULE_TYPES:
|
|
966
|
-
issues.append(Issue("error", "invalid_type", f"Invalid type '{rule_type}'; must be 'always' or '
|
|
1086
|
+
issues.append(Issue("error", "invalid_type", f"Invalid type '{rule_type}'; must be 'always', 'auto', or 'manual'"))
|
|
967
1087
|
|
|
968
1088
|
# source field
|
|
969
1089
|
rule_source = extract_frontmatter_field(frontmatter, SOURCE_PATTERN)
|
|
@@ -1015,19 +1135,26 @@ def lint_rule(path: Path, text: str) -> LintResult:
|
|
|
1015
1135
|
if DOUBLE_BLANK_PATTERN.search(text):
|
|
1016
1136
|
issues.append(Issue("warning", "double_blank_lines", "File contains double or triple blank lines"))
|
|
1017
1137
|
|
|
1018
|
-
# --- Content checks (
|
|
1019
|
-
#
|
|
1020
|
-
#
|
|
1021
|
-
#
|
|
1138
|
+
# --- Content checks (docs/contracts/linter-structural-model.md) ---
|
|
1139
|
+
# Structural-density gate replaces fenced-ratio + dual-threshold (Phase 3
|
|
1140
|
+
# of road-to-structural-linter-reform, 2026-05-08): warn only when the
|
|
1141
|
+
# rule is long, prose-dominant, AND ships no Iron-Law block. Hard error
|
|
1142
|
+
# at 200 lines stays unconditional.
|
|
1022
1143
|
line_count = len([line for line in text.splitlines() if line.strip()])
|
|
1023
1144
|
total_lines = len(text.splitlines())
|
|
1024
|
-
fenced_ratio = _fenced_content_ratio(text)
|
|
1025
1145
|
if total_lines > 200:
|
|
1026
1146
|
issues.append(Issue("error", "rule_too_large", f"Rule has {total_lines} lines (hard limit: 200); must split or move to guideline"))
|
|
1027
|
-
elif line_count > 60
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1147
|
+
elif line_count > 60:
|
|
1148
|
+
density = _density_score(text)
|
|
1149
|
+
iron_blocks = _iron_law_blocks(text)
|
|
1150
|
+
if density < 0.5 and iron_blocks == 0:
|
|
1151
|
+
issues.append(Issue(
|
|
1152
|
+
"warning",
|
|
1153
|
+
"long_rule",
|
|
1154
|
+
f"Rule has {line_count} non-empty lines, density {density:.2f} < 0.50, "
|
|
1155
|
+
f"no Iron-Law block; rules should be concise "
|
|
1156
|
+
f"(see linter-structural-model contract)",
|
|
1157
|
+
))
|
|
1031
1158
|
|
|
1032
1159
|
for bad_sign in RULE_BAD_SIGNS:
|
|
1033
1160
|
if bad_sign in text:
|
|
@@ -1171,17 +1298,25 @@ def lint_command(path: Path, text: str) -> LintResult:
|
|
|
1171
1298
|
if not has_steps and not has_numbered:
|
|
1172
1299
|
issues.append(Issue("warning", "no_steps", "Command has no Steps section or numbered sub-headings"))
|
|
1173
1300
|
|
|
1174
|
-
# --- Size check (
|
|
1175
|
-
#
|
|
1176
|
-
# 2026-05-
|
|
1177
|
-
#
|
|
1301
|
+
# --- Size check (docs/contracts/linter-structural-model.md) ---
|
|
1302
|
+
# Structural-density gate replaces sub-section + code-block heuristic
|
|
1303
|
+
# (Phase 3 of road-to-structural-linter-reform, 2026-05-08): warn only
|
|
1304
|
+
# when the command is large, lacks a delegation signal (frontmatter
|
|
1305
|
+
# cluster:/routes_to: OR ≥ 3 markdown links to other .md files), AND
|
|
1306
|
+
# has density < 0.65.
|
|
1178
1307
|
word_count = len(text.split())
|
|
1179
1308
|
if word_count > 1000:
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1309
|
+
density = _density_score(text)
|
|
1310
|
+
delegated = _command_delegation_signal(text, frontmatter)
|
|
1311
|
+
if not delegated and density < 0.65:
|
|
1312
|
+
issues.append(Issue(
|
|
1313
|
+
"warning",
|
|
1314
|
+
"large_command",
|
|
1315
|
+
f"Command has {word_count} words, density {density:.2f} < 0.65, "
|
|
1316
|
+
f"no delegation signal (frontmatter cluster:/routes_to: or "
|
|
1317
|
+
f"≥ 3 .md links); review for split or delegation "
|
|
1318
|
+
f"(see linter-structural-model contract)",
|
|
1319
|
+
))
|
|
1185
1320
|
|
|
1186
1321
|
# File must end with exactly one newline
|
|
1187
1322
|
if not text.endswith("\n"):
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 6.6 platform spot-check via AI council.
|
|
3
|
+
|
|
4
|
+
Sends the refactored package-root AGENTS.md and the consumer template
|
|
5
|
+
to Sonnet 4.5 + gpt-4o, asks each member to answer five questions
|
|
6
|
+
that simulate a fresh agent landing on the file. Records qualitative
|
|
7
|
+
verdicts in agents/reports/thin-root-platform-spotcheck.md.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
sys.path.insert(0, str(ROOT))
|
|
17
|
+
|
|
18
|
+
from scripts.ai_council.clients import ( # noqa: E402
|
|
19
|
+
AnthropicClient,
|
|
20
|
+
OpenAIClient,
|
|
21
|
+
load_anthropic_key,
|
|
22
|
+
load_openai_key,
|
|
23
|
+
)
|
|
24
|
+
from scripts.ai_council.orchestrator import ( # noqa: E402
|
|
25
|
+
CostBudget,
|
|
26
|
+
CouncilQuestion,
|
|
27
|
+
consult,
|
|
28
|
+
)
|
|
29
|
+
from scripts.ai_council.pricing import load_prices # noqa: E402
|
|
30
|
+
|
|
31
|
+
QUESTIONS = """
|
|
32
|
+
You are evaluating whether the AGENTS.md file below is a sufficient
|
|
33
|
+
entry point for an AI coding agent landing on this repository for
|
|
34
|
+
the first time. You see only the AGENTS.md content; you do NOT have
|
|
35
|
+
file-system access. Answer the following five questions in JSON
|
|
36
|
+
shape `{"q1": {...}, ..., "q5": {...}}` where each value is
|
|
37
|
+
`{"answer": <string>, "confidence": "high"|"medium"|"low",
|
|
38
|
+
"pointer_used": <one of the linked paths from AGENTS.md, or null>}`.
|
|
39
|
+
|
|
40
|
+
Q1. Where do I edit content in this repo / project? (a path)
|
|
41
|
+
Q2. What command do I run to verify everything is green before opening a PR?
|
|
42
|
+
Q3. Where would I find the always-active behavioural rules?
|
|
43
|
+
Q4. If only this file is reachable, what five things must I assume to be true to act safely? (cite the emergency-triage block)
|
|
44
|
+
Q5. What outboard target document would I open to learn the package-self-orientation / the consumer-fill-out guide? (a path)
|
|
45
|
+
|
|
46
|
+
After the JSON, add a short prose verdict (≤ 5 sentences) on:
|
|
47
|
+
- Whether the pointer-following worked (could you cite a path for Q1, Q3, Q5?)
|
|
48
|
+
- Whether the emergency-triage block answered Q4 unambiguously.
|
|
49
|
+
- One concrete improvement you'd make to the AGENTS.md.
|
|
50
|
+
|
|
51
|
+
Do not invent file paths. If a question cannot be answered from the
|
|
52
|
+
file alone, set `"pointer_used": null` and lower confidence.
|
|
53
|
+
""".strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> int:
|
|
57
|
+
package_root = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
|
|
58
|
+
consumer_template = (
|
|
59
|
+
ROOT / ".agent-src.uncompressed" / "templates" / "AGENTS.md"
|
|
60
|
+
).read_text(encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
artefact = (
|
|
63
|
+
"## Artefact A — package-root AGENTS.md\n\n"
|
|
64
|
+
f"```markdown\n{package_root}\n```\n\n"
|
|
65
|
+
"## Artefact B — consumer-template AGENTS.md\n\n"
|
|
66
|
+
f"```markdown\n{consumer_template}\n```\n\n"
|
|
67
|
+
f"{QUESTIONS}\n"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
members = [
|
|
71
|
+
AnthropicClient(model="claude-sonnet-4-5", api_key=load_anthropic_key()),
|
|
72
|
+
OpenAIClient(model="gpt-4o", api_key=load_openai_key()),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
question = CouncilQuestion(
|
|
76
|
+
mode="files",
|
|
77
|
+
user_prompt=artefact,
|
|
78
|
+
max_tokens=1500,
|
|
79
|
+
)
|
|
80
|
+
budget = CostBudget(max_total_usd=2.00, max_calls=4)
|
|
81
|
+
table = load_prices()
|
|
82
|
+
|
|
83
|
+
print("Running spot-check council …", file=sys.stderr)
|
|
84
|
+
responses = consult(members, question, budget, table=table, rounds=1)
|
|
85
|
+
|
|
86
|
+
out_dir = ROOT / "agents" / "reports"
|
|
87
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
md_path = out_dir / "thin-root-platform-spotcheck.md"
|
|
89
|
+
json_path = out_dir / "thin-root-platform-spotcheck.json"
|
|
90
|
+
|
|
91
|
+
md_lines = [
|
|
92
|
+
"# Thin-Root platform spot-check (Phase 6.6)",
|
|
93
|
+
"",
|
|
94
|
+
"> AI-council proxy for the manual platform spot-check. Two",
|
|
95
|
+
"> external reviewers (Sonnet 4.5, gpt-4o) simulate a fresh",
|
|
96
|
+
"> agent landing on the refactored AGENTS.md and answer five",
|
|
97
|
+
"> orientation questions from the file alone.",
|
|
98
|
+
"",
|
|
99
|
+
"## Verdicts",
|
|
100
|
+
"",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
raw = []
|
|
104
|
+
for r in responses:
|
|
105
|
+
body = r.text or f"<error: {r.error}>"
|
|
106
|
+
raw.append({
|
|
107
|
+
"provider": r.provider,
|
|
108
|
+
"model": r.model,
|
|
109
|
+
"tokens_in": r.input_tokens,
|
|
110
|
+
"tokens_out": r.output_tokens,
|
|
111
|
+
"latency_ms": r.latency_ms,
|
|
112
|
+
"error": r.error,
|
|
113
|
+
"text": body,
|
|
114
|
+
})
|
|
115
|
+
md_lines.append(f"### {r.provider} ({r.model})")
|
|
116
|
+
md_lines.append("")
|
|
117
|
+
md_lines.append(f"- tokens in: {r.input_tokens} · out: {r.output_tokens} · latency: {r.latency_ms}ms")
|
|
118
|
+
if r.error:
|
|
119
|
+
md_lines.append(f"- error: `{r.error}`")
|
|
120
|
+
md_lines.append("")
|
|
121
|
+
md_lines.append("```")
|
|
122
|
+
md_lines.append(body[:8000])
|
|
123
|
+
md_lines.append("```")
|
|
124
|
+
md_lines.append("")
|
|
125
|
+
|
|
126
|
+
md_path.write_text("\n".join(md_lines), encoding="utf-8")
|
|
127
|
+
json_path.write_text(json.dumps(raw, indent=2), encoding="utf-8")
|
|
128
|
+
print(f"✅ Wrote {md_path}", file=sys.stderr)
|
|
129
|
+
print(f"✅ Wrote {json_path}", file=sys.stderr)
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
raise SystemExit(main())
|
package/scripts/update_counts.py
CHANGED
|
@@ -67,16 +67,12 @@ TARGETS: list[tuple[str, list[tuple[str, str]]]] = [
|
|
|
67
67
|
# the raw file count this script computes.
|
|
68
68
|
],
|
|
69
69
|
),
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
(r"(guidelines/\s+\()(\d+)( guidelines\))", "guidelines"),
|
|
77
|
-
(r"(personas/\s+\()(\d+)( personas\))", "personas"),
|
|
78
|
-
],
|
|
79
|
-
),
|
|
70
|
+
# Note: AGENTS.md previously held the per-directory count annotations
|
|
71
|
+
# (`skills/ (N skills)`, `rules/ (N rules)`, ...). The Thin-Root
|
|
72
|
+
# refactor (Phase 6, road-to-augment-limit-fit, 2026-05-08) made
|
|
73
|
+
# AGENTS.md a navigation-only surface — counts now live in README.md
|
|
74
|
+
# and docs/architecture.md. The corresponding pytest sentinel lives
|
|
75
|
+
# in tests/test_readme_hero_counts.py::test_agents_md_is_thin_root_navigation_surface.
|
|
80
76
|
(
|
|
81
77
|
"docs/getting-started.md",
|
|
82
78
|
[
|