@event4u/agent-config 1.24.0 → 1.25.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 (72) hide show
  1. package/.agent-src/commands/review-routing.md +7 -10
  2. package/.agent-src/contexts/authority/kernel-rule-edits.md +48 -0
  3. package/.agent-src/contexts/authority/scope-mechanics.md +15 -0
  4. package/.agent-src/contexts/contracts/consumer-agents-md-guide.md +127 -0
  5. package/.agent-src/contexts/contracts/emergency-triage-block.md +53 -0
  6. package/.agent-src/rules/analysis-skill-routing.md +1 -1
  7. package/.agent-src/rules/artifact-drafting-protocol.md +1 -1
  8. package/.agent-src/rules/artifact-engagement-recording.md +1 -1
  9. package/.agent-src/rules/augment-source-of-truth.md +1 -1
  10. package/.agent-src/rules/autonomous-execution.md +1 -1
  11. package/.agent-src/rules/caveman-speak.md +1 -1
  12. package/.agent-src/rules/cli-output-handling.md +1 -1
  13. package/.agent-src/rules/command-suggestion-policy.md +1 -1
  14. package/.agent-src/rules/docs-sync.md +1 -1
  15. package/.agent-src/rules/guidelines.md +1 -1
  16. package/.agent-src/rules/improve-before-implement.md +1 -1
  17. package/.agent-src/rules/invite-challenge.md +1 -1
  18. package/.agent-src/rules/minimal-safe-diff.md +1 -1
  19. package/.agent-src/rules/model-recommendation.md +1 -1
  20. package/.agent-src/rules/no-attribution-footers.md +1 -1
  21. package/.agent-src/rules/no-roadmap-references.md +56 -20
  22. package/.agent-src/rules/onboarding-gate.md +1 -1
  23. package/.agent-src/rules/package-ci-checks.md +1 -1
  24. package/.agent-src/rules/reviewer-awareness.md +9 -2
  25. package/.agent-src/rules/roadmap-progress-sync.md +1 -1
  26. package/.agent-src/rules/scope-control.md +6 -0
  27. package/.agent-src/rules/security-sensitive-stop.md +1 -1
  28. package/.agent-src/rules/size-enforcement.md +1 -1
  29. package/.agent-src/rules/token-optimizer-maintenance.md +1 -1
  30. package/.agent-src/rules/ui-audit-gate.md +1 -1
  31. package/.agent-src/skills/adr-create/SKILL.md +2 -1
  32. package/.agent-src/skills/agents-md-thin-root/SKILL.md +125 -0
  33. package/.agent-src/skills/ai-council/SKILL.md +9 -7
  34. package/.agent-src/skills/review-routing/SKILL.md +3 -4
  35. package/.agent-src/templates/AGENTS.md +18 -148
  36. package/.agent-src/templates/copilot-instructions.md +41 -17
  37. package/.agent-src/templates/github-workflows/pr-risk-review.yml +1 -1
  38. package/.agent-src/templates/scripts/pr_review_routing.py +1 -1
  39. package/.claude-plugin/marketplace.json +2 -1
  40. package/AGENTS.md +18 -216
  41. package/CHANGELOG.md +44 -0
  42. package/README.md +2 -2
  43. package/docs/architecture.md +13 -7
  44. package/docs/catalog.md +26 -27
  45. package/docs/contracts/agents-md-tech-stack.md +74 -0
  46. package/docs/contracts/linear-ai-rules-inclusion.md +1 -1
  47. package/docs/contracts/package-self-orientation.md +135 -0
  48. package/docs/contracts/rule-classification.md +4 -4
  49. package/docs/decisions/ADR-004-rule-governance-pruning.md +240 -0
  50. package/docs/getting-started.md +1 -1
  51. package/docs/guidelines/agent-infra/review-routing-data-format.md +1 -2
  52. package/package.json +1 -1
  53. package/scripts/_p4_migrate.py +5 -5
  54. package/scripts/audit_auto_rules.py +159 -0
  55. package/scripts/audit_likelihood.py +148 -0
  56. package/scripts/audit_overlap.py +145 -0
  57. package/scripts/build_rule_trigger_matrix.py +3 -5
  58. package/scripts/check_augment_description_cap.py +79 -0
  59. package/scripts/check_council_references.py +3 -3
  60. package/scripts/check_kernel_rule_bundle.py +151 -0
  61. package/scripts/check_references.py +21 -1
  62. package/scripts/compile_router.py +3 -0
  63. package/scripts/install.sh +0 -1
  64. package/scripts/lint_agents_md.py +168 -0
  65. package/scripts/measure_augment_budget.py +208 -0
  66. package/scripts/schemas/rule.schema.json +2 -1
  67. package/scripts/skill_linter.py +10 -4
  68. package/scripts/spotcheck_thin_root.py +134 -0
  69. package/scripts/update_counts.py +6 -10
  70. package/.agent-src/rules/no-council-references.md +0 -76
  71. package/.agent-src/rules/review-routing-awareness.md +0 -19
  72. package/.agent-src/templates/copilot-review-instructions.md +0 -76
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """Thin-Root contract linter for AGENTS.md files (Phase 7).
3
+
4
+ Enforces caps + pointer-ratio + pointer-anatomy + emergency-triage
5
+ contract from `.agent-src.uncompressed/skills/agents-md-thin-root/SKILL.md`:
6
+
7
+ (a) total char-count under FAIL/WARN budgets per file class
8
+ (b) substantive-pointer ratio >= 0.40
9
+ (c) every pointer's *why* clause >= 60 chars
10
+ (d) every pointer target resolves on disk (anchor validity)
11
+ (e) emergency-triage section present with the five canonical questions
12
+
13
+ Exit non-zero on any (a) FAIL, (b)–(e) error. WARN is informational.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ import sys
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ ROOT = Path(__file__).resolve().parent.parent
23
+ QUIET = "--quiet" in sys.argv
24
+
25
+ LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
26
+ TRIAGE_KEYWORDS = (
27
+ "what is this repo",
28
+ "what language",
29
+ "where do i edit",
30
+ "lint / test / sync",
31
+ "where do the always",
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class Target:
37
+ path: Path
38
+ label: str
39
+ fail_at: int
40
+ warn_at: int
41
+ template: bool # consumer template — relax pointer-target resolution
42
+
43
+
44
+ TARGETS = [
45
+ Target(ROOT / "AGENTS.md", "package-root", 3000, 2800, template=False),
46
+ Target(
47
+ ROOT / ".agent-src.uncompressed" / "templates" / "AGENTS.md",
48
+ "consumer-template", 2500, 2300, template=True,
49
+ ),
50
+ ]
51
+
52
+
53
+ def _strip_links(line: str) -> str:
54
+ return LINK_RE.sub(lambda m: m.group(1), line)
55
+
56
+
57
+ def _resolve(target_str: str, template: bool) -> bool:
58
+ raw = target_str.split("#", 1)[0].strip()
59
+ if raw.startswith("http://") or raw.startswith("https://"):
60
+ return True
61
+ candidates = [ROOT / raw]
62
+ if template and raw.startswith(".augment/"):
63
+ candidates.append(ROOT / raw.replace(".augment/", ".agent-src.uncompressed/", 1))
64
+ candidates.append(ROOT / raw.replace(".augment/", ".agent-src/", 1))
65
+ if raw.startswith(".agent-src/"):
66
+ candidates.append(ROOT / raw.replace(".agent-src/", ".agent-src.uncompressed/", 1))
67
+ return any(c.exists() for c in candidates)
68
+
69
+
70
+ def lint_file(t: Target) -> tuple[bool, list[str], list[str]]:
71
+ """Return (ok, errors, warnings)."""
72
+ errors: list[str] = []
73
+ warnings: list[str] = []
74
+ if not t.path.exists():
75
+ return False, [f"{t.label}: {t.path} not found"], []
76
+
77
+ text = t.path.read_text(encoding="utf-8")
78
+ size = len(text.encode("utf-8"))
79
+
80
+ # (a) size
81
+ if size > t.fail_at:
82
+ errors.append(f"{t.label}: {size} chars > FAIL cap {t.fail_at}")
83
+ elif size > t.warn_at:
84
+ warnings.append(f"{t.label}: {size} chars > WARN cap {t.warn_at}")
85
+
86
+ # Filter out structural lines that are not "prose" the contract
87
+ # asks us to replace with pointers: headings, code fences + content,
88
+ # HTML comments, and Markdown table rows.
89
+ lines = text.splitlines()
90
+ in_fence = False
91
+ in_comment = False
92
+ prose: list[str] = []
93
+ for ln in lines:
94
+ s = ln.strip()
95
+ if not s:
96
+ continue
97
+ if s.startswith("```"):
98
+ in_fence = not in_fence
99
+ continue
100
+ if in_fence:
101
+ continue
102
+ if "<!--" in s:
103
+ in_comment = True
104
+ if in_comment:
105
+ if "-->" in s:
106
+ in_comment = False
107
+ continue
108
+ if s.startswith("#"): # heading
109
+ continue
110
+ if s.startswith("|"): # markdown table row / separator
111
+ continue
112
+ prose.append(ln)
113
+
114
+ non_blank = prose
115
+ pointer_lines = 0
116
+
117
+ for ln in non_blank:
118
+ m = LINK_RE.search(ln)
119
+ if not m:
120
+ continue
121
+ target = m.group(2)
122
+ # (d) target resolves
123
+ if not _resolve(target, t.template):
124
+ errors.append(f"{t.label}: broken pointer target `{target}` in line: {ln.strip()[:100]}")
125
+ # (c) why-clause length: line minus link syntax
126
+ why = _strip_links(ln).strip()
127
+ if len(why) >= 60:
128
+ pointer_lines += 1
129
+ # else line has a link but no real why-clause — does not count
130
+
131
+ # (b) ratio
132
+ ratio = pointer_lines / max(len(non_blank), 1)
133
+ if ratio < 0.40:
134
+ errors.append(
135
+ f"{t.label}: substantive-pointer ratio {ratio:.2f} < 0.40 "
136
+ f"({pointer_lines}/{len(non_blank)} non-blank lines)"
137
+ )
138
+
139
+ # (e) emergency-triage block
140
+ lower = text.lower()
141
+ missing = [k for k in TRIAGE_KEYWORDS if k not in lower]
142
+ if missing:
143
+ errors.append(f"{t.label}: emergency-triage block missing keywords: {missing}")
144
+ if "emergency triage" not in lower:
145
+ errors.append(f"{t.label}: missing 'Emergency triage' section heading")
146
+
147
+ return not errors, errors, warnings
148
+
149
+
150
+ def main() -> int:
151
+ rc = 0
152
+ for t in TARGETS:
153
+ ok, errors, warnings = lint_file(t)
154
+ if not QUIET or errors or warnings:
155
+ print(f"== {t.label} ({t.path.relative_to(ROOT)}) ==")
156
+ for w in warnings:
157
+ print(f" ⚠️ {w}")
158
+ for e in errors:
159
+ print(f" ❌ {e}")
160
+ if ok and not warnings and not QUIET:
161
+ print(f" ✅ ok ({t.path.stat().st_size} bytes)")
162
+ if not ok:
163
+ rc = 1
164
+ return rc
165
+
166
+
167
+ if __name__ == "__main__":
168
+ raise SystemExit(main())
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+ """Measure the Augment workspace-guidelines budget (Phase 1.1 of
3
+ road-to-augment-limit-fit).
4
+
5
+ Mirrors Augment's accounting model for the workspace prompt:
6
+
7
+ 1. `AGENTS.md` body (full file, including frontmatter) injected verbatim.
8
+ 2. `always`-type rules under `.augment/rules/` — full body injected.
9
+ 3. `auto`-type rules — only a registry stub is injected per rule:
10
+
11
+ If the user prompt matches the description "<desc>", read the
12
+ file located in <path>
13
+
14
+ The body of an `auto` rule is NOT counted; only the stub line is.
15
+
16
+ The 49,512-char ceiling is the empirical limit observed against the
17
+ Augment Code workspace prompt (2026-05-08 baseline). This script emits
18
+ a per-component breakdown plus the total against that ceiling.
19
+
20
+ Output:
21
+ - Default: stdout summary (totals + per-component breakdown).
22
+ - `--json`: deterministic JSON.
23
+ - `--trend-append`: append a snapshot record to
24
+ `agents/.augment-budget-history.jsonl`.
25
+
26
+ Exit codes: 0 = under fail threshold, 1 = at/above fail threshold,
27
+ 3 = internal error.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import datetime as _dt
34
+ import json
35
+ import re
36
+ import sys
37
+ from pathlib import Path
38
+
39
+ REPO_ROOT = Path(__file__).resolve().parent.parent
40
+ AGENTS_MD = REPO_ROOT / "AGENTS.md"
41
+ RULES_DIR = REPO_ROOT / ".augment" / "rules"
42
+ TREND_FILE = REPO_ROOT / "agents" / ".augment-budget-history.jsonl"
43
+
44
+ # Augment workspace-guidelines ceiling — empirical 2026-05-08.
45
+ TOTAL_CAP = 49_512
46
+ WARN_THRESHOLD = 0.85
47
+ FAIL_THRESHOLD = 0.95
48
+
49
+ # Stub template Augment injects for `type: auto` rules. Measured by
50
+ # subtracting variable-length fields (description, path) from a real
51
+ # rendered stub in the host system prompt.
52
+ STUB_TEMPLATE = (
53
+ 'If the user prompt matches the description "{desc}", '
54
+ "read the file located in {path}"
55
+ )
56
+
57
+
58
+ def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
59
+ if not text.startswith("---\n"):
60
+ return {}, text
61
+ end = text.find("\n---", 4)
62
+ if end < 0:
63
+ return {}, text
64
+ fm_block = text[4:end]
65
+ body = text[end + 4 :].lstrip("\n")
66
+ fm: dict[str, str] = {}
67
+ for line in fm_block.splitlines():
68
+ m = re.match(r"^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$", line)
69
+ if m:
70
+ fm[m.group(1)] = m.group(2).strip().strip('"').strip("'")
71
+ return fm, body
72
+
73
+
74
+ def measure() -> dict:
75
+ components: dict[str, dict] = {}
76
+
77
+ # 1. AGENTS.md
78
+ agents_text = AGENTS_MD.read_text() if AGENTS_MD.exists() else ""
79
+ components["agents_md"] = {
80
+ "path": str(AGENTS_MD.relative_to(REPO_ROOT)),
81
+ "chars": len(agents_text),
82
+ }
83
+
84
+ # 2 + 3. Rules under .augment/rules/.
85
+ always_total = 0
86
+ always_rules: list[dict] = []
87
+ auto_total = 0
88
+ auto_rules: list[dict] = []
89
+
90
+ for rule_path in sorted(RULES_DIR.glob("*.md")):
91
+ text = rule_path.read_text()
92
+ fm, _body = parse_frontmatter(text)
93
+ rtype = fm.get("type", "")
94
+ rel = str(rule_path.relative_to(REPO_ROOT))
95
+ if rtype == "always":
96
+ chars = len(text)
97
+ always_total += chars
98
+ always_rules.append({"path": rel, "chars": chars})
99
+ elif rtype == "auto":
100
+ desc = fm.get("description", "")
101
+ stub = STUB_TEMPLATE.format(desc=desc, path=rel)
102
+ chars = len(stub)
103
+ auto_total += chars
104
+ auto_rules.append(
105
+ {"path": rel, "desc_chars": len(desc), "stub_chars": chars}
106
+ )
107
+
108
+ components["always_rules"] = {
109
+ "count": len(always_rules),
110
+ "chars": always_total,
111
+ "rules": sorted(always_rules, key=lambda r: -r["chars"]),
112
+ }
113
+ components["auto_rules"] = {
114
+ "count": len(auto_rules),
115
+ "chars": auto_total,
116
+ "rules": sorted(auto_rules, key=lambda r: -r["stub_chars"]),
117
+ }
118
+
119
+ total = (
120
+ components["agents_md"]["chars"]
121
+ + always_total
122
+ + auto_total
123
+ )
124
+ return {
125
+ "ts": _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
126
+ "total": total,
127
+ "cap": TOTAL_CAP,
128
+ "utilisation": round(total / TOTAL_CAP, 4),
129
+ "components": components,
130
+ }
131
+
132
+
133
+ def render_text(data: dict) -> str:
134
+ total = data["total"]
135
+ cap = data["cap"]
136
+ util = data["utilisation"]
137
+ a = data["components"]["agents_md"]["chars"]
138
+ ar = data["components"]["always_rules"]
139
+ aur = data["components"]["auto_rules"]
140
+ lines = [
141
+ f"Augment workspace-guidelines budget — cap {cap:,} chars",
142
+ "",
143
+ f" AGENTS.md {a:>6,} chars ({a/cap*100:5.1f}%)",
144
+ f" always-rules ({ar['count']:>2}) {ar['chars']:>6,} chars ({ar['chars']/cap*100:5.1f}%)",
145
+ f" auto-rule stubs ({aur['count']:>2}) {aur['chars']:>6,} chars ({aur['chars']/cap*100:5.1f}%)",
146
+ " " + "-" * 50,
147
+ f" TOTAL {total:>6,} chars ({util*100:5.1f}%)",
148
+ "",
149
+ ]
150
+ if util >= 1.0:
151
+ lines.append(f"❌ OVER CAP by {total - cap:,} chars")
152
+ elif util >= FAIL_THRESHOLD:
153
+ lines.append(f"❌ FAIL — utilisation {util*100:.1f}% ≥ {FAIL_THRESHOLD*100:.0f}%")
154
+ elif util >= WARN_THRESHOLD:
155
+ lines.append(f"⚠️ WARN — utilisation {util*100:.1f}% ≥ {WARN_THRESHOLD*100:.0f}%")
156
+ else:
157
+ lines.append(f"✅ OK — utilisation {util*100:.1f}%")
158
+ return "\n".join(lines)
159
+
160
+
161
+ def main() -> int:
162
+ parser = argparse.ArgumentParser(description=__doc__)
163
+ parser.add_argument("--json", action="store_true", help="Emit JSON")
164
+ parser.add_argument(
165
+ "--trend-append",
166
+ action="store_true",
167
+ help="Append a snapshot record to agents/.augment-budget-history.jsonl",
168
+ )
169
+ parser.add_argument(
170
+ "--check",
171
+ action="store_true",
172
+ help="Exit non-zero when utilisation ≥ FAIL_THRESHOLD or over cap",
173
+ )
174
+ args = parser.parse_args()
175
+
176
+ data = measure()
177
+
178
+ if args.trend_append:
179
+ TREND_FILE.parent.mkdir(parents=True, exist_ok=True)
180
+ rec = {
181
+ "ts": data["ts"],
182
+ "total": data["total"],
183
+ "cap": data["cap"],
184
+ "utilisation": data["utilisation"],
185
+ "agents_md": data["components"]["agents_md"]["chars"],
186
+ "always_rules": data["components"]["always_rules"]["chars"],
187
+ "auto_rules": data["components"]["auto_rules"]["chars"],
188
+ }
189
+ with TREND_FILE.open("a") as fh:
190
+ fh.write(json.dumps(rec, sort_keys=True) + "\n")
191
+
192
+ if args.json:
193
+ print(json.dumps(data, indent=2, sort_keys=True))
194
+ else:
195
+ print(render_text(data))
196
+
197
+ if args.check:
198
+ if data["utilisation"] >= 1.0 or data["utilisation"] >= FAIL_THRESHOLD:
199
+ return 1
200
+ return 0
201
+
202
+
203
+ if __name__ == "__main__":
204
+ try:
205
+ sys.exit(main())
206
+ except Exception as exc: # pragma: no cover - defensive top-level guard
207
+ print(f"❌ measure_augment_budget: internal error: {exc}", file=sys.stderr)
208
+ sys.exit(3)
@@ -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",
@@ -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
 
@@ -683,6 +683,12 @@ def lint_router_frontmatter(rule_id: str, frontmatter: str,
683
683
  triggers = _parse_yaml_list(frontmatter, "triggers")
684
684
  routes_to = _parse_yaml_list(frontmatter, "routes_to")
685
685
 
686
+ # Manual rules are reference-only — not auto-injected, not router-routed
687
+ # (ADR-004). Skip router validation so legacy triggers/routes_to fields
688
+ # remain documented in the rule body without forcing maintenance.
689
+ if rule_type == "manual":
690
+ return issues
691
+
686
692
  is_kernel = rule_id in KERNEL_RULE_IDS or rule_type == "always"
687
693
 
688
694
  if is_kernel:
@@ -961,9 +967,9 @@ def lint_rule(path: Path, text: str) -> LintResult:
961
967
  # type field
962
968
  rule_type = extract_frontmatter_field(frontmatter, TYPE_PATTERN)
963
969
  if rule_type is None:
964
- issues.append(Issue("error", "missing_type", "Frontmatter missing 'type' field (must be 'always' or 'auto')"))
970
+ issues.append(Issue("error", "missing_type", "Frontmatter missing 'type' field (must be 'always', 'auto', or 'manual')"))
965
971
  elif rule_type not in VALID_RULE_TYPES:
966
- issues.append(Issue("error", "invalid_type", f"Invalid type '{rule_type}'; must be 'always' or 'auto'"))
972
+ issues.append(Issue("error", "invalid_type", f"Invalid type '{rule_type}'; must be 'always', 'auto', or 'manual'"))
967
973
 
968
974
  # source field
969
975
  rule_source = extract_frontmatter_field(frontmatter, SOURCE_PATTERN)
@@ -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())
@@ -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
- "AGENTS.md",
72
- [
73
- (r"(skills/\s+\()(\d+)( skills\))", "skills"),
74
- (r"(rules/\s+\()(\d+)( rules\))", "rules"),
75
- (r"(commands/\s+\()(\d+)( commands\))", "commands"),
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
  [
@@ -1,76 +0,0 @@
1
- ---
2
- type: "auto"
3
- tier: "mechanical-already"
4
- description: "Linking a specific file in agents/council-{questions,responses,sessions}/ from any artifact — council files are gitignored, local-only, auto-pruned; inline the convergence instead"
5
- alwaysApply: false
6
- source: package
7
- triggers:
8
- - path_prefix: "agents/council-questions/"
9
- - path_prefix: "agents/council-responses/"
10
- - path_prefix: "agents/council-sessions/"
11
- - intent: "link to council artefact"
12
- routes_to:
13
- - "skill:ai-council"
14
- validator_ignore:
15
- - type: "substring"
16
- pattern: ".agent-src.uncompressed/"
17
- reason: "Rule references the authoring tree when contrasting transient council files."
18
- ---
19
-
20
- # No Council References from Any Artifact
21
-
22
- Council artefacts under `agents/council-{questions,responses,sessions}/`
23
- are **gitignored, local-only, and auto-pruned** after
24
- `ai_council.session_retention_days` (default 7). They are
25
- disposable scratch — never part of the repo, never visible to a
26
- reviewer who clones, never durable across the retention window.
27
-
28
- A link to a specific council file rots three ways: gitignored
29
- (not in cloned repo), pruned after retention window (gone even
30
- locally), and the installed `.augment/` projection cannot follow a
31
- path that does not exist in the consumer.
32
-
33
- ## The Iron Law
34
-
35
- ```
36
- NEVER LINK TO A SPECIFIC FILE INSIDE
37
- agents/council-{questions,responses,sessions}/
38
- FROM ANY ARTIFACT — ROADMAPS INCLUDED.
39
- INLINE THE CONVERGENCE WITH DATE + MEMBERS, NEVER THE PATH.
40
- ```
41
-
42
- Applies to **every** artifact. Council artefacts are more transient
43
- than roadmaps — the local copy disappears too.
44
-
45
- ## Forbidden vs allowed
46
-
47
- **Forbidden** in any `*.md` / `*.yml` / `*.json` / `*.py`:
48
- `agents/council-questions/<file>.md`,
49
- `agents/council-responses/<file>.json`,
50
- `agents/council-sessions/<file>.json` or `<timestamp>/...`.
51
-
52
- **Allowed**: directory mentions (talking about the output convention,
53
- not a specific file); the `ai-council` skill and `/council:*` commands
54
- documenting the output path schema; inline convergence summary —
55
- e.g. *"Council (claude-sonnet-4-5 + gpt-4o, 2026-05-06) converged
56
- on …"* with date + members, no filepath.
57
-
58
- ## What to do instead
59
-
60
- Identify the durable conclusion (decision, contract, lesson),
61
- inline a convergence-summary block (members, date, cost if relevant
62
- — see `ai-council` § Output format), and optionally promote the
63
- lesson to `agents/contexts/`. The context is durable; the council
64
- file was the catalyst.
65
-
66
- Failure mode: *"I'll just link to the session JSON, it's evidence."*
67
- The session is gone in 7 days. **Inline first, link never.**
68
-
69
- ## See also
70
-
71
- - [`no-roadmap-references`](no-roadmap-references.md) — sibling rule
72
- for the roadmap layer
73
- - [`augment-source-of-truth`](augment-source-of-truth.md) — edit
74
- `.agent-src.uncompressed/`
75
- - [`ai-council`](../skills/ai-council/SKILL.md) — output path
76
- convention and convergence-summary format
@@ -1,19 +0,0 @@
1
- ---
2
- type: "auto"
3
- tier: "2a"
4
- description: "When routing reviewers or flagging risk hotspots — consult ownership-map and historical-bug-patterns before suggesting reviewers or claiming a change is safe"
5
- source: package
6
- triggers:
7
- - keyword: "reviewer"
8
- - phrase: "risk hotspot"
9
- - phrase: "ownership map"
10
- routes_to:
11
- - "skill:review-routing"
12
- ---
13
-
14
- # Review Routing Awareness
15
-
16
- **Iron Law.** Consult ownership-map and historical-bug-patterns before suggesting reviewers or claiming a change is safe.
17
-
18
- Body migrated to `skill:review-routing` (per P4 of `road-to-kernel-and-router.md`).
19
- Trigger-set above activates this routing under the `balanced` and `full` profiles.