tribunal-kit 2.4.2 → 2.4.4
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/patterns/generator.md +9 -0
- package/.agent/patterns/inversion.md +12 -0
- package/.agent/patterns/pipeline.md +9 -0
- package/.agent/patterns/reviewer.md +13 -0
- package/.agent/patterns/tool-wrapper.md +9 -0
- 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/scripts/swarm_dispatcher.py +34 -1
- package/.agent/skills/documentation-templates/SKILL.md +17 -0
- package/.agent/skills/github-operations/SKILL.md +354 -354
- package/.agent/skills/readme-builder/SKILL.md +270 -270
- package/.agent/workflows/strengthen-skills.md +99 -99
- package/bin/tribunal-kit.js +547 -542
- package/package.json +53 -38
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Generator Pattern
|
|
2
|
+
|
|
3
|
+
**Purpose**: Produce structured output by filling a reusable template governed by quality rules.
|
|
4
|
+
|
|
5
|
+
## Protocol
|
|
6
|
+
When a skill inherits this pattern, the agent is tasked with producing a specific formatted artifact (like a configuration file, documentation page, or scaffolding code).
|
|
7
|
+
1. **Template Retrieval**: Locate and strictly adhere to the provided template structure (the "assets") defined by the specific skill.
|
|
8
|
+
2. **Constraint Application**: Apply all quality rules and constraints (the "references") required by the skill while fleshing out the template.
|
|
9
|
+
3. **No Halucination Formatting**: Do not invent new sections, alter the required Markdown/JSON structure, or add unauthorized commentary unless it fits directly into the predefined template slots.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Inversion Pattern
|
|
2
|
+
|
|
3
|
+
**Purpose**: Interview the user before taking action.
|
|
4
|
+
|
|
5
|
+
## Protocol
|
|
6
|
+
When a skill inherits this pattern, you MUST NOT proceed with execution immediately. Instead, rely on the "Socratic Gate". You must pause and ask the user questions using the following structured phases:
|
|
7
|
+
1. **Identify Missing Context**: Evaluate the user's prompt against what is absolutely necessary to execute the skill.
|
|
8
|
+
2. **Phase 1 (Goal & Constraints)**: Ask the user about the real outcome and any hard constraints.
|
|
9
|
+
3. **Phase 2 (Out of Scope)**: Confirm what should explicitly NOT be done.
|
|
10
|
+
4. **Phase 3 (Done Condition)**: Verify how you will know the task is completed.
|
|
11
|
+
|
|
12
|
+
You must receive explicit answers or a "do your best" override before writing code or executing substantive actions.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Pipeline Pattern
|
|
2
|
+
|
|
3
|
+
**Purpose**: Link multiple execution steps together with explicit validation gates between them.
|
|
4
|
+
|
|
5
|
+
## Protocol
|
|
6
|
+
When a skill inherits this pattern, the agent must execute its instructions sequentially and rigidly.
|
|
7
|
+
1. **Step-by-Step Execution**: You must not skip steps or combine multiple distinct phases into a single massive generative output.
|
|
8
|
+
2. **Validation Gates**: After completing Step N, you must validate that the output of Step N meets its success criteria before moving to Step N+1.
|
|
9
|
+
3. **Halting**: If any gate fails validation, you must HALT the pipeline and either initiate an Error Recovery Protocol or report the failure to the user. Do not proceed with subsequent steps with broken inputs.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Reviewer Pattern
|
|
2
|
+
|
|
3
|
+
**Purpose**: Evaluate code or content against a strict external checklist.
|
|
4
|
+
|
|
5
|
+
## Protocol
|
|
6
|
+
When a skill inherits this pattern, the agent assumes the role of an evaluator. Do NOT generate novel content or fix the problem automatically unless explicitly instructed.
|
|
7
|
+
1. **Checklist Enforcement**: You must read the evaluation checklist provided in the specific skill.
|
|
8
|
+
2. **Review Output**: For every item in the checklist, determine if it passes or fails.
|
|
9
|
+
3. **Severity Grading**: Group all findings by severity:
|
|
10
|
+
- **Critical**: Must fix before proceeding (e.g. security violations, build errors)
|
|
11
|
+
- **Warning**: Should fix (e.g. best practice violations, performance risks)
|
|
12
|
+
- **Info**: Stylistic or minor suggestions
|
|
13
|
+
4. **Separation of Concerns**: Only evaluate the "what" (the checklist) based on the "how" (this standard format). Do not blur your own opinions into the checklist constraints.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Tool Wrapper Pattern
|
|
2
|
+
|
|
3
|
+
**Purpose**: Package an external library's or CLI tool's conventions as on-demand, executable knowledge.
|
|
4
|
+
|
|
5
|
+
## Protocol
|
|
6
|
+
When a skill inherits this pattern, the agent MUST NOT guess how to use the target tool. You are acting strictly as a wrapper for this specific utility.
|
|
7
|
+
1. **Consult References**: Read the provided documentation, usage examples, or reference notes in the skill definitions BEFORE issuing any commands.
|
|
8
|
+
2. **Strict Adherence**: Follow the rules defined in the skill exactly as written. Do not improvise flags, parameters, or endpoints that are not explicitly authorized by the reference.
|
|
9
|
+
3. **Command Execution**: If the tool is a CLI command or Python script (e.g. `test_runner.py`), construct the command accurately based solely on the referenced conventions, execute it, and report the direct output.
|
|
@@ -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()
|