tribunal-kit 2.4.0 → 2.4.3
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/scripts/patch_skills_meta.py +177 -177
- package/.agent/scripts/patch_skills_output.py +285 -285
- package/.agent/scripts/strengthen_skills.py +220 -220
- package/.agent/skills/github-operations/SKILL.md +354 -0
- package/.agent/skills/readme-builder/SKILL.md +270 -0
- package/.agent/workflows/strengthen-skills.md +99 -99
- package/bin/tribunal-kit.js +16 -11
- package/package.json +53 -38
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
patch_skills_meta.py — Injects version/freshness metadata into SKILL.md frontmatter.
|
|
4
|
-
|
|
5
|
-
Adds the following fields to YAML frontmatter if missing:
|
|
6
|
-
version: 1.0.0
|
|
7
|
-
last-updated: 2026-03-12
|
|
8
|
-
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
9
|
-
|
|
10
|
-
Usage:
|
|
11
|
-
python .agent/scripts/patch_skills_meta.py .
|
|
12
|
-
python .agent/scripts/patch_skills_meta.py . --dry-run
|
|
13
|
-
python .agent/scripts/patch_skills_meta.py . --skill python-pro
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import os
|
|
17
|
-
import sys
|
|
18
|
-
import argparse
|
|
19
|
-
import re
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
|
|
22
|
-
RED = "\033[91m"
|
|
23
|
-
GREEN = "\033[92m"
|
|
24
|
-
YELLOW = "\033[93m"
|
|
25
|
-
BLUE = "\033[94m"
|
|
26
|
-
BOLD = "\033[1m"
|
|
27
|
-
RESET = "\033[0m"
|
|
28
|
-
|
|
29
|
-
META_FIELDS = {
|
|
30
|
-
"version": "1.0.0",
|
|
31
|
-
"last-updated": "2026-03-12",
|
|
32
|
-
"applies-to-model": "gemini-2.5-pro, claude-3-7-sonnet",
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def header(title: str) -> None:
|
|
37
|
-
print(f"\n{BOLD}{BLUE}━━━ {title} ━━━{RESET}")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def ok(msg: str) -> None:
|
|
41
|
-
print(f" {GREEN}✅ {msg}{RESET}")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def skip(msg: str) -> None:
|
|
45
|
-
print(f" {YELLOW}⏭️ {msg}{RESET}")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def warn(msg: str) -> None:
|
|
49
|
-
print(f" {YELLOW}⚠️ {msg}{RESET}")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def fail(msg: str) -> None:
|
|
53
|
-
print(f" {RED}❌ {msg}{RESET}")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def patch_frontmatter(content: str) -> tuple[str, list[str]]:
|
|
57
|
-
"""
|
|
58
|
-
Parse the YAML frontmatter block and inject missing meta fields.
|
|
59
|
-
Returns (patched_content, list_of_added_fields).
|
|
60
|
-
If no frontmatter is found, injects a minimal one.
|
|
61
|
-
"""
|
|
62
|
-
added: list[str] = []
|
|
63
|
-
|
|
64
|
-
# Match frontmatter block: starts and ends with ---
|
|
65
|
-
fm_pattern = re.compile(r"^---\r?\n(.*?)\r?\n---", re.DOTALL)
|
|
66
|
-
match = fm_pattern.match(content)
|
|
67
|
-
|
|
68
|
-
if not match:
|
|
69
|
-
# No frontmatter — prepend a minimal block
|
|
70
|
-
new_fm_lines = ["---"]
|
|
71
|
-
for key, value in META_FIELDS.items():
|
|
72
|
-
new_fm_lines.append(f"{key}: {value}")
|
|
73
|
-
added.append(key)
|
|
74
|
-
new_fm_lines.append("---")
|
|
75
|
-
new_fm = "\n".join(new_fm_lines)
|
|
76
|
-
return new_fm + "\n\n" + content, added
|
|
77
|
-
|
|
78
|
-
fm_text = match.group(1)
|
|
79
|
-
fm_end = match.end()
|
|
80
|
-
|
|
81
|
-
# Parse existing lines preserving order
|
|
82
|
-
existing_keys = set()
|
|
83
|
-
for line in fm_text.splitlines():
|
|
84
|
-
m = re.match(r"^([a-zA-Z0-9_-]+)\s*:", line)
|
|
85
|
-
if m:
|
|
86
|
-
existing_keys.add(m.group(1))
|
|
87
|
-
|
|
88
|
-
# Build new frontmatter
|
|
89
|
-
new_fm_lines = fm_text.rstrip().splitlines()
|
|
90
|
-
for key, value in META_FIELDS.items():
|
|
91
|
-
if key not in existing_keys:
|
|
92
|
-
new_fm_lines.append(f"{key}: {value}")
|
|
93
|
-
added.append(key)
|
|
94
|
-
|
|
95
|
-
if not added:
|
|
96
|
-
return content, []
|
|
97
|
-
|
|
98
|
-
new_fm_block = "---\n" + "\n".join(new_fm_lines) + "\n---"
|
|
99
|
-
patched = new_fm_block + content[fm_end:]
|
|
100
|
-
return patched, added
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def process_skill(skill_path: Path, dry_run: bool) -> str:
|
|
104
|
-
"""Process a single SKILL.md. Returns 'updated', 'skipped', or 'error'."""
|
|
105
|
-
try:
|
|
106
|
-
content = skill_path.read_text(encoding="utf-8")
|
|
107
|
-
patched, added = patch_frontmatter(content)
|
|
108
|
-
|
|
109
|
-
skill_name = skill_path.parent.name
|
|
110
|
-
|
|
111
|
-
if not added:
|
|
112
|
-
skip(f"{skill_name} — all meta fields present")
|
|
113
|
-
return "skipped"
|
|
114
|
-
|
|
115
|
-
field_list = ", ".join(added)
|
|
116
|
-
if dry_run:
|
|
117
|
-
warn(f"[DRY RUN] {skill_name} — would add: {field_list}")
|
|
118
|
-
return "updated"
|
|
119
|
-
|
|
120
|
-
skill_path.write_text(patched, encoding="utf-8")
|
|
121
|
-
ok(f"{skill_name} — added: {field_list}")
|
|
122
|
-
return "updated"
|
|
123
|
-
|
|
124
|
-
except Exception as e:
|
|
125
|
-
fail(f"{skill_path.parent.name} — {e}")
|
|
126
|
-
return "error"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def main() -> None:
|
|
130
|
-
parser = argparse.ArgumentParser(
|
|
131
|
-
description="Injects version/freshness metadata into SKILL.md frontmatter"
|
|
132
|
-
)
|
|
133
|
-
parser.add_argument("path", help="Project root directory")
|
|
134
|
-
parser.add_argument("--dry-run", action="store_true", help="Show changes without writing")
|
|
135
|
-
parser.add_argument("--skill", help="Only patch a specific skill by name")
|
|
136
|
-
args = parser.parse_args()
|
|
137
|
-
|
|
138
|
-
project_root = Path(args.path).resolve()
|
|
139
|
-
skills_dir = project_root / ".agent" / "skills"
|
|
140
|
-
|
|
141
|
-
if not skills_dir.is_dir():
|
|
142
|
-
fail(f"Skills directory not found: {skills_dir}")
|
|
143
|
-
sys.exit(1)
|
|
144
|
-
|
|
145
|
-
print(f"{BOLD}Tribunal — patch_skills_meta.py{RESET}")
|
|
146
|
-
if args.dry_run:
|
|
147
|
-
print(f" {YELLOW}DRY RUN — no files will be written{RESET}")
|
|
148
|
-
print(f"Skills dir: {skills_dir}\n")
|
|
149
|
-
|
|
150
|
-
counts = {"updated": 0, "skipped": 0, "error": 0}
|
|
151
|
-
|
|
152
|
-
header("Patching Frontmatter")
|
|
153
|
-
for skill_dir in sorted(skills_dir.iterdir()):
|
|
154
|
-
if not skill_dir.is_dir():
|
|
155
|
-
continue
|
|
156
|
-
if args.skill and skill_dir.name != args.skill:
|
|
157
|
-
continue
|
|
158
|
-
skill_md = skill_dir / "SKILL.md"
|
|
159
|
-
if not skill_md.exists():
|
|
160
|
-
warn(f"{skill_dir.name} — no SKILL.md found")
|
|
161
|
-
continue
|
|
162
|
-
result = process_skill(skill_md, args.dry_run)
|
|
163
|
-
counts[result] += 1
|
|
164
|
-
|
|
165
|
-
print(f"\n{BOLD}━━━ Summary ━━━{RESET}")
|
|
166
|
-
print(f" {GREEN}✅ Updated: {counts['updated']}{RESET}")
|
|
167
|
-
print(f" {YELLOW}⏭️ Skipped: {counts['skipped']}{RESET}")
|
|
168
|
-
if counts["error"]:
|
|
169
|
-
print(f" {RED}❌ Errors: {counts['error']}{RESET}")
|
|
170
|
-
if args.dry_run:
|
|
171
|
-
print(f" {YELLOW}(dry-run — nothing written){RESET}")
|
|
172
|
-
|
|
173
|
-
sys.exit(1 if counts["error"] > 0 else 0)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if __name__ == "__main__":
|
|
177
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
patch_skills_meta.py — Injects version/freshness metadata into SKILL.md frontmatter.
|
|
4
|
+
|
|
5
|
+
Adds the following fields to YAML frontmatter if missing:
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
last-updated: 2026-03-12
|
|
8
|
+
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python .agent/scripts/patch_skills_meta.py .
|
|
12
|
+
python .agent/scripts/patch_skills_meta.py . --dry-run
|
|
13
|
+
python .agent/scripts/patch_skills_meta.py . --skill python-pro
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import argparse
|
|
19
|
+
import re
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
RED = "\033[91m"
|
|
23
|
+
GREEN = "\033[92m"
|
|
24
|
+
YELLOW = "\033[93m"
|
|
25
|
+
BLUE = "\033[94m"
|
|
26
|
+
BOLD = "\033[1m"
|
|
27
|
+
RESET = "\033[0m"
|
|
28
|
+
|
|
29
|
+
META_FIELDS = {
|
|
30
|
+
"version": "1.0.0",
|
|
31
|
+
"last-updated": "2026-03-12",
|
|
32
|
+
"applies-to-model": "gemini-2.5-pro, claude-3-7-sonnet",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def header(title: str) -> None:
|
|
37
|
+
print(f"\n{BOLD}{BLUE}━━━ {title} ━━━{RESET}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ok(msg: str) -> None:
|
|
41
|
+
print(f" {GREEN}✅ {msg}{RESET}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def skip(msg: str) -> None:
|
|
45
|
+
print(f" {YELLOW}⏭️ {msg}{RESET}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def warn(msg: str) -> None:
|
|
49
|
+
print(f" {YELLOW}⚠️ {msg}{RESET}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fail(msg: str) -> None:
|
|
53
|
+
print(f" {RED}❌ {msg}{RESET}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def patch_frontmatter(content: str) -> tuple[str, list[str]]:
|
|
57
|
+
"""
|
|
58
|
+
Parse the YAML frontmatter block and inject missing meta fields.
|
|
59
|
+
Returns (patched_content, list_of_added_fields).
|
|
60
|
+
If no frontmatter is found, injects a minimal one.
|
|
61
|
+
"""
|
|
62
|
+
added: list[str] = []
|
|
63
|
+
|
|
64
|
+
# Match frontmatter block: starts and ends with ---
|
|
65
|
+
fm_pattern = re.compile(r"^---\r?\n(.*?)\r?\n---", re.DOTALL)
|
|
66
|
+
match = fm_pattern.match(content)
|
|
67
|
+
|
|
68
|
+
if not match:
|
|
69
|
+
# No frontmatter — prepend a minimal block
|
|
70
|
+
new_fm_lines = ["---"]
|
|
71
|
+
for key, value in META_FIELDS.items():
|
|
72
|
+
new_fm_lines.append(f"{key}: {value}")
|
|
73
|
+
added.append(key)
|
|
74
|
+
new_fm_lines.append("---")
|
|
75
|
+
new_fm = "\n".join(new_fm_lines)
|
|
76
|
+
return new_fm + "\n\n" + content, added
|
|
77
|
+
|
|
78
|
+
fm_text = match.group(1)
|
|
79
|
+
fm_end = match.end()
|
|
80
|
+
|
|
81
|
+
# Parse existing lines preserving order
|
|
82
|
+
existing_keys = set()
|
|
83
|
+
for line in fm_text.splitlines():
|
|
84
|
+
m = re.match(r"^([a-zA-Z0-9_-]+)\s*:", line)
|
|
85
|
+
if m:
|
|
86
|
+
existing_keys.add(m.group(1))
|
|
87
|
+
|
|
88
|
+
# Build new frontmatter
|
|
89
|
+
new_fm_lines = fm_text.rstrip().splitlines()
|
|
90
|
+
for key, value in META_FIELDS.items():
|
|
91
|
+
if key not in existing_keys:
|
|
92
|
+
new_fm_lines.append(f"{key}: {value}")
|
|
93
|
+
added.append(key)
|
|
94
|
+
|
|
95
|
+
if not added:
|
|
96
|
+
return content, []
|
|
97
|
+
|
|
98
|
+
new_fm_block = "---\n" + "\n".join(new_fm_lines) + "\n---"
|
|
99
|
+
patched = new_fm_block + content[fm_end:]
|
|
100
|
+
return patched, added
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def process_skill(skill_path: Path, dry_run: bool) -> str:
|
|
104
|
+
"""Process a single SKILL.md. Returns 'updated', 'skipped', or 'error'."""
|
|
105
|
+
try:
|
|
106
|
+
content = skill_path.read_text(encoding="utf-8")
|
|
107
|
+
patched, added = patch_frontmatter(content)
|
|
108
|
+
|
|
109
|
+
skill_name = skill_path.parent.name
|
|
110
|
+
|
|
111
|
+
if not added:
|
|
112
|
+
skip(f"{skill_name} — all meta fields present")
|
|
113
|
+
return "skipped"
|
|
114
|
+
|
|
115
|
+
field_list = ", ".join(added)
|
|
116
|
+
if dry_run:
|
|
117
|
+
warn(f"[DRY RUN] {skill_name} — would add: {field_list}")
|
|
118
|
+
return "updated"
|
|
119
|
+
|
|
120
|
+
skill_path.write_text(patched, encoding="utf-8")
|
|
121
|
+
ok(f"{skill_name} — added: {field_list}")
|
|
122
|
+
return "updated"
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
fail(f"{skill_path.parent.name} — {e}")
|
|
126
|
+
return "error"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main() -> None:
|
|
130
|
+
parser = argparse.ArgumentParser(
|
|
131
|
+
description="Injects version/freshness metadata into SKILL.md frontmatter"
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument("path", help="Project root directory")
|
|
134
|
+
parser.add_argument("--dry-run", action="store_true", help="Show changes without writing")
|
|
135
|
+
parser.add_argument("--skill", help="Only patch a specific skill by name")
|
|
136
|
+
args = parser.parse_args()
|
|
137
|
+
|
|
138
|
+
project_root = Path(args.path).resolve()
|
|
139
|
+
skills_dir = project_root / ".agent" / "skills"
|
|
140
|
+
|
|
141
|
+
if not skills_dir.is_dir():
|
|
142
|
+
fail(f"Skills directory not found: {skills_dir}")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
print(f"{BOLD}Tribunal — patch_skills_meta.py{RESET}")
|
|
146
|
+
if args.dry_run:
|
|
147
|
+
print(f" {YELLOW}DRY RUN — no files will be written{RESET}")
|
|
148
|
+
print(f"Skills dir: {skills_dir}\n")
|
|
149
|
+
|
|
150
|
+
counts = {"updated": 0, "skipped": 0, "error": 0}
|
|
151
|
+
|
|
152
|
+
header("Patching Frontmatter")
|
|
153
|
+
for skill_dir in sorted(skills_dir.iterdir()):
|
|
154
|
+
if not skill_dir.is_dir():
|
|
155
|
+
continue
|
|
156
|
+
if args.skill and skill_dir.name != args.skill:
|
|
157
|
+
continue
|
|
158
|
+
skill_md = skill_dir / "SKILL.md"
|
|
159
|
+
if not skill_md.exists():
|
|
160
|
+
warn(f"{skill_dir.name} — no SKILL.md found")
|
|
161
|
+
continue
|
|
162
|
+
result = process_skill(skill_md, args.dry_run)
|
|
163
|
+
counts[result] += 1
|
|
164
|
+
|
|
165
|
+
print(f"\n{BOLD}━━━ Summary ━━━{RESET}")
|
|
166
|
+
print(f" {GREEN}✅ Updated: {counts['updated']}{RESET}")
|
|
167
|
+
print(f" {YELLOW}⏭️ Skipped: {counts['skipped']}{RESET}")
|
|
168
|
+
if counts["error"]:
|
|
169
|
+
print(f" {RED}❌ Errors: {counts['error']}{RESET}")
|
|
170
|
+
if args.dry_run:
|
|
171
|
+
print(f" {YELLOW}(dry-run — nothing written){RESET}")
|
|
172
|
+
|
|
173
|
+
sys.exit(1 if counts["error"] > 0 else 0)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
main()
|