@event4u/agent-config 1.22.0 → 1.24.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/agents/cleanup.md +31 -17
- package/.agent-src/commands/analyze-reference-repo.md +3 -0
- package/.agent-src/commands/commit/in-chunks.md +30 -10
- package/.agent-src/commands/commit.md +46 -6
- package/.agent-src/commands/compress.md +19 -13
- package/.agent-src/commands/cost-report.md +120 -0
- package/.agent-src/commands/create-pr/description-only.md +8 -0
- package/.agent-src/commands/create-pr.md +95 -80
- package/.agent-src/commands/feature/plan.md +13 -7
- package/.agent-src/commands/memory/add.md +16 -8
- package/.agent-src/commands/memory/promote.md +17 -9
- package/.agent-src/commands/optimize/rtk.md +16 -11
- package/.agent-src/commands/prepare-for-review.md +12 -6
- package/.agent-src/commands/project-analyze.md +31 -20
- package/.agent-src/commands/review-changes.md +24 -15
- package/.agent-src/commands/roadmap/create.md +14 -9
- package/.agent-src/commands/roadmap/process-full.md +41 -1
- package/.agent-src/contexts/contracts/frugality-charter.md +57 -0
- package/.agent-src/contexts/execution/roadmap-process-loop.md +29 -6
- package/.agent-src/rules/architecture.md +9 -0
- package/.agent-src/rules/ask-when-uncertain.md +3 -13
- package/.agent-src/rules/caveman-speak.md +78 -0
- package/.agent-src/rules/direct-answers.md +5 -14
- package/.agent-src/rules/markdown-safe-codeblocks.md +6 -7
- package/.agent-src/rules/no-cheap-questions.md +4 -14
- package/.agent-src/rules/roadmap-progress-sync.md +37 -3
- package/.agent-src/rules/token-efficiency.md +5 -7
- package/.agent-src/skills/adr-create/SKILL.md +197 -0
- package/.agent-src/skills/agent-docs-writing/SKILL.md +23 -1
- package/.agent-src/skills/command-writing/SKILL.md +23 -0
- package/.agent-src/skills/context-authoring/SKILL.md +23 -0
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +23 -0
- package/.agent-src/skills/guideline-writing/SKILL.md +22 -0
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +9 -0
- package/.agent-src/skills/markitdown/SKILL.md +239 -0
- package/.agent-src/skills/persona-writing/SKILL.md +153 -0
- package/.agent-src/skills/readme-writing/SKILL.md +20 -0
- package/.agent-src/skills/readme-writing-package/SKILL.md +19 -0
- package/.agent-src/skills/roadmap-writing/SKILL.md +157 -0
- package/.agent-src/skills/rule-writing/SKILL.md +22 -0
- package/.agent-src/skills/script-writing/SKILL.md +226 -0
- package/.agent-src/skills/skill-writing/SKILL.md +23 -0
- package/.agent-src/skills/test-driven-development/SKILL.md +24 -0
- package/.agent-src/skills/universal-project-analysis/SKILL.md +8 -0
- package/.agent-src/templates/agent-settings.md +73 -0
- package/.agent-src/templates/command.md +15 -10
- package/.agent-src/templates/rule.md +6 -0
- package/.agent-src/templates/skill.md +32 -0
- package/.claude-plugin/marketplace.json +10 -4
- package/AGENTS.md +14 -3
- package/CHANGELOG.md +61 -0
- package/README.md +5 -5
- package/docs/architecture.md +4 -4
- package/docs/catalog.md +25 -8
- package/docs/customization.md +72 -0
- package/docs/decisions/INDEX.md +15 -0
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -19
- package/docs/guidelines/agent-infra/carve-out-predicates.md +17 -0
- package/docs/guidelines/agent-infra/mcp-request-signing.md +199 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +11 -4
- package/package.json +1 -1
- package/scripts/_lib/__init__.py +5 -0
- package/scripts/_lib/script_output.py +140 -0
- package/scripts/adr/regenerate_index.py +79 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_add_quiet.py +149 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_inject_quiet_flag.py +33 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_v2.sh +36 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_measure_verbosity.sh +26 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_per_task.sh +41 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_silent_taskfiles.py +98 -0
- package/scripts/check_augmentignore.py +4 -1
- package/scripts/check_command_count_messaging.py +4 -1
- package/scripts/check_compressed_paths.py +4 -1
- package/scripts/check_council_layout.py +4 -1
- package/scripts/check_council_references.py +4 -1
- package/scripts/check_iron_law_prominence.py +3 -1
- package/scripts/check_md_language.py +3 -1
- package/scripts/check_memory_proposal.py +3 -1
- package/scripts/check_public_catalog_links.py +4 -1
- package/scripts/check_reply_consistency.py +8 -2
- package/scripts/check_roadmap_trackable.py +4 -1
- package/scripts/compile_router.py +27 -0
- package/scripts/compress.py +33 -19
- package/scripts/cost/budget.mjs +152 -0
- package/scripts/cost/track.mjs +144 -0
- package/scripts/first-run.sh +3 -9
- package/scripts/install-hooks.sh +19 -1
- package/scripts/install.py +17 -12
- package/scripts/install.sh +19 -8
- package/scripts/lint_examples.py +6 -2
- package/scripts/lint_handoffs.py +4 -1
- package/scripts/lint_load_context.py +4 -1
- package/scripts/lint_roadmap_complexity.py +6 -2
- package/scripts/lint_rule_interactions.py +4 -1
- package/scripts/lint_rule_tiers.py +4 -1
- package/scripts/measure_frugality_savings.py +164 -0
- package/scripts/measure_markitdown_lift.py +127 -0
- package/scripts/runtime_dispatcher.py +11 -0
- package/scripts/skill_linter.py +207 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 0 baseline harness for road-to-trim-frugality-canon.
|
|
3
|
+
|
|
4
|
+
Measures the *current state* of the frugality canon along four
|
|
5
|
+
deterministic axes. Output: JSONL baseline appended to
|
|
6
|
+
agents/.frugality-baseline.jsonl (gitignored).
|
|
7
|
+
|
|
8
|
+
Metrics:
|
|
9
|
+
A. footprint — per-rule char/token count, kernel/tier breakdown
|
|
10
|
+
B. fillers — filler-phrase prevalence in chat-history corpus
|
|
11
|
+
(heuristic signal, not full transcript)
|
|
12
|
+
C. compression — uncompressed → compressed char delta per rule
|
|
13
|
+
D. redundancy — cross-ref overlap across "Interactions:" /
|
|
14
|
+
"See also" sections in the canon
|
|
15
|
+
|
|
16
|
+
Trim phases re-run this harness after each PR. Decline condition fires
|
|
17
|
+
if metric B regresses (filler prevalence increases) or metric C drops
|
|
18
|
+
below current baseline by >10% per rule.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
CANON_RULES = [
|
|
28
|
+
("direct-answers", "kernel"),
|
|
29
|
+
("no-cheap-questions", "kernel"),
|
|
30
|
+
("ask-when-uncertain", "kernel"),
|
|
31
|
+
("user-interaction", "tier_1"),
|
|
32
|
+
("caveman-speak", "tier_1"),
|
|
33
|
+
("token-efficiency", "tier_2"),
|
|
34
|
+
]
|
|
35
|
+
CHARTER = "frugality-charter"
|
|
36
|
+
|
|
37
|
+
FILLER_PATTERNS = [
|
|
38
|
+
r"\bgreat question\b", r"\bfascinating\b", r"\bexcellent point\b",
|
|
39
|
+
r"\blet me\s+(check|look|find|verify|investigate|see)\b",
|
|
40
|
+
r"\bnow\s+(i'll|i will|let's)\b",
|
|
41
|
+
r"\bgoing to\s+(check|run|use|call|invoke)\b",
|
|
42
|
+
r"\bperfect\b!?", r"\bawesome\b!?",
|
|
43
|
+
r"\bhere's what i\b", r"\bfound it\b",
|
|
44
|
+
r"^\s*(ok|okay|alright)[!,.]\s",
|
|
45
|
+
]
|
|
46
|
+
FILLER_RE = re.compile("|".join(FILLER_PATTERNS), re.IGNORECASE | re.MULTILINE)
|
|
47
|
+
|
|
48
|
+
# Cross-ref section headers to count for redundancy metric
|
|
49
|
+
XREF_HEADERS = re.compile(r"^##\s+(Interactions|See also|Related)\s*$", re.MULTILINE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read(path: Path) -> str:
|
|
53
|
+
return path.read_text() if path.exists() else ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def metric_a_footprint(root: Path) -> dict:
|
|
57
|
+
"""Per-rule char count, tier classification, total kernel %."""
|
|
58
|
+
rows = []
|
|
59
|
+
kernel_total = 0
|
|
60
|
+
tier1_total = 0
|
|
61
|
+
tier2_total = 0
|
|
62
|
+
for name, tier in CANON_RULES:
|
|
63
|
+
compressed = root / ".agent-src" / "rules" / f"{name}.md"
|
|
64
|
+
chars = len(_read(compressed))
|
|
65
|
+
tokens = chars // 4 # rough 4-char/token approximation
|
|
66
|
+
rows.append({"rule": name, "tier": tier, "chars": chars, "tokens_approx": tokens})
|
|
67
|
+
if tier == "kernel":
|
|
68
|
+
kernel_total += chars
|
|
69
|
+
elif tier == "tier_1":
|
|
70
|
+
tier1_total += chars
|
|
71
|
+
elif tier == "tier_2":
|
|
72
|
+
tier2_total += chars
|
|
73
|
+
charter_chars = len(_read(root / ".agent-src" / "contexts" / "contracts" / f"{CHARTER}.md"))
|
|
74
|
+
return {
|
|
75
|
+
"rules": rows,
|
|
76
|
+
"kernel_total_chars": kernel_total,
|
|
77
|
+
"tier_1_total_chars": tier1_total,
|
|
78
|
+
"tier_2_total_chars": tier2_total,
|
|
79
|
+
"charter_chars": charter_chars,
|
|
80
|
+
"kernel_budget_chars": 26000,
|
|
81
|
+
"kernel_pct": round(100 * kernel_total / 26000, 2),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def metric_b_fillers(corpus: Path) -> dict:
|
|
86
|
+
"""Filler-phrase hits per agent turn in chat-history corpus."""
|
|
87
|
+
if not corpus.exists():
|
|
88
|
+
return {"corpus_present": False}
|
|
89
|
+
lines = corpus.read_text().splitlines()
|
|
90
|
+
agent_turns = 0
|
|
91
|
+
filler_hits = 0
|
|
92
|
+
total_chars = 0
|
|
93
|
+
for ln in lines[1:]:
|
|
94
|
+
try:
|
|
95
|
+
d = json.loads(ln)
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
continue
|
|
98
|
+
if d.get("t") != "agent":
|
|
99
|
+
continue
|
|
100
|
+
text = d.get("text", "")
|
|
101
|
+
agent_turns += 1
|
|
102
|
+
total_chars += len(text)
|
|
103
|
+
filler_hits += len(FILLER_RE.findall(text))
|
|
104
|
+
return {
|
|
105
|
+
"corpus_present": True,
|
|
106
|
+
"agent_turns": agent_turns,
|
|
107
|
+
"filler_hits_total": filler_hits,
|
|
108
|
+
"filler_hits_per_turn": round(filler_hits / max(agent_turns, 1), 3),
|
|
109
|
+
"agent_chars_total": total_chars,
|
|
110
|
+
"patterns_count": len(FILLER_PATTERNS),
|
|
111
|
+
"note": "chat-history texts are digests, not full transcripts; signal not output volume",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def metric_c_compression(root: Path) -> dict:
|
|
116
|
+
"""Uncompressed → compressed char delta per rule."""
|
|
117
|
+
rows = []
|
|
118
|
+
for name, _ in CANON_RULES:
|
|
119
|
+
un = len(_read(root / ".agent-src.uncompressed" / "rules" / f"{name}.md"))
|
|
120
|
+
co = len(_read(root / ".agent-src" / "rules" / f"{name}.md"))
|
|
121
|
+
delta = un - co
|
|
122
|
+
ratio = round(co / un, 3) if un else 0
|
|
123
|
+
rows.append({"rule": name, "uncompressed_chars": un, "compressed_chars": co, "delta": delta, "ratio": ratio})
|
|
124
|
+
return {"rules": rows}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def metric_d_redundancy(root: Path) -> dict:
|
|
128
|
+
"""Cross-ref section count + total xref-block size."""
|
|
129
|
+
rows = []
|
|
130
|
+
for name, _ in CANON_RULES:
|
|
131
|
+
path = root / ".agent-src.uncompressed" / "rules" / f"{name}.md"
|
|
132
|
+
text = _read(path)
|
|
133
|
+
xref_count = len(XREF_HEADERS.findall(text))
|
|
134
|
+
# naive: chars after last xref header to EOF
|
|
135
|
+
m = list(XREF_HEADERS.finditer(text))
|
|
136
|
+
xref_block_chars = (len(text) - m[-1].start()) if m else 0
|
|
137
|
+
rows.append({"rule": name, "xref_sections": xref_count, "xref_block_chars": xref_block_chars})
|
|
138
|
+
return {"rules": rows, "total_xref_chars": sum(r["xref_block_chars"] for r in rows)}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main() -> int:
|
|
142
|
+
root = Path(__file__).resolve().parent.parent
|
|
143
|
+
corpus = root / "agents" / ".agent-chat-history"
|
|
144
|
+
|
|
145
|
+
record = {
|
|
146
|
+
"schema_version": 1,
|
|
147
|
+
"ts": datetime.now(tz=timezone.utc).isoformat(timespec="seconds"),
|
|
148
|
+
"phase": "phase_0_baseline",
|
|
149
|
+
"metric_a_footprint": metric_a_footprint(root),
|
|
150
|
+
"metric_b_fillers": metric_b_fillers(corpus),
|
|
151
|
+
"metric_c_compression": metric_c_compression(root),
|
|
152
|
+
"metric_d_redundancy": metric_d_redundancy(root),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
out = root / "agents" / ".frugality-baseline.jsonl"
|
|
156
|
+
with out.open("a") as fh:
|
|
157
|
+
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
158
|
+
print(json.dumps(record, indent=2, ensure_ascii=False))
|
|
159
|
+
print(f"\nappended → {out.relative_to(root)}", flush=True)
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure markitdown's token-saving lift on the bundled corpus.
|
|
3
|
+
|
|
4
|
+
Runs against `tests/fixtures/markitdown-corpus/`. By default (no flags) the
|
|
5
|
+
script computes the baseline-only — raw byte size and a tokens-per-4-bytes
|
|
6
|
+
estimate — without calling `markitdown-mcp`. With `--convert`, the script
|
|
7
|
+
tries to invoke `markitdown` (CLI binary) via subprocess and computes the
|
|
8
|
+
converted-Markdown token estimate plus the ratio per file.
|
|
9
|
+
|
|
10
|
+
Stdlib-only. Never installs anything. Never invokes a network host. Never
|
|
11
|
+
calls `markitdown-mcp` over HTTP — only through the `markitdown` CLI on
|
|
12
|
+
the user's PATH (peer-side install per the skill's Step 1 recipes).
|
|
13
|
+
|
|
14
|
+
Exit codes:
|
|
15
|
+
0 — baseline produced (always, when fixtures exist)
|
|
16
|
+
2 — corpus not found
|
|
17
|
+
3 — `--convert` was requested but `markitdown` is not on PATH
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
CORPUS = REPO_ROOT / "tests" / "fixtures" / "markitdown-corpus"
|
|
30
|
+
TOKEN_PER_BYTES = 4 # rough OpenAI/Anthropic tokenizer-of-thumb
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _baseline_tokens(p: Path) -> int:
|
|
34
|
+
return max(1, p.stat().st_size // TOKEN_PER_BYTES)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _converted_tokens(p: Path, *, binary: str) -> int | None:
|
|
38
|
+
try:
|
|
39
|
+
out = subprocess.run(
|
|
40
|
+
[binary, str(p)],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
check=False,
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=30,
|
|
45
|
+
)
|
|
46
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
47
|
+
return None
|
|
48
|
+
if out.returncode != 0:
|
|
49
|
+
return None
|
|
50
|
+
chars = len(out.stdout)
|
|
51
|
+
if chars == 0:
|
|
52
|
+
return None
|
|
53
|
+
return max(1, chars // TOKEN_PER_BYTES)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _format_ratio(baseline: int, converted: int | None) -> str:
|
|
57
|
+
if converted is None or converted == 0:
|
|
58
|
+
return "—"
|
|
59
|
+
ratio = baseline / converted
|
|
60
|
+
return f"{ratio:.1f}×"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main() -> int:
|
|
64
|
+
parser = argparse.ArgumentParser(description="Measure markitdown lift on the bundled corpus.")
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--convert",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Invoke `markitdown <fixture>` per file and compute the converted-token ratio.",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--binary",
|
|
72
|
+
default="markitdown",
|
|
73
|
+
help="Name or path of the markitdown CLI binary (default: markitdown).",
|
|
74
|
+
)
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
|
|
77
|
+
if not CORPUS.is_dir():
|
|
78
|
+
print(f"ERROR: corpus not found at {CORPUS}", file=sys.stderr)
|
|
79
|
+
print(
|
|
80
|
+
"Generate it: python3 tests/fixtures/markitdown-corpus/_generate.py",
|
|
81
|
+
file=sys.stderr,
|
|
82
|
+
)
|
|
83
|
+
return 2
|
|
84
|
+
|
|
85
|
+
fixtures = sorted(p for p in CORPUS.iterdir() if p.is_file() and p.suffix in {".pdf", ".pptx", ".docx", ".xlsx"})
|
|
86
|
+
if not fixtures:
|
|
87
|
+
print(f"ERROR: no fixtures in {CORPUS}", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
binary_path: str | None = None
|
|
91
|
+
if args.convert:
|
|
92
|
+
binary_path = shutil.which(args.binary)
|
|
93
|
+
if binary_path is None:
|
|
94
|
+
print(
|
|
95
|
+
f"ERROR: --convert requested but `{args.binary}` not on PATH.\n"
|
|
96
|
+
"Install peer-side per the skill's Step 1 recipes "
|
|
97
|
+
"(Docker / pipx / uv) and re-run.",
|
|
98
|
+
file=sys.stderr,
|
|
99
|
+
)
|
|
100
|
+
return 3
|
|
101
|
+
|
|
102
|
+
print(f"Corpus: {CORPUS.relative_to(REPO_ROOT)} ({len(fixtures)} files)")
|
|
103
|
+
print(f"Mode: {'convert (peer markitdown CLI)' if binary_path else 'baseline-only'}")
|
|
104
|
+
if binary_path:
|
|
105
|
+
print(f"Binary: {binary_path}")
|
|
106
|
+
print()
|
|
107
|
+
header = f"{'fixture':<32} {'bytes':>7} {'baseline tok':>13} {'converted tok':>14} {'ratio':>7}"
|
|
108
|
+
print(header)
|
|
109
|
+
print("-" * len(header))
|
|
110
|
+
for p in fixtures:
|
|
111
|
+
size = p.stat().st_size
|
|
112
|
+
base = _baseline_tokens(p)
|
|
113
|
+
converted = _converted_tokens(p, binary=binary_path) if binary_path else None
|
|
114
|
+
ratio = _format_ratio(base, converted)
|
|
115
|
+
conv_str = f"{converted}" if converted is not None else "—"
|
|
116
|
+
print(f"{p.name:<32} {size:>7} {base:>13} {conv_str:>14} {ratio:>7}")
|
|
117
|
+
print()
|
|
118
|
+
if not binary_path:
|
|
119
|
+
print(
|
|
120
|
+
"Re-run with --convert (after installing markitdown-mcp peer-side per the skill's "
|
|
121
|
+
"Step 1 recipes) for the actual ratio."
|
|
122
|
+
)
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|
|
@@ -30,6 +30,7 @@ from typing import List, Optional
|
|
|
30
30
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
31
31
|
from runtime_registry import SkillRuntime, build_registry
|
|
32
32
|
from runtime_handler import ExecutionResult, HandlerError, execute_shell
|
|
33
|
+
from _lib.script_output import resolve_level # type: ignore[import-not-found]
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
@dataclass
|
|
@@ -187,6 +188,16 @@ def _print_execution(result: ExecutionResult, fmt: str) -> None:
|
|
|
187
188
|
if fmt == "json":
|
|
188
189
|
print(json.dumps(asdict(result), indent=2))
|
|
189
190
|
return
|
|
191
|
+
level = resolve_level()
|
|
192
|
+
if level == "silent" and result.is_success:
|
|
193
|
+
return
|
|
194
|
+
if level == "minimal" and result.is_success:
|
|
195
|
+
marker = "✅" if result.is_success else "❌"
|
|
196
|
+
print(
|
|
197
|
+
f"{marker} {result.skill_name} · {result.handler} · "
|
|
198
|
+
f"exit={result.exit_code} ({result.duration_ms}ms)"
|
|
199
|
+
)
|
|
200
|
+
return
|
|
190
201
|
print(f"Skill: {result.skill_name}")
|
|
191
202
|
print(f"Handler: {result.handler}")
|
|
192
203
|
print(f"Command: {' '.join(result.command)}")
|
package/scripts/skill_linter.py
CHANGED
|
@@ -76,6 +76,26 @@ RULE_BAD_SIGNS = [
|
|
|
76
76
|
"## Gotchas",
|
|
77
77
|
]
|
|
78
78
|
|
|
79
|
+
# --- Frugality charter validator (see road-to-token-frugality Phase 0.4) ---
|
|
80
|
+
# Layer 1 = writer-cite check (every writer skill carries the section + link).
|
|
81
|
+
# Layer 2 = charter index integrity (the four canonical rules referenced by
|
|
82
|
+
# the charter resolve to real H2/H3 anchors in the rule files).
|
|
83
|
+
|
|
84
|
+
FRUGALITY_WRITER_SKILLS = {
|
|
85
|
+
"skill-writing", "rule-writing", "command-writing",
|
|
86
|
+
"guideline-writing", "context-authoring", "agent-docs-writing",
|
|
87
|
+
"conventional-commits-writing", "readme-writing",
|
|
88
|
+
"readme-writing-package", "adr-create",
|
|
89
|
+
"persona-writing", "roadmap-writing", "script-writing",
|
|
90
|
+
}
|
|
91
|
+
FRUGALITY_CHARTER_RELPATH = "contexts/communication/frugality-charter.md"
|
|
92
|
+
FRUGALITY_CHARTER_INDEX_RULES = {
|
|
93
|
+
"direct-answers.md": "iron-law-3",
|
|
94
|
+
"user-interaction.md": "iron-law-1",
|
|
95
|
+
"no-cheap-questions.md": "pre-send-self-check",
|
|
96
|
+
"token-efficiency.md": "the-iron-laws",
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
VAGUE_VALIDATION_PATTERNS = [
|
|
80
100
|
r"\bcheck if it works\b",
|
|
81
101
|
r"\bverify it works\b",
|
|
@@ -1381,6 +1401,15 @@ def gather_all_candidate_files(root: Path) -> list[Path]:
|
|
|
1381
1401
|
if not f.is_symlink():
|
|
1382
1402
|
candidates.append(f)
|
|
1383
1403
|
|
|
1404
|
+
# Frugality charter (Phase 0.4 Layer 2). Lives in contexts/, not
|
|
1405
|
+
# walked by the artifact-type loops above, but still needs the
|
|
1406
|
+
# index-integrity check.
|
|
1407
|
+
for base in (root / ".agent-src.uncompressed", root / ".agent-src"):
|
|
1408
|
+
charter = base / FRUGALITY_CHARTER_RELPATH
|
|
1409
|
+
if charter.exists() and not charter.is_symlink():
|
|
1410
|
+
candidates.append(charter)
|
|
1411
|
+
break
|
|
1412
|
+
|
|
1384
1413
|
return sorted(set(candidates))
|
|
1385
1414
|
|
|
1386
1415
|
|
|
@@ -1860,6 +1889,156 @@ def lint_verification_maturity(path: Path, text: str, artifact_type: str) -> Lis
|
|
|
1860
1889
|
# --- Governance & packaging checks ---
|
|
1861
1890
|
|
|
1862
1891
|
|
|
1892
|
+
# --- Frugality validator helpers + Layers 1 & 2 ---
|
|
1893
|
+
|
|
1894
|
+
def _heading_to_slug(heading: str) -> str:
|
|
1895
|
+
"""Slugify a markdown heading using GitHub's algorithm: lowercase,
|
|
1896
|
+
drop punctuation (em-dash, period, etc.), spaces -> hyphens,
|
|
1897
|
+
preserve adjacent hyphens (so `Iron Law 3 — Brevity` becomes
|
|
1898
|
+
`iron-law-3--brevity`, matching the anchor GitHub renders)."""
|
|
1899
|
+
s = heading.strip().lower()
|
|
1900
|
+
s = re.sub(r"[^a-z0-9 \-]", "", s)
|
|
1901
|
+
s = s.replace(" ", "-")
|
|
1902
|
+
return s.strip("-")
|
|
1903
|
+
|
|
1904
|
+
|
|
1905
|
+
def _extract_heading_slugs(text: str) -> set[str]:
|
|
1906
|
+
"""Return the set of slugs for every H2/H3 heading in a markdown body."""
|
|
1907
|
+
slugs: set[str] = set()
|
|
1908
|
+
for line in text.splitlines():
|
|
1909
|
+
if line.startswith("## ") or line.startswith("### "):
|
|
1910
|
+
heading = line.split(" ", 1)[1].strip()
|
|
1911
|
+
slugs.add(_heading_to_slug(heading))
|
|
1912
|
+
return slugs
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
def _skill_id_from_path(path: Path) -> Optional[str]:
|
|
1916
|
+
"""Extract the writer-skill id from a SKILL.md path. Returns the
|
|
1917
|
+
parent-directory name, or None if the file is not a SKILL.md."""
|
|
1918
|
+
if path.name.lower() != "skill.md":
|
|
1919
|
+
return None
|
|
1920
|
+
return path.parent.name
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
def _is_frugality_charter(path: Path) -> bool:
|
|
1924
|
+
"""True iff the path ends in the canonical charter relpath, regardless
|
|
1925
|
+
of whether it lives under .agent-src/ or .agent-src.uncompressed/."""
|
|
1926
|
+
norm = str(path).replace("\\", "/")
|
|
1927
|
+
return norm.endswith("/" + FRUGALITY_CHARTER_RELPATH)
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
# Section header recognised by Layer 1. Literal H2 only — sub-headings
|
|
1931
|
+
# inside the section do not count as the section itself.
|
|
1932
|
+
_FRUGALITY_STANDARDS_PATTERN = re.compile(
|
|
1933
|
+
r"^##\s+Frugality Standards\s*$", re.MULTILINE
|
|
1934
|
+
)
|
|
1935
|
+
_FRUGALITY_CHARTER_LINK_PATTERN = re.compile(
|
|
1936
|
+
r"\]\([^)]*frugality-charter\.md[^)]*\)"
|
|
1937
|
+
)
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
def lint_frugality_writer_cite(path: Path, text: str,
|
|
1941
|
+
artifact_type: str) -> List[Issue]:
|
|
1942
|
+
"""Layer 1 — every writer skill must carry a `## Frugality Standards`
|
|
1943
|
+
section that links to the charter. No-op for non-writer skills and
|
|
1944
|
+
non-skill artifacts."""
|
|
1945
|
+
if artifact_type != "skill":
|
|
1946
|
+
return []
|
|
1947
|
+
skill_id = _skill_id_from_path(path)
|
|
1948
|
+
if skill_id is None or skill_id not in FRUGALITY_WRITER_SKILLS:
|
|
1949
|
+
return []
|
|
1950
|
+
issues: List[Issue] = []
|
|
1951
|
+
section_match = _FRUGALITY_STANDARDS_PATTERN.search(text)
|
|
1952
|
+
if not section_match:
|
|
1953
|
+
issues.append(Issue(
|
|
1954
|
+
"error", "frugality_section_missing",
|
|
1955
|
+
"Writer skill must carry a `## Frugality Standards` section "
|
|
1956
|
+
"(road-to-token-frugality Phase 0.4 Layer 1)",
|
|
1957
|
+
))
|
|
1958
|
+
return issues
|
|
1959
|
+
# Section body = from match-end to next H2 or EOF.
|
|
1960
|
+
body_start = section_match.end()
|
|
1961
|
+
next_h2 = re.search(r"^##\s+", text[body_start:], re.MULTILINE)
|
|
1962
|
+
body_end = body_start + next_h2.start() if next_h2 else len(text)
|
|
1963
|
+
body = text[body_start:body_end]
|
|
1964
|
+
if not _FRUGALITY_CHARTER_LINK_PATTERN.search(body):
|
|
1965
|
+
issues.append(Issue(
|
|
1966
|
+
"error", "frugality_charter_cite_missing",
|
|
1967
|
+
"`## Frugality Standards` section must link to "
|
|
1968
|
+
"`frugality-charter.md` (road-to-token-frugality Phase 0.4 "
|
|
1969
|
+
"Layer 1)",
|
|
1970
|
+
))
|
|
1971
|
+
return issues
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
# Markdown link pattern: [text](path#anchor) — anchor optional.
|
|
1975
|
+
_MD_LINK_PATTERN = re.compile(
|
|
1976
|
+
r"\[[^\]]+\]\(([^)#]+)(?:#([^)]+))?\)"
|
|
1977
|
+
)
|
|
1978
|
+
|
|
1979
|
+
|
|
1980
|
+
def lint_frugality_charter_index(path: Path, text: str) -> List[Issue]:
|
|
1981
|
+
"""Layer 2 — every cited anchor must resolve to a real H2/H3 heading
|
|
1982
|
+
in the target rule file, AND each of the four canonical rules must
|
|
1983
|
+
be cited at least once with the required canonical anchor substring.
|
|
1984
|
+
Additional citations to the same rule (net-new sections referencing
|
|
1985
|
+
other anchors) are validated for resolution but do not need the
|
|
1986
|
+
canonical substring."""
|
|
1987
|
+
if not _is_frugality_charter(path):
|
|
1988
|
+
return []
|
|
1989
|
+
issues: List[Issue] = []
|
|
1990
|
+
rules_dir = path.parent.parent.parent / "rules"
|
|
1991
|
+
rule_slugs_cache: dict[str, set[str]] = {}
|
|
1992
|
+
canonical_satisfied: set[str] = set()
|
|
1993
|
+
for link_match in _MD_LINK_PATTERN.finditer(text):
|
|
1994
|
+
link_path, link_anchor = link_match.group(1), link_match.group(2)
|
|
1995
|
+
rule_name = Path(link_path).name
|
|
1996
|
+
if rule_name not in FRUGALITY_CHARTER_INDEX_RULES:
|
|
1997
|
+
continue
|
|
1998
|
+
if link_anchor is None:
|
|
1999
|
+
continue
|
|
2000
|
+
anchor_lc = link_anchor.lower()
|
|
2001
|
+
required_substr = FRUGALITY_CHARTER_INDEX_RULES[rule_name]
|
|
2002
|
+
if required_substr in anchor_lc:
|
|
2003
|
+
canonical_satisfied.add(rule_name)
|
|
2004
|
+
if rule_name not in rule_slugs_cache:
|
|
2005
|
+
rule_file = rules_dir / rule_name
|
|
2006
|
+
if not rule_file.exists():
|
|
2007
|
+
issues.append(Issue(
|
|
2008
|
+
"error", "frugality_charter_rule_missing",
|
|
2009
|
+
f"Charter cites {rule_name} but the rule file does "
|
|
2010
|
+
f"not exist at {rule_file}",
|
|
2011
|
+
))
|
|
2012
|
+
rule_slugs_cache[rule_name] = set()
|
|
2013
|
+
continue
|
|
2014
|
+
try:
|
|
2015
|
+
rule_text = rule_file.read_text(encoding="utf-8")
|
|
2016
|
+
except OSError as e:
|
|
2017
|
+
issues.append(Issue(
|
|
2018
|
+
"error", "frugality_charter_rule_unreadable",
|
|
2019
|
+
f"Cannot read {rule_name}: {e}",
|
|
2020
|
+
))
|
|
2021
|
+
rule_slugs_cache[rule_name] = set()
|
|
2022
|
+
continue
|
|
2023
|
+
rule_slugs_cache[rule_name] = _extract_heading_slugs(rule_text)
|
|
2024
|
+
if anchor_lc not in rule_slugs_cache[rule_name]:
|
|
2025
|
+
issues.append(Issue(
|
|
2026
|
+
"error", "frugality_charter_anchor_unresolved",
|
|
2027
|
+
f"Charter cites {rule_name}#{link_anchor} but no H2/H3 "
|
|
2028
|
+
f"heading with that slug exists in the rule file",
|
|
2029
|
+
))
|
|
2030
|
+
missing = set(FRUGALITY_CHARTER_INDEX_RULES) - canonical_satisfied
|
|
2031
|
+
for rule_name in sorted(missing):
|
|
2032
|
+
required_substr = FRUGALITY_CHARTER_INDEX_RULES[rule_name]
|
|
2033
|
+
issues.append(Issue(
|
|
2034
|
+
"error", "frugality_charter_canonical_missing",
|
|
2035
|
+
f"Charter index lacks a canonical citation of {rule_name} "
|
|
2036
|
+
f"with anchor containing '{required_substr}' "
|
|
2037
|
+
f"(road-to-token-frugality Phase 0.4 Layer 2)",
|
|
2038
|
+
))
|
|
2039
|
+
return issues
|
|
2040
|
+
|
|
2041
|
+
|
|
1863
2042
|
def lint_governance(path: Path, text: str, artifact_type: str, repo_root: Path | None = None) -> List[Issue]:
|
|
1864
2043
|
"""Check governance and packaging consistency.
|
|
1865
2044
|
|
|
@@ -2139,6 +2318,17 @@ def lint_file(path: Path, repo_root: Path | None = None) -> LintResult:
|
|
|
2139
2318
|
elif artifact_type == "persona":
|
|
2140
2319
|
result = lint_persona(display_path, text)
|
|
2141
2320
|
else:
|
|
2321
|
+
# Frugality charter lives in contexts/ (artifact_type == unknown)
|
|
2322
|
+
# but still needs Layer 2 index-integrity validation.
|
|
2323
|
+
if _is_frugality_charter(path):
|
|
2324
|
+
charter_issues = lint_frugality_charter_index(path, text)
|
|
2325
|
+
return LintResult(
|
|
2326
|
+
file=str(display_path),
|
|
2327
|
+
artifact_type="unknown",
|
|
2328
|
+
status=classify_status(charter_issues),
|
|
2329
|
+
issues=charter_issues,
|
|
2330
|
+
suggestions=[],
|
|
2331
|
+
)
|
|
2142
2332
|
return lint_unknown(display_path, text)
|
|
2143
2333
|
|
|
2144
2334
|
# Post-processing: frontmatter schema validation (errors). Runs first
|
|
@@ -2195,10 +2385,20 @@ def lint_file(path: Path, repo_root: Path | None = None) -> LintResult:
|
|
|
2195
2385
|
result.issues.extend(malice_issues)
|
|
2196
2386
|
result.status = classify_status(result.issues)
|
|
2197
2387
|
|
|
2388
|
+
# Post-processing: frugality validator Layer 1 (writer-cite). Errors
|
|
2389
|
+
# if a writer skill lacks the `## Frugality Standards` section or its
|
|
2390
|
+
# link to the charter.
|
|
2391
|
+
frugality_issues = lint_frugality_writer_cite(
|
|
2392
|
+
display_path, text, artifact_type
|
|
2393
|
+
)
|
|
2394
|
+
if frugality_issues:
|
|
2395
|
+
result.issues.extend(frugality_issues)
|
|
2396
|
+
result.status = classify_status(result.issues)
|
|
2397
|
+
|
|
2198
2398
|
return result
|
|
2199
2399
|
|
|
2200
2400
|
|
|
2201
|
-
def format_text(results: list[LintResult]) -> str:
|
|
2401
|
+
def format_text(results: list[LintResult], quiet: bool = False) -> str:
|
|
2202
2402
|
lines: list[str] = []
|
|
2203
2403
|
# Phase 5.2: malice findings render in the spec shape
|
|
2204
2404
|
# ``<path>:<line>:malice:<pattern>:<matched>`` ahead of the badge
|
|
@@ -2216,7 +2416,10 @@ def format_text(results: list[LintResult]) -> str:
|
|
|
2216
2416
|
if malice_total:
|
|
2217
2417
|
lines.append("")
|
|
2218
2418
|
|
|
2419
|
+
# P10.5: quiet mode skips PASS-without-issues; malice + WARN/FAIL still rendered.
|
|
2219
2420
|
for result in results:
|
|
2421
|
+
if quiet and result.status == "pass" and not result.issues and not result.suggestions:
|
|
2422
|
+
continue
|
|
2220
2423
|
badge = {"pass": "[PASS]", "pass_with_warnings": "[WARN]", "fail": "[FAIL]"}[result.status]
|
|
2221
2424
|
lines.append(f"{badge} {result.file} ({result.artifact_type})")
|
|
2222
2425
|
if result.issues:
|
|
@@ -2458,6 +2661,8 @@ def parse_args() -> argparse.Namespace:
|
|
|
2458
2661
|
parser.add_argument("--strict-warnings", action="store_true", help="Return non-zero on warnings")
|
|
2459
2662
|
parser.add_argument("--report", action="store_true", help="Output quality score report")
|
|
2460
2663
|
parser.add_argument("--repo-root", default=".", help="Repository root")
|
|
2664
|
+
parser.add_argument("--quiet", action="store_true",
|
|
2665
|
+
help="suppress per-file PASS lines; keep malice + WARN/FAIL + summary (P10.5)")
|
|
2461
2666
|
return parser.parse_args()
|
|
2462
2667
|
|
|
2463
2668
|
|
|
@@ -2604,7 +2809,7 @@ def main() -> int:
|
|
|
2604
2809
|
elif args.format == "json":
|
|
2605
2810
|
print(format_json(results))
|
|
2606
2811
|
else:
|
|
2607
|
-
print(format_text(results))
|
|
2812
|
+
print(format_text(results, quiet=args.quiet))
|
|
2608
2813
|
|
|
2609
2814
|
return compute_exit_code(results, strict_warnings=args.strict_warnings)
|
|
2610
2815
|
|