skilldrop-cli 0.16.43 → 0.16.45
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/package.json +1 -1
- package/packs/ai-engineering/skills/agent-config-review/manifest.json +1 -1
- package/packs/ai-engineering/skills/agent-config-review/scripts/agent_config_scan.py +6 -1
- package/packs/ai-engineering/skills/ai-usage-report/manifest.json +1 -1
- package/packs/ai-engineering/skills/ai-usage-report/scripts/build_report.py +30 -13
- package/packs/career/skills/cover-letter/manifest.json +1 -1
- package/packs/career/skills/cover-letter/scripts/claim_check.py +25 -3
- package/packs/career/skills/resume-tailor/manifest.json +1 -1
- package/packs/career/skills/resume-tailor/scripts/claim_check.py +25 -3
- package/packs/converters/skills/file-to-markdown/manifest.json +1 -1
- package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +4 -2
- package/packs/converters/skills/md-to-confluence/manifest.json +1 -1
- package/packs/converters/skills/md-to-confluence/scripts/md_to_confluence.py +8 -5
- package/packs/converters/skills/md-to-html/manifest.json +1 -1
- package/packs/converters/skills/md-to-html/scripts/md_to_html.py +10 -0
- package/packs/converters/skills/md-to-pptx/manifest.json +1 -1
- package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +5 -4
- package/packs/converters/skills/mermaid-render/manifest.json +1 -1
- package/packs/converters/skills/mermaid-render/scripts/render_mermaid.py +2 -1
- package/packs/core/skills/output-hygiene/manifest.json +1 -1
- package/packs/core/skills/output-hygiene/scripts/scrub.py +3 -2
- package/packs/data-analytics/skills/sql-review/manifest.json +1 -1
- package/packs/data-analytics/skills/sql-review/scripts/sql_lint.py +10 -3
- package/packs/design/skills/brand-kit/manifest.json +1 -1
- package/packs/design/skills/brand-kit/scripts/check_brand.py +22 -6
- package/packs/design/skills/deck-builder/manifest.json +1 -1
- package/packs/design/skills/deck-builder/scripts/build_deck.py +19 -9
- package/packs/design/skills/marketing-flyer/manifest.json +1 -1
- package/packs/design/skills/marketing-flyer/scripts/build_flyer.py +10 -1
- package/packs/dev-team/skills/behaviour-equivalence/manifest.json +1 -1
- package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +14 -2
- package/packs/dev-team/skills/codebase-tour/manifest.json +1 -1
- package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +6 -1
- package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +1 -1
- package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +2 -2
- package/packs/dev-team/skills/pre-merge-review/manifest.json +1 -1
- package/packs/dev-team/skills/pre-merge-review/scripts/gate.py +7 -1
- package/packs/dev-team/skills/rollout-plan/manifest.json +1 -1
- package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +2 -2
- package/packs/frontend/skills/bundle-size-review/manifest.json +1 -1
- package/packs/frontend/skills/bundle-size-review/scripts/bundle_stats.py +14 -2
- package/packs/frontend/skills/web-performance-review/manifest.json +1 -1
- package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +18 -3
- package/packs/grc/skills/risk-register/manifest.json +1 -1
- package/packs/grc/skills/risk-register/scripts/check_register.py +15 -2
- package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +1 -1
- package/packs/infra-as-code/skills/terraform-plan-review/scripts/plan_summary.py +15 -3
- package/packs/product-manager/skills/feedback-synthesis/manifest.json +1 -1
- package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +6 -1
- package/packs/product-manager/skills/lean-canvas/manifest.json +1 -1
- package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +1 -1
- package/packs/research/skills/hypothesis-comparison/manifest.json +1 -1
- package/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +13 -2
- package/packs/research/skills/source-synthesis/manifest.json +1 -1
- package/packs/research/skills/source-synthesis/scripts/citations.py +2 -1
- package/packs/skill-engineering/skills/skill-review/manifest.json +1 -1
- package/packs/skill-engineering/skills/skill-review/scripts/lint_skill.py +4 -1
- package/packs/solution-architect/skills/event-contract-draft/manifest.json +1 -1
- package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +23 -4
- package/packs/solution-architect/skills/figma-diagrams/manifest.json +1 -1
- package/packs/solution-architect/skills/figma-diagrams/scripts/frame_to_mermaid.py +1 -1
- package/packs/solution-architect/skills/figma-diagrams/scripts/post_comment.py +1 -1
- package/packs/solution-architect/skills/interactive-diagram/manifest.json +1 -1
- package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +5 -2
- package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -1
- package/packs/sre-oncall/skills/cloud-cost-review/scripts/cost_summary.py +7 -2
- package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -1
- package/packs/sre-oncall/skills/delivery-metrics-report/scripts/delivery_metrics.py +4 -0
- package/packs/trackers/skills/backlog-triage/manifest.json +1 -1
- package/packs/trackers/skills/backlog-triage/scripts/triage_backlog.py +11 -3
- package/packs/trackers/skills/team-status-report/manifest.json +1 -1
- package/packs/trackers/skills/team-status-report/scripts/status_counts.py +9 -2
- package/packs/trackers/skills/tracker-brief-sync/manifest.json +1 -1
- package/packs/trackers/skills/tracker-brief-sync/scripts/brief_sync.py +1 -1
|
@@ -34,13 +34,17 @@ import zipfile
|
|
|
34
34
|
from dataclasses import dataclass
|
|
35
35
|
from pathlib import Path
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
from pptx
|
|
39
|
-
from pptx.
|
|
40
|
-
from pptx.
|
|
41
|
-
from pptx.enum.
|
|
42
|
-
from pptx.enum.
|
|
43
|
-
from pptx.
|
|
37
|
+
try:
|
|
38
|
+
from pptx import Presentation
|
|
39
|
+
from pptx.chart.data import CategoryChartData
|
|
40
|
+
from pptx.dml.color import RGBColor
|
|
41
|
+
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
|
|
42
|
+
from pptx.enum.shapes import MSO_SHAPE, PP_PLACEHOLDER
|
|
43
|
+
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
|
|
44
|
+
from pptx.util import Emu, Inches, Pt
|
|
45
|
+
except ImportError:
|
|
46
|
+
sys.stderr.write("error: python-pptx is not installed; install it with: pip install python-pptx\n")
|
|
47
|
+
sys.exit(1)
|
|
44
48
|
|
|
45
49
|
# The design canvas every hardcoded position below is expressed in. Real slide
|
|
46
50
|
# geometry comes from Canvas, which scales these to the actual deck size.
|
|
@@ -960,7 +964,8 @@ def load_brand(ref, base: Path):
|
|
|
960
964
|
return {}, base
|
|
961
965
|
if isinstance(ref, dict):
|
|
962
966
|
return ref, base
|
|
963
|
-
path = (
|
|
967
|
+
path = Path(ref).expanduser()
|
|
968
|
+
path = path if path.is_absolute() else base / path
|
|
964
969
|
if not path.exists():
|
|
965
970
|
warn(f"brand file not found: {path}; building without it")
|
|
966
971
|
return {}, base
|
|
@@ -1024,7 +1029,12 @@ def place_logo(slide, c: "Canvas", palette: dict, logical: str) -> None:
|
|
|
1024
1029
|
|
|
1025
1030
|
|
|
1026
1031
|
def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool, brand_ref=None) -> None:
|
|
1027
|
-
|
|
1032
|
+
try:
|
|
1033
|
+
spec = json.loads(spec_path.read_text(encoding="utf-8-sig"))
|
|
1034
|
+
except json.JSONDecodeError as e:
|
|
1035
|
+
die(f"spec {spec_path} is not valid JSON ({e}); fix it and re-run")
|
|
1036
|
+
except UnicodeDecodeError:
|
|
1037
|
+
die(f"spec {spec_path} is not UTF-8 text; save it as UTF-8 and re-run")
|
|
1028
1038
|
spec_dir = spec_path.parent
|
|
1029
1039
|
brand, brand_dir = load_brand(brand_ref or spec.get("brand"), spec_dir)
|
|
1030
1040
|
if brand:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marketing-flyer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Generate a print-ready, on-brand marketing flyer — event, launch, open day, offer, hiring — as one self-contained HTML file (Letter, A4, A5, or square and story sizes for social) that saves to PDF, built from the user's own logo, colours, fonts and content, with one call to action, a short headline, contrast checked, and no invented dates, prices or contact details. Use when the user wants a flyer, poster, one-pager, handout, leaflet or event announcement \"in our branding\", or says \"make a flyer for…\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -79,9 +79,14 @@ def data_uri(path, label):
|
|
|
79
79
|
|
|
80
80
|
def load_json(path):
|
|
81
81
|
try:
|
|
82
|
-
|
|
82
|
+
data = json.loads(Path(path).read_text(encoding="utf-8-sig"))
|
|
83
83
|
except (OSError, json.JSONDecodeError) as e:
|
|
84
84
|
die(f"cannot read {path}: {e}")
|
|
85
|
+
except UnicodeDecodeError:
|
|
86
|
+
die(f"cannot read {path}: it is not UTF-8 text; save it as UTF-8")
|
|
87
|
+
if not isinstance(data, dict):
|
|
88
|
+
die(f"cannot read {path}: expected a JSON object {{...}}, got a {type(data).__name__}")
|
|
89
|
+
return data
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
def resolve(base, ref):
|
|
@@ -164,9 +169,13 @@ def build(spec_path, out_path, brand_ref=None):
|
|
|
164
169
|
if isinstance(cta, list):
|
|
165
170
|
warn("more than one call to action; a flyer gets one — using the first")
|
|
166
171
|
cta = cta[0] if cta else None
|
|
172
|
+
if isinstance(cta, str):
|
|
173
|
+
cta = {"text": cta}
|
|
167
174
|
if not cta:
|
|
168
175
|
warn("no call to action — what should a reader do next?")
|
|
169
176
|
img = spec.get("image") or {}
|
|
177
|
+
if isinstance(img, str):
|
|
178
|
+
img = {"path": img}
|
|
170
179
|
hero = data_uri(resolve(base, img.get("path")), "image") if isinstance(img, dict) else data_uri(resolve(base, img), "image")
|
|
171
180
|
if hero and isinstance(img, dict) and not img.get("alt"):
|
|
172
181
|
warn("image has no alt text")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "behaviour-equivalence",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Prove that a rewritten, migrated or refactored component behaves the same as the old one with a golden-master comparison — record inputs and outputs at a boundary (HTTP, function calls, batch files, DB writes, events) from production-shaped traffic and edge cases, run the old system twice to find its nondeterminism, normalise narrowly field by field, replay against the new one, triage every difference as defect, intended change (with a named approver) or missing normalisation, and give a verdict of EQUIVALENT, EQUIVALENT EXCEPT LISTED CHANGES, NOT EQUIVALENT or NOT PROVEN. Use when the user asks \"does the new one behave the same as the old one\", \"can we sign off the rewrite\", \"are these diffs just noise\", wants a golden-master, approval or shadow-traffic comparison, a parity check between old and new outputs, or says \"prove behaviour equivalence\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -143,7 +143,9 @@ def load_rules(path):
|
|
|
143
143
|
items = raw["rules"]
|
|
144
144
|
elif isinstance(raw, dict):
|
|
145
145
|
items = []
|
|
146
|
-
for p, spec in raw.items():
|
|
146
|
+
for i, (p, spec) in enumerate(raw.items(), 1):
|
|
147
|
+
if not isinstance(spec, (str, dict)):
|
|
148
|
+
raise BadInput("rule {} ({}): spec must be a rule name or an object, got {!r}".format(i, p, spec))
|
|
147
149
|
spec = {"rule": spec} if isinstance(spec, str) else dict(spec)
|
|
148
150
|
spec["path"] = p
|
|
149
151
|
items.append(spec)
|
|
@@ -493,7 +495,7 @@ def render(rep, max_diffs):
|
|
|
493
495
|
return "\n".join(out) + "\n"
|
|
494
496
|
|
|
495
497
|
|
|
496
|
-
def
|
|
498
|
+
def _main(argv=None):
|
|
497
499
|
ap = argparse.ArgumentParser(description="Golden-master diff of old vs new recorded outputs.")
|
|
498
500
|
ap.add_argument("old", help="directory or JSONL file of the old system's outputs")
|
|
499
501
|
ap.add_argument("new", help="directory or JSONL file of the new system's outputs")
|
|
@@ -520,5 +522,15 @@ def main(argv=None):
|
|
|
520
522
|
return 0 if rep["verdict"] != "NOT EQUIVALENT" else 1
|
|
521
523
|
|
|
522
524
|
|
|
525
|
+
|
|
526
|
+
def main(argv=None):
|
|
527
|
+
"""Run, and turn an input that parses but has the wrong shape into one clear error."""
|
|
528
|
+
try:
|
|
529
|
+
return _main(argv)
|
|
530
|
+
except (KeyError, TypeError, AttributeError, IndexError, ValueError) as e:
|
|
531
|
+
print("error: couldn't read the input as recordings and rules (%s: %s). If it is one, this is a bug: "
|
|
532
|
+
"please report it with the file." % (type(e).__name__, e), file=sys.stderr)
|
|
533
|
+
return 2
|
|
534
|
+
|
|
523
535
|
if __name__ == "__main__":
|
|
524
536
|
sys.exit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebase-tour",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Get a newcomer (a new engineer, or the agent itself) oriented in an unfamiliar repository before they make one specific change, with a short tour scoped to that change. It gives the install, run and test commands taken from CI, package scripts and the Makefile; the path the change touches, traced through the code with file:line citations; where the change goes; the conventions the code actually follows; the traps (generated files, a second copy of the logic, untested code, env vars, flags); and a first step that can be checked. Use when someone says \"I'm new to this repo\", \"get me oriented\", \"where would I make this change\", \"walk me through the code for X\", \"onboard me before I start this ticket\", or \"how do I run and test this\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -164,7 +164,12 @@ def tox_nox(rel, name, lines, out, targets):
|
|
|
164
164
|
if sec.group(2):
|
|
165
165
|
targets["tox"].add(sec.group(2))
|
|
166
166
|
elif env and re.match(r"^\s*commands\s*=", line):
|
|
167
|
-
|
|
167
|
+
cmds = [line.split("=", 1)[1].strip()]
|
|
168
|
+
for nxt in lines[i + 1:]:
|
|
169
|
+
if not nxt.strip() or not nxt[0].isspace():
|
|
170
|
+
break
|
|
171
|
+
cmds.append(nxt.strip())
|
|
172
|
+
cmd = "; ".join(c for c in cmds if c and not c.startswith("#"))
|
|
168
173
|
out.append({"command": "tox -e %s" % env, "runs": cmd, "where": "%s:%d" % (rel, i + 1)})
|
|
169
174
|
else:
|
|
170
175
|
for i, line in enumerate(lines):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dependency-upgrade-review",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Give a merge verdict on a dependency-upgrade pull request (Dependabot, Renovate or a hand-made bump) across npm, pip, Go, Cargo and Maven lockfiles and GitHub Actions pins. Reads the release notes for every skipped version, lists what the lockfile changes beyond the PR title (transitive bumps, new and removed packages, install scripts, registry changes), checks each SHA pin against its release tag and against the PR's other pins, checks raised runtime floors against the images and runners that install it, says whether CI ran the changed code, traces what each caller of a changed API now does and writes the rollback. The reply leads with MERGE, MERGE AFTER CHECKS or DO NOT MERGE and the reasons that decide it. Use when the user says \"review this Dependabot PR\", \"is this Renovate bump safe to merge\", \"can I merge this dependency upgrade\", \"check this lockfile diff\", or \"is this action SHA pin right\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -333,7 +333,7 @@ def h_package_lock(rep, path, hunks):
|
|
|
333
333
|
|
|
334
334
|
def h_yarn_lock(rep, path, hunks):
|
|
335
335
|
def key(text):
|
|
336
|
-
m = re.match(r'^"?((?:@[^@/\s"]+/)?[^@\s",]+)@[
|
|
336
|
+
m = re.match(r'^"?((?:@[^@/\s"]+/)?[^@\s",]+)@[^"]*"?:\s*$', text)
|
|
337
337
|
return m.group(1) if m and not text.startswith((" ", "#")) else None
|
|
338
338
|
fields = {"version": re.compile(r'^\s+version:?\s+"?([^"\s]+)"?'),
|
|
339
339
|
"resolved": re.compile(r'^\s+resolved:?\s+"?([^"\s#]+)'),
|
|
@@ -599,7 +599,7 @@ def analyse(files, title):
|
|
|
599
599
|
t = title.lower()
|
|
600
600
|
for r in rep.changes:
|
|
601
601
|
short = r["name"].split("/")[-1].lower()
|
|
602
|
-
r["in_title"] = r[
|
|
602
|
+
r["in_title"] = re.search(rf"(?<![\w@/.-]){re.escape(r['name'].lower())}(?![\w-])", t) is not None or (len(short) > 3 and re.search(rf"\b{re.escape(short)}\b", t) is not None)
|
|
603
603
|
for (repo, tag_), shas in rep.sha_pins.items():
|
|
604
604
|
if len(shas) > 1:
|
|
605
605
|
where = "; ".join(f"{s} in {', '.join(dict.fromkeys(ps))}" for s, ps in shas.items())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pre-merge-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Gate an existing change for merge — run one skill that fires the whole production-readiness pass. First a deterministic mechanical gate (lint + typecheck + tests, via a script whose exit code decides), then the three-reviewer panel dispatched in parallel — devils-advocate (correctness), security-reviewer (exploitability), code-quality (craft) — then a single READY / NOT READY verdict. Use before opening a PR or merging, when you want the full review fleet run on a diff you already have. Do NOT use to implement a feature from a spec (that's feature-implement-loop) or to review a document (that's doc-critique).",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -64,7 +64,13 @@ def detect(root):
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def load_config(path):
|
|
67
|
-
|
|
67
|
+
try:
|
|
68
|
+
with open(path, encoding="utf-8-sig") as f:
|
|
69
|
+
data = json.load(f)
|
|
70
|
+
except UnicodeDecodeError:
|
|
71
|
+
raise SystemExit(f"gate: --config {path} is not UTF-8 — save it as UTF-8 and retry")
|
|
72
|
+
except (OSError, ValueError) as exc:
|
|
73
|
+
raise SystemExit(f"gate: cannot read --config {path}: {exc}")
|
|
68
74
|
cmds = data.get("commands") if isinstance(data, dict) else data
|
|
69
75
|
if not isinstance(cmds, list) or not all(isinstance(c, str) for c in cmds):
|
|
70
76
|
raise SystemExit("gate: --config must hold a list of command strings (or {\"commands\": [...]})")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rollout-plan",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Plan the progressive release of a code change behind a feature flag or canary — the exposure ladder (internal, beta cohort, 1%, 5%, 25%, 50%, 100%, or what the traffic supports), and for each step who is exposed, decision and guardrail metrics with a baseline and a threshold against a control cohort, the minimum traffic and time before the numbers mean anything, a promote / hold / roll back rule set in advance, who watches and what aborts automatically; then what turning the flag off does not undo, and who removes the flag by when. Use when the user asks for a rollout plan, canary plan, ramp schedule, progressive delivery or percentage rollout, \"how long should the canary run\", \"when do we go from 5% to 25%\", or how to turn on a flagged feature safely.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -161,8 +161,8 @@ def main():
|
|
|
161
161
|
p2 = p1 + d if args.direction == "up" else p1 - d
|
|
162
162
|
if not 0 < p2 < 1:
|
|
163
163
|
fail("baseline plus the regression falls outside 0-100%")
|
|
164
|
-
if args.traffic <= 0:
|
|
165
|
-
fail("--traffic must be greater than zero")
|
|
164
|
+
if not math.isfinite(args.traffic) or args.traffic <= 0:
|
|
165
|
+
fail("--traffic must be a finite number greater than zero")
|
|
166
166
|
if not 0 < args.alpha < 1 or not 0 < args.power < 1:
|
|
167
167
|
fail("--alpha and --power must be between 0 and 1")
|
|
168
168
|
if args.max_hours <= 0:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bundle-size-review",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Find what makes a JavaScript bundle heavy and what can safely come out, from the bundler's stats file (webpack stats.json, an esbuild metafile, or the rollup-plugin-visualizer raw-data JSON that Vite builds use) read together with the repo around it (package.json, .browserslistrc, the lockfile, the bundler config and the source files that import the heavy modules). Gives each entry's first-load JavaScript in the exact bytes the stats record, kept apart from lazy chunks; finds whole-library imports, CommonJS packages that can't be tree-shaken, packages bundled twice and polyfills the browser targets don't need; names the import chain that pulls each module in before suggesting a removal; and, given before and after stats, says what grew, why, whether it lands in the first load and which per-entry budget would have caught it. Use when the user says \"why is our bundle so big\", \"what can we cut from the bundle\", \"review this stats.json\", \"why is lodash or moment in our bundle\", \"the bundle grew in this PR\", or \"set a bundle size budget\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -500,6 +500,8 @@ def load(path):
|
|
|
500
500
|
text = fh.read()
|
|
501
501
|
except OSError as e:
|
|
502
502
|
raise InputError("cannot read %s: %s" % (path, e))
|
|
503
|
+
except UnicodeDecodeError:
|
|
504
|
+
raise InputError("%s is not UTF-8 text; save it as UTF-8" % path)
|
|
503
505
|
if not text.strip():
|
|
504
506
|
raise InputError("%s is empty" % path)
|
|
505
507
|
try:
|
|
@@ -1023,7 +1025,7 @@ def parse_browserslist(queries):
|
|
|
1023
1025
|
if len(m.groups()) == 3:
|
|
1024
1026
|
v = ver_tuple(m.group(3))
|
|
1025
1027
|
if m.group(2) == ">":
|
|
1026
|
-
v = (v[
|
|
1028
|
+
v = v[:-1] + (v[-1] + 1,)
|
|
1027
1029
|
else:
|
|
1028
1030
|
v = ver_tuple(m.group(2))
|
|
1029
1031
|
b = BROWSER_ALIAS[m.group(1)]
|
|
@@ -1574,7 +1576,7 @@ def to_jsonable(o):
|
|
|
1574
1576
|
raise TypeError(type(o))
|
|
1575
1577
|
|
|
1576
1578
|
|
|
1577
|
-
def
|
|
1579
|
+
def _main(argv=None):
|
|
1578
1580
|
ap = argparse.ArgumentParser(description="Size a JS bundle from webpack stats, an esbuild metafile or rollup-plugin-visualizer raw data, and read the project files around it.")
|
|
1579
1581
|
ap.add_argument("stats", nargs="?", help="stats file, or - for stdin")
|
|
1580
1582
|
ap.add_argument("--compare", nargs=2, metavar=("BEFORE", "AFTER"), help="diff two stats files")
|
|
@@ -1629,5 +1631,15 @@ def main(argv=None):
|
|
|
1629
1631
|
return 1 if over else 0
|
|
1630
1632
|
|
|
1631
1633
|
|
|
1634
|
+
|
|
1635
|
+
def main(argv=None):
|
|
1636
|
+
"""Run, and turn an input that parses but has the wrong shape into one clear error."""
|
|
1637
|
+
try:
|
|
1638
|
+
return _main(argv)
|
|
1639
|
+
except (KeyError, TypeError, AttributeError, IndexError, ValueError) as e:
|
|
1640
|
+
print("error: couldn't read the input as webpack, esbuild or Vite stats (%s: %s). If it is one, this is a bug: "
|
|
1641
|
+
"please report it with the file." % (type(e).__name__, e), file=sys.stderr)
|
|
1642
|
+
return 2
|
|
1643
|
+
|
|
1632
1644
|
if __name__ == "__main__":
|
|
1633
1645
|
sys.exit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web-performance-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Read a Lighthouse report or PageSpeed Insights result, with field data when there is any (CrUX, PSI loadingExperience or RUM p75), and say what makes the page slow for real users and what to fix first, ranked by how much each fix moves LCP, INP and CLS at the 75th percentile. Names the LCP element and which LCP phase dominates, the elements that shift and why, the long tasks and third parties by main-thread time, and treats estimated savings as lab estimates that overlap. Use when the user shares a Lighthouse JSON or PSI result and asks \"why is this page slow\", \"what should we fix first\", \"we score 92 but Search Console says LCP is poor\", \"did our change fix INP\", \"review this Lighthouse report\", or \"how do we pass Core Web Vitals\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -107,9 +107,11 @@ def die(msg):
|
|
|
107
107
|
|
|
108
108
|
def load(path):
|
|
109
109
|
try:
|
|
110
|
-
raw = sys.stdin.read() if path == "-" else open(path, encoding="utf-8").read()
|
|
110
|
+
raw = sys.stdin.read() if path == "-" else open(path, encoding="utf-8-sig").read()
|
|
111
111
|
except OSError as exc:
|
|
112
112
|
die(f"cannot read {path}: {exc.strerror}")
|
|
113
|
+
except UnicodeDecodeError:
|
|
114
|
+
die(f"{path} is not UTF-8 text; save it as UTF-8 and retry")
|
|
113
115
|
try:
|
|
114
116
|
return json.loads(raw)
|
|
115
117
|
except json.JSONDecodeError as exc:
|
|
@@ -131,6 +133,9 @@ def classify(doc, path):
|
|
|
131
133
|
lhr = doc.get("lighthouseResult")
|
|
132
134
|
if lhr is not None and not isinstance(lhr, dict):
|
|
133
135
|
die(f"{path}: lighthouseResult is not an object")
|
|
136
|
+
if lhr and lhr.get("runtimeError"):
|
|
137
|
+
err = lhr["runtimeError"]
|
|
138
|
+
die(f"{path}: Lighthouse run failed ({err.get('code')}: {err.get('message')})")
|
|
134
139
|
return lhr, fields
|
|
135
140
|
if "record" in doc and isinstance(doc["record"], dict):
|
|
136
141
|
f = crux_field(doc["record"])
|
|
@@ -697,7 +702,7 @@ def cmp_md(c):
|
|
|
697
702
|
return "\n".join(L)
|
|
698
703
|
|
|
699
704
|
|
|
700
|
-
def
|
|
705
|
+
def _main():
|
|
701
706
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
702
707
|
ap.add_argument("reports", nargs="+", help="Lighthouse JSON, PSI JSON or CrUX JSON path, or - for stdin (max 2)")
|
|
703
708
|
ap.add_argument("--field", help="extra field data: a PSI response or CrUX API record JSON")
|
|
@@ -739,5 +744,15 @@ def main():
|
|
|
739
744
|
print(text)
|
|
740
745
|
|
|
741
746
|
|
|
747
|
+
|
|
748
|
+
def main():
|
|
749
|
+
"""Run, and turn an input that parses but has the wrong shape into one clear error."""
|
|
750
|
+
try:
|
|
751
|
+
return _main()
|
|
752
|
+
except (KeyError, TypeError, AttributeError, IndexError, ValueError) as e:
|
|
753
|
+
print("error: couldn't read the input as a Lighthouse, PageSpeed Insights or CrUX report (%s: %s). If it is one, this is a bug: "
|
|
754
|
+
"please report it with the file." % (type(e).__name__, e), file=sys.stderr)
|
|
755
|
+
return 2
|
|
756
|
+
|
|
742
757
|
if __name__ == "__main__":
|
|
743
|
-
main()
|
|
758
|
+
sys.exit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "risk-register",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Build or update an enterprise or project risk register — each risk written as cause, event and consequence, scored for likelihood and impact on a defined scale, inherent and residual ratings, a named owner, a treatment (avoid, reduce, transfer, accept), controls, a due date and a review trigger — with a heat map, the top risks that need a decision, and a stdlib script that validates the register CSV and flags overdue reviews. Use when the user wants a risk register, a risk log, a RAID log's risk section, a heat map, or says \"what are our top risks\", \"update the risk register\", \"score these risks\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -144,14 +144,24 @@ def main(argv=None):
|
|
|
144
144
|
try:
|
|
145
145
|
if args.csv == "-":
|
|
146
146
|
text = sys.stdin.read()
|
|
147
|
+
if text.startswith("\ufeff"): # Excel's "CSV UTF-8" BOM, as the file path strips it
|
|
148
|
+
text = text[1:]
|
|
147
149
|
else:
|
|
148
150
|
with io.open(args.csv, "r", encoding="utf-8-sig", newline="") as fh:
|
|
149
151
|
text = fh.read()
|
|
150
152
|
except (IOError, OSError) as exc:
|
|
151
153
|
print("error: cannot read %s: %s" % (args.csv, exc), file=sys.stderr)
|
|
152
154
|
return 2
|
|
155
|
+
except UnicodeDecodeError:
|
|
156
|
+
print("error: %s is not UTF-8 text; save it as UTF-8 (in Excel: CSV UTF-8)" % args.csv, file=sys.stderr)
|
|
157
|
+
return 2
|
|
153
158
|
|
|
154
|
-
|
|
159
|
+
try: # parse once up front, so a malformed file is one clear error, not a traceback mid-loop
|
|
160
|
+
list(csv.reader(io.StringIO(text, newline="")))
|
|
161
|
+
except csv.Error as exc:
|
|
162
|
+
print("error: %s is not a readable CSV: %s" % (args.csv, exc), file=sys.stderr)
|
|
163
|
+
return 2
|
|
164
|
+
reader = csv.DictReader(io.StringIO(text, newline=""))
|
|
155
165
|
if not reader.fieldnames:
|
|
156
166
|
print("error: %s has no header row" % args.csv, file=sys.stderr)
|
|
157
167
|
return 2
|
|
@@ -169,7 +179,10 @@ def main(argv=None):
|
|
|
169
179
|
seen_ids = {}
|
|
170
180
|
total_rows = 0
|
|
171
181
|
|
|
172
|
-
for
|
|
182
|
+
for raw in reader:
|
|
183
|
+
# reader.line_num counts blank lines and ends on this record's last line; step back over
|
|
184
|
+
# newlines inside quoted cells to the line the record starts on.
|
|
185
|
+
line_no = reader.line_num - sum(v.count("\n") for v in raw.values() if isinstance(v, str))
|
|
173
186
|
row = dict((k.strip(), (v or "").strip()) for k, v in raw.items() if k is not None)
|
|
174
187
|
if not any(row.values()):
|
|
175
188
|
continue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terraform-plan-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Review a terraform plan before apply, from the plan text or terraform show -json output. Flags every destroy and replace with the attribute forcing it, changes to stateful resources such as databases, buckets and volumes, IAM and security-group widening, drift nobody made in code, and changes outside the intended scope, then gives a SAFE TO APPLY, APPLY WITH CARE or DO NOT APPLY verdict with reasons. Use when the user pastes a plan, says \"review this terraform plan\", \"is this plan safe to apply\", \"why is terraform replacing my database\", or \"check the plan before I apply\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -66,9 +66,11 @@ def die(msg):
|
|
|
66
66
|
|
|
67
67
|
def load_plan(path):
|
|
68
68
|
try:
|
|
69
|
-
raw = sys.stdin.read() if path == "-" else open(path, encoding="utf-8").read()
|
|
69
|
+
raw = sys.stdin.read() if path == "-" else open(path, encoding="utf-8-sig").read()
|
|
70
70
|
except OSError as e:
|
|
71
71
|
die(f"cannot read {path}: {e.strerror}")
|
|
72
|
+
except UnicodeDecodeError:
|
|
73
|
+
die(f"{path} is not UTF-8 text; save it as UTF-8 (e.g. terraform show -json tfplan | Out-File -Encoding utf8 plan.json) and retry")
|
|
72
74
|
stripped = raw.lstrip()
|
|
73
75
|
if not stripped:
|
|
74
76
|
die("input is empty")
|
|
@@ -336,7 +338,7 @@ def to_markdown(r):
|
|
|
336
338
|
return "\n".join(lines) + "\n"
|
|
337
339
|
|
|
338
340
|
|
|
339
|
-
def
|
|
341
|
+
def _main():
|
|
340
342
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
341
343
|
ap.add_argument("plan", help="path to `terraform show -json` output, or - for stdin")
|
|
342
344
|
ap.add_argument("--scope", action="append", default=[],
|
|
@@ -357,5 +359,15 @@ def main():
|
|
|
357
359
|
sys.stdout.write(text)
|
|
358
360
|
|
|
359
361
|
|
|
362
|
+
|
|
363
|
+
def main():
|
|
364
|
+
"""Run, and turn an input that parses but has the wrong shape into one clear error."""
|
|
365
|
+
try:
|
|
366
|
+
return _main()
|
|
367
|
+
except (KeyError, TypeError, AttributeError, IndexError, ValueError) as e:
|
|
368
|
+
print("error: couldn't read the input as a Terraform JSON plan (%s: %s). If it is one, this is a bug: "
|
|
369
|
+
"please report it with the file." % (type(e).__name__, e), file=sys.stderr)
|
|
370
|
+
return 2
|
|
371
|
+
|
|
360
372
|
if __name__ == "__main__":
|
|
361
|
-
main()
|
|
373
|
+
sys.exit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feedback-synthesis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn raw customer feedback — interview notes, support tickets, NPS or CSAT verbatims, app-store reviews, sales-call notes — into ranked themes a product decision can rest on, each a problem statement rather than a feature request, counted by distinct people or accounts not mentions, with segments, severity kept separate from frequency, verbatim quotes with source IDs, what people value, where sources disagree, what the sample cannot tell, and the questions for the next interviews. A stdlib script computes the counts. Use when the user says \"what are customers telling us\", \"synthesise this feedback\", \"find the themes in these tickets / reviews / NPS comments\", \"affinity map these interview notes\", or \"what should we fix first based on feedback\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -75,6 +75,8 @@ def load(path: Path, person_col: str, ignore: set, segments_wanted: list):
|
|
|
75
75
|
text = path.read_text(encoding="utf-8-sig")
|
|
76
76
|
except OSError as e:
|
|
77
77
|
die(f"cannot read {path}: {e}")
|
|
78
|
+
except UnicodeDecodeError:
|
|
79
|
+
die(f"{path} is not UTF-8 text; save it as UTF-8 (in Excel: CSV UTF-8)")
|
|
78
80
|
reader = csv.DictReader(text.splitlines())
|
|
79
81
|
if not reader.fieldnames:
|
|
80
82
|
die(f"{path} has no header row")
|
|
@@ -334,7 +336,10 @@ def main(argv=None) -> int:
|
|
|
334
336
|
a = ap.parse_args(argv)
|
|
335
337
|
|
|
336
338
|
ignore = DEFAULT_IGNORE | {c.lower() for c in a.ignore}
|
|
337
|
-
|
|
339
|
+
try:
|
|
340
|
+
rows, segs, warnings = load(a.csv, a.person_col, ignore, a.segment)
|
|
341
|
+
except csv.Error as e: # e.g. a NUL byte, which Python before 3.11 rejects
|
|
342
|
+
die(f"{a.csv} is not a readable CSV: {e}")
|
|
338
343
|
if not rows:
|
|
339
344
|
die(f"{a.csv} has no feedback rows")
|
|
340
345
|
label = a.person_label or ("person" if a.person_col == "person_id" else a.person_col.replace("_id", "").replace("_", " "))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lean-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn a startup or internal-product idea into a lean canvas that finds what could kill it — the nine boxes filled only from what the user said, each entry tagged evidence-backed (with its source) or assumption, the assumptions ranked by how likely each is wrong times how badly the idea fails if it is, the cheapest test with a pass/fail line set in advance for the top ones, a computed unit-economics check when price and costs are given, and a plain call when the idea should change before anything is built. Use when the user says \"lean canvas\", \"help me think through my startup idea\", \"what are the riskiest assumptions\", \"how do I validate this idea\", \"is this idea worth building\", or \"poke holes in my business model\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -155,7 +155,7 @@ def compute(inp: dict) -> tuple[list[str], list[str]]:
|
|
|
155
155
|
f"Break-even needs {be_whole:,} active customers but the stated market holds {int(market):,}."
|
|
156
156
|
)
|
|
157
157
|
else:
|
|
158
|
-
share = be_whole / market * 100
|
|
158
|
+
share = be_whole / market * 100 if market else 0.0 # 0 needed of a 0 market
|
|
159
159
|
lines.append(f"Break-even share of the stated market: {share:.1f}%")
|
|
160
160
|
if share >= 25:
|
|
161
161
|
flags.append(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypothesis-comparison",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Compare rival explanations for an observation with analysis of competing hypotheses (ACH) — list the hypotheses and the evidence, build a consistency matrix (consistent / inconsistent / neutral), rank hypotheses by how little evidence contradicts them rather than how much supports them, name the diagnostic evidence and the linchpins, and say which next observation would tell the leaders apart. Use when the user asks \"why did X happen\", \"which explanation fits the evidence\", \"what's really causing this\", wants to stop arguing over a favourite theory, or says \"compare these hypotheses\", \"competing explanations\", \"ACH\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -33,6 +33,7 @@ Exit codes: 0 report written, 2 bad input.
|
|
|
33
33
|
|
|
34
34
|
import argparse
|
|
35
35
|
import csv
|
|
36
|
+
import io
|
|
36
37
|
import sys
|
|
37
38
|
from pathlib import Path
|
|
38
39
|
|
|
@@ -53,7 +54,11 @@ def load(path):
|
|
|
53
54
|
p = Path(path)
|
|
54
55
|
if not p.is_file():
|
|
55
56
|
fail("file not found: " + str(path))
|
|
56
|
-
|
|
57
|
+
try:
|
|
58
|
+
text = p.read_text(encoding="utf-8-sig")
|
|
59
|
+
except UnicodeDecodeError:
|
|
60
|
+
fail("{} is not UTF-8 text; save it as UTF-8 (in Excel: CSV UTF-8)".format(path))
|
|
61
|
+
with io.StringIO(text, newline="") as f:
|
|
57
62
|
reader = csv.DictReader(f)
|
|
58
63
|
if not reader.fieldnames:
|
|
59
64
|
fail("the CSV is empty")
|
|
@@ -66,6 +71,9 @@ def load(path):
|
|
|
66
71
|
fail("the CSV needs at least two hypothesis columns; ACH compares rivals")
|
|
67
72
|
rows = []
|
|
68
73
|
for i, raw in enumerate(reader, start=1):
|
|
74
|
+
if None in raw:
|
|
75
|
+
fail("row {}: more cells than the header has columns; put double quotes around any "
|
|
76
|
+
"cell that contains a comma".format(i + 1))
|
|
69
77
|
row = {(k or "").strip(): (v or "").strip() for k, v in raw.items()}
|
|
70
78
|
lrow = {k.lower(): v for k, v in row.items()}
|
|
71
79
|
ev = lrow.get("evidence", "")
|
|
@@ -120,7 +128,10 @@ def main():
|
|
|
120
128
|
ap.add_argument("-o", "--output", help="write the report here instead of stdout")
|
|
121
129
|
args = ap.parse_args()
|
|
122
130
|
|
|
123
|
-
|
|
131
|
+
try:
|
|
132
|
+
hyps, rows = load(args.matrix)
|
|
133
|
+
except csv.Error as e: # e.g. a NUL byte, which Python before 3.11 rejects
|
|
134
|
+
fail("{} is not a readable CSV: {}".format(args.matrix, e))
|
|
124
135
|
s = scores(hyps, rows)
|
|
125
136
|
order = ranking(hyps, s)
|
|
126
137
|
top = leaders(hyps, s)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "source-synthesis",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Answer a question from a set of sources the user supplied — papers, reports, articles, interview notes, emails — saying only what the material supports, labelling anything that comes from elsewhere where it appears, counting a source that repeats another once, and giving a labelled estimate when the user needs a figure the sources don't settle. Written in the length and format the user asked for; a full cited literature review only when that is the ask. Use when the user hands over several sources and asks to \"synthesise these\", \"what do these papers say\", \"pull together what we know\", \"literature review\", or \"where do the sources agree and disagree\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -44,6 +44,7 @@ SOURCE_LINE = re.compile(r"^\s*(?:[-*+]\s+)?\[?(S\d+)\]?\s*[:.)\]\-—–]\s*(\S
|
|
|
44
44
|
BRACKET = re.compile(r"\[([^\[\]]+)\](?!\()")
|
|
45
45
|
CITE_PART = re.compile(r"^\s*(S\d+)\s*(?::\s*(.*?))?\s*$")
|
|
46
46
|
LOOKS_LIKE_CITE = re.compile(r"^\s*S\d+\b")
|
|
47
|
+
COMMA_CITE = re.compile(r",\s*S\d+\s*(?::|,|$)") # [S1:p4, S2:p7]: parts joined by a comma, not ";"
|
|
47
48
|
OUTSIDE = re.compile(r"\[outside\]", re.IGNORECASE)
|
|
48
49
|
NO_CITE = "<!-- no-cite -->"
|
|
49
50
|
HEADING = re.compile(r"^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$")
|
|
@@ -218,7 +219,7 @@ def main():
|
|
|
218
219
|
continue
|
|
219
220
|
for part in inner.split(";"):
|
|
220
221
|
pm = CITE_PART.match(part)
|
|
221
|
-
if not pm:
|
|
222
|
+
if not pm or COMMA_CITE.search(pm.group(2) or ""):
|
|
222
223
|
malformed.append((n, "[" + inner + "]"))
|
|
223
224
|
continue
|
|
224
225
|
sid, loc = pm.group(1), (pm.group(2) or "").strip()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Audit an existing Agent Skill folder (SKILL.md plus its scripts, references and evals) for Claude Code, Codex, Copilot, Cursor, Kiro or Antigravity, and return a READY / FIX FIRST / REWRITE verdict with severity-tagged findings across five areas — whether a router would pick it and whether it collides with a sibling, portability, safety (mapped to the OWASP Agentic Skills Top 10), structure, and eval coverage — backed by a stdlib lint script for the mechanical checks. Use when the user says \"review this skill\", \"is my SKILL.md any good\", \"why doesn't my skill trigger\", \"is this skill safe to install\", or wants a skill checked before sharing or publishing it.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -263,7 +263,7 @@ def read_lines(path):
|
|
|
263
263
|
|
|
264
264
|
|
|
265
265
|
def check_structure(rep, skill_dir, text):
|
|
266
|
-
n = text.count("\n") + 1
|
|
266
|
+
n = text.count("\n") + (1 if text and not text.endswith("\n") else 0)
|
|
267
267
|
rep.facts["skill_md_lines"] = n
|
|
268
268
|
if n > MAX_LINES:
|
|
269
269
|
rep.add("WARN", "structure", "SKILL.md is %d lines; past %d, split material into reference files" % (n, MAX_LINES), "SKILL.md")
|
|
@@ -322,6 +322,9 @@ def check_evals(rep, skill_dir, name):
|
|
|
322
322
|
if not isinstance(e, dict) or not e.get("prompt"):
|
|
323
323
|
rep.add("ERROR", "evals", "eval #%d has no prompt" % i, "evals/evals.json")
|
|
324
324
|
continue
|
|
325
|
+
if not isinstance(e["prompt"], str):
|
|
326
|
+
rep.add("ERROR", "evals", "eval #%d prompt is not a string; write it as the text a user would type" % i, "evals/evals.json")
|
|
327
|
+
continue
|
|
325
328
|
if not isinstance(e.get("assertions"), list) or not e["assertions"]:
|
|
326
329
|
rep.add("ERROR", "evals", "eval #%d has no assertions; nothing says what a passing output looks like" % i, "evals/evals.json")
|
|
327
330
|
continue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "event-contract-draft",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Draft the contract for an event or message interface (Kafka topics, SNS/SQS, EventBridge, Pub/Sub, RabbitMQ, webhooks) as an AsyncAPI 3.0 document plus a short decisions page — past-tense event names, a required envelope with event id for deduplication, the partition key and the ordering it does and doesn't give, at-least-once delivery and how consumers stay idempotent, data minimisation with PII marked, additive-only versioning with dated deprecation, and retention, replay and dead-letter handling. Use when the user wants to define events, design a Kafka topic or message schema, write an AsyncAPI spec, agree what a producer publishes and what consumers must do, or version an event safely.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|