@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,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check_kernel_rule_bundle — Phase 4.2 of road-to-always-budget-relief.
|
|
3
|
+
|
|
4
|
+
Fails when a single PR (or commit range) modifies more than one
|
|
5
|
+
kernel rule under `.agent-src.uncompressed/rules/`. Override via the
|
|
6
|
+
PR label `bundled-always-rules-acknowledged`.
|
|
7
|
+
|
|
8
|
+
Kernel set is the locked 9-rule list in
|
|
9
|
+
`docs/contracts/rule-classification.md` § 3.1, mirrored as
|
|
10
|
+
`KERNEL_RULES` below. The list is short and stable; on kernel-set
|
|
11
|
+
change, update both files in the same PR.
|
|
12
|
+
|
|
13
|
+
Inputs:
|
|
14
|
+
--base-ref REF git ref to diff against (default: origin/main, then main)
|
|
15
|
+
--label NAME PR label that overrides the gate (default:
|
|
16
|
+
bundled-always-rules-acknowledged)
|
|
17
|
+
--event-path P GitHub event JSON (defaults to $GITHUB_EVENT_PATH)
|
|
18
|
+
--files F [F …] override changed-file list (testing only)
|
|
19
|
+
|
|
20
|
+
Exit codes: 0 = pass · 1 = fail (> 1 kernel rule, no override) ·
|
|
21
|
+
3 = internal error.
|
|
22
|
+
|
|
23
|
+
Source: `agents/contexts/adr-always-budget-relief-strategy.md`.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
KERNEL_RULES = frozenset({
|
|
35
|
+
"agent-authority.md",
|
|
36
|
+
"ask-when-uncertain.md",
|
|
37
|
+
"commit-policy.md",
|
|
38
|
+
"direct-answers.md",
|
|
39
|
+
"language-and-tone.md",
|
|
40
|
+
"no-cheap-questions.md",
|
|
41
|
+
"non-destructive-by-default.md",
|
|
42
|
+
"scope-control.md",
|
|
43
|
+
"verify-before-complete.md",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
KERNEL_DIR = ".agent-src.uncompressed/rules"
|
|
47
|
+
DEFAULT_LABEL = "bundled-always-rules-acknowledged"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _git_changed_files(base_ref: str) -> list[str]:
|
|
51
|
+
try:
|
|
52
|
+
out = subprocess.check_output(
|
|
53
|
+
["git", "diff", "--name-only", f"{base_ref}...HEAD"],
|
|
54
|
+
stderr=subprocess.STDOUT,
|
|
55
|
+
text=True,
|
|
56
|
+
)
|
|
57
|
+
except subprocess.CalledProcessError as exc:
|
|
58
|
+
print(f"❌ git diff failed: {exc.output.strip()}", file=sys.stderr)
|
|
59
|
+
return []
|
|
60
|
+
return [line for line in out.splitlines() if line.strip()]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_base_ref(explicit: str | None) -> str:
|
|
64
|
+
if explicit:
|
|
65
|
+
return explicit
|
|
66
|
+
for candidate in ("origin/main", "origin/master", "main", "master"):
|
|
67
|
+
try:
|
|
68
|
+
subprocess.check_output(
|
|
69
|
+
["git", "rev-parse", "--verify", candidate],
|
|
70
|
+
stderr=subprocess.DEVNULL,
|
|
71
|
+
)
|
|
72
|
+
return candidate
|
|
73
|
+
except subprocess.CalledProcessError:
|
|
74
|
+
continue
|
|
75
|
+
return "HEAD~1"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _pr_labels(event_path: str | None) -> list[str]:
|
|
79
|
+
path = event_path or os.environ.get("GITHUB_EVENT_PATH")
|
|
80
|
+
if not path or not Path(path).exists():
|
|
81
|
+
return []
|
|
82
|
+
try:
|
|
83
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
84
|
+
except (OSError, json.JSONDecodeError):
|
|
85
|
+
return []
|
|
86
|
+
pr = data.get("pull_request") or {}
|
|
87
|
+
return [lbl.get("name", "") for lbl in pr.get("labels", []) if lbl.get("name")]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _kernel_changes(files: list[str]) -> list[str]:
|
|
91
|
+
hits: list[str] = []
|
|
92
|
+
for path in files:
|
|
93
|
+
if not path.startswith(f"{KERNEL_DIR}/"):
|
|
94
|
+
continue
|
|
95
|
+
name = Path(path).name
|
|
96
|
+
if name in KERNEL_RULES:
|
|
97
|
+
hits.append(path)
|
|
98
|
+
return sorted(set(hits))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main(argv: list[str] | None = None) -> int:
|
|
102
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
103
|
+
ap.add_argument("--base-ref", default=None)
|
|
104
|
+
ap.add_argument("--label", default=DEFAULT_LABEL)
|
|
105
|
+
ap.add_argument("--event-path", default=None)
|
|
106
|
+
ap.add_argument("--files", nargs="*", default=None)
|
|
107
|
+
args = ap.parse_args(argv)
|
|
108
|
+
|
|
109
|
+
files = args.files or _git_changed_files(_resolve_base_ref(args.base_ref))
|
|
110
|
+
hits = _kernel_changes(files)
|
|
111
|
+
|
|
112
|
+
if len(hits) <= 1:
|
|
113
|
+
if hits:
|
|
114
|
+
print(f"✅ OK kernel-rule bundle: 1 rule touched ({hits[0]})")
|
|
115
|
+
else:
|
|
116
|
+
print("✅ OK kernel-rule bundle: no kernel rule touched")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
labels = _pr_labels(args.event_path)
|
|
120
|
+
if args.label in labels:
|
|
121
|
+
print(
|
|
122
|
+
f"✅ OK kernel-rule bundle: {len(hits)} rules touched but "
|
|
123
|
+
f"label '{args.label}' present"
|
|
124
|
+
)
|
|
125
|
+
for h in hits:
|
|
126
|
+
print(f" · {h}")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
print(
|
|
130
|
+
f"❌ FAIL kernel-rule bundle: {len(hits)} kernel rules touched in "
|
|
131
|
+
f"one PR — slow-rollout requires one-rule-per-PR.",
|
|
132
|
+
file=sys.stderr,
|
|
133
|
+
)
|
|
134
|
+
print(" Touched:", file=sys.stderr)
|
|
135
|
+
for h in hits:
|
|
136
|
+
print(f" · {h}", file=sys.stderr)
|
|
137
|
+
print(
|
|
138
|
+
f" Override: add the label '{args.label}' on the PR and "
|
|
139
|
+
f"document the bundle rationale in the PR body.",
|
|
140
|
+
file=sys.stderr,
|
|
141
|
+
)
|
|
142
|
+
print(
|
|
143
|
+
" Source: agents/contexts/adr-always-budget-relief-strategy.md "
|
|
144
|
+
"(Phase 4.2).",
|
|
145
|
+
file=sys.stderr,
|
|
146
|
+
)
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
sys.exit(main())
|
|
@@ -228,6 +228,10 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
|
|
|
228
228
|
))
|
|
229
229
|
|
|
230
230
|
in_code_block = False
|
|
231
|
+
# Track whether we are inside an unchecked-TODO bullet (multi-line
|
|
232
|
+
# roadmap items wrap continuation text under the `- [ ]` line and
|
|
233
|
+
# those continuation lines must inherit the forward-ref exemption).
|
|
234
|
+
in_unchecked_todo = False
|
|
231
235
|
for i, line in enumerate(text.splitlines(), 1):
|
|
232
236
|
stripped = line.strip()
|
|
233
237
|
if stripped.startswith("```"):
|
|
@@ -237,9 +241,25 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
|
|
|
237
241
|
continue
|
|
238
242
|
|
|
239
243
|
# Unchecked TODO checkboxes document future work — their refs are
|
|
240
|
-
# forward-looking and will not resolve yet.
|
|
244
|
+
# forward-looking and will not resolve yet. Track multi-line bullets:
|
|
245
|
+
# any `- [ ]` opens a TODO context; a new top-level bullet, heading,
|
|
246
|
+
# or blank line closes it.
|
|
241
247
|
if UNCHECKED_TODO_PATTERN.match(line):
|
|
248
|
+
in_unchecked_todo = True
|
|
242
249
|
continue
|
|
250
|
+
if in_unchecked_todo:
|
|
251
|
+
if not stripped:
|
|
252
|
+
in_unchecked_todo = False
|
|
253
|
+
continue
|
|
254
|
+
# A new bullet (checked or unchecked) or a heading closes the
|
|
255
|
+
# current TODO context. An indented continuation line keeps it.
|
|
256
|
+
if re.match(r"^[-*+]\s+\[", line) or stripped.startswith("#"):
|
|
257
|
+
in_unchecked_todo = False
|
|
258
|
+
elif line[:1] in (" ", "\t"):
|
|
259
|
+
# Indented continuation of the unchecked TODO — skip.
|
|
260
|
+
continue
|
|
261
|
+
else:
|
|
262
|
+
in_unchecked_todo = False
|
|
243
263
|
|
|
244
264
|
# File path references
|
|
245
265
|
for m in PATH_PATTERN.finditer(line):
|
|
@@ -125,6 +125,9 @@ def _collect(rules_dir: Path) -> dict:
|
|
|
125
125
|
if not COMPILE_TIME_TOGGLES[rule_id](settings):
|
|
126
126
|
continue
|
|
127
127
|
rule_type = str(fm.get("type", "auto"))
|
|
128
|
+
# Manual rules are reference-only (ADR-004) — no router emission.
|
|
129
|
+
if rule_type == "manual":
|
|
130
|
+
continue
|
|
128
131
|
tier = _resolve_tier(rule_type, fm.get("tier", ""))
|
|
129
132
|
if tier not in ALLOWED_TIERS:
|
|
130
133
|
continue
|
package/scripts/install.sh
CHANGED
|
@@ -728,7 +728,6 @@ main() {
|
|
|
728
728
|
# into consumer projects.
|
|
729
729
|
copy_if_missing "$SOURCE_PAYLOAD/templates/AGENTS.md" "$TARGET_DIR/AGENTS.md"
|
|
730
730
|
copy_if_missing "$SOURCE_PAYLOAD/templates/copilot-instructions.md" "$TARGET_DIR/.github/copilot-instructions.md"
|
|
731
|
-
copy_if_missing "$SOURCE_PAYLOAD/templates/copilot-review-instructions.md" "$TARGET_DIR/.github/copilot-review-instructions.md"
|
|
732
731
|
|
|
733
732
|
# 3. Create tool-specific symlinks
|
|
734
733
|
create_tool_symlinks "$TARGET_DIR"
|
|
@@ -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)
|