skilldrop-cli 0.16.43 → 0.16.44

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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/packs/ai-engineering/skills/agent-config-review/manifest.json +1 -1
  3. package/packs/ai-engineering/skills/agent-config-review/scripts/agent_config_scan.py +6 -1
  4. package/packs/ai-engineering/skills/ai-usage-report/manifest.json +1 -1
  5. package/packs/ai-engineering/skills/ai-usage-report/scripts/build_report.py +30 -13
  6. package/packs/career/skills/cover-letter/manifest.json +1 -1
  7. package/packs/career/skills/cover-letter/scripts/claim_check.py +25 -3
  8. package/packs/career/skills/resume-tailor/manifest.json +1 -1
  9. package/packs/career/skills/resume-tailor/scripts/claim_check.py +25 -3
  10. package/packs/converters/skills/file-to-markdown/manifest.json +1 -1
  11. package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +4 -2
  12. package/packs/converters/skills/md-to-confluence/manifest.json +1 -1
  13. package/packs/converters/skills/md-to-confluence/scripts/md_to_confluence.py +8 -5
  14. package/packs/converters/skills/md-to-html/manifest.json +1 -1
  15. package/packs/converters/skills/md-to-html/scripts/md_to_html.py +7 -0
  16. package/packs/converters/skills/md-to-pptx/manifest.json +1 -1
  17. package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +5 -4
  18. package/packs/converters/skills/mermaid-render/manifest.json +1 -1
  19. package/packs/converters/skills/mermaid-render/scripts/render_mermaid.py +2 -1
  20. package/packs/core/skills/output-hygiene/manifest.json +1 -1
  21. package/packs/core/skills/output-hygiene/scripts/scrub.py +3 -2
  22. package/packs/data-analytics/skills/sql-review/manifest.json +1 -1
  23. package/packs/data-analytics/skills/sql-review/scripts/sql_lint.py +10 -3
  24. package/packs/design/skills/brand-kit/manifest.json +1 -1
  25. package/packs/design/skills/brand-kit/scripts/check_brand.py +11 -5
  26. package/packs/design/skills/deck-builder/manifest.json +1 -1
  27. package/packs/design/skills/deck-builder/scripts/build_deck.py +19 -9
  28. package/packs/design/skills/marketing-flyer/manifest.json +1 -1
  29. package/packs/design/skills/marketing-flyer/scripts/build_flyer.py +10 -1
  30. package/packs/dev-team/skills/behaviour-equivalence/manifest.json +1 -1
  31. package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +3 -1
  32. package/packs/dev-team/skills/codebase-tour/manifest.json +1 -1
  33. package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +6 -1
  34. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +1 -1
  35. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +2 -2
  36. package/packs/dev-team/skills/pre-merge-review/manifest.json +1 -1
  37. package/packs/dev-team/skills/pre-merge-review/scripts/gate.py +7 -1
  38. package/packs/dev-team/skills/rollout-plan/manifest.json +1 -1
  39. package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +2 -2
  40. package/packs/frontend/skills/bundle-size-review/manifest.json +1 -1
  41. package/packs/frontend/skills/bundle-size-review/scripts/bundle_stats.py +1 -1
  42. package/packs/frontend/skills/web-performance-review/manifest.json +1 -1
  43. package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +6 -1
  44. package/packs/grc/skills/risk-register/manifest.json +1 -1
  45. package/packs/grc/skills/risk-register/scripts/check_register.py +6 -1
  46. package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +1 -1
  47. package/packs/infra-as-code/skills/terraform-plan-review/scripts/plan_summary.py +3 -1
  48. package/packs/product-manager/skills/lean-canvas/manifest.json +1 -1
  49. package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +1 -1
  50. package/packs/research/skills/hypothesis-comparison/manifest.json +1 -1
  51. package/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +3 -0
  52. package/packs/research/skills/source-synthesis/manifest.json +1 -1
  53. package/packs/research/skills/source-synthesis/scripts/citations.py +2 -1
  54. package/packs/skill-engineering/skills/skill-review/manifest.json +1 -1
  55. package/packs/skill-engineering/skills/skill-review/scripts/lint_skill.py +4 -1
  56. package/packs/solution-architect/skills/event-contract-draft/manifest.json +1 -1
  57. package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +9 -2
  58. package/packs/solution-architect/skills/figma-diagrams/manifest.json +1 -1
  59. package/packs/solution-architect/skills/figma-diagrams/scripts/frame_to_mermaid.py +1 -1
  60. package/packs/solution-architect/skills/figma-diagrams/scripts/post_comment.py +1 -1
  61. package/packs/solution-architect/skills/interactive-diagram/manifest.json +1 -1
  62. package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +5 -2
  63. package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -1
  64. package/packs/sre-oncall/skills/cloud-cost-review/scripts/cost_summary.py +5 -2
  65. package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -1
  66. package/packs/sre-oncall/skills/delivery-metrics-report/scripts/delivery_metrics.py +4 -0
  67. package/packs/trackers/skills/backlog-triage/manifest.json +1 -1
  68. package/packs/trackers/skills/backlog-triage/scripts/triage_backlog.py +7 -2
  69. package/packs/trackers/skills/team-status-report/manifest.json +1 -1
  70. package/packs/trackers/skills/team-status-report/scripts/status_counts.py +5 -1
  71. package/packs/trackers/skills/tracker-brief-sync/manifest.json +1 -1
  72. package/packs/trackers/skills/tracker-brief-sync/scripts/brief_sync.py +1 -1
@@ -79,9 +79,14 @@ def data_uri(path, label):
79
79
 
80
80
  def load_json(path):
81
81
  try:
82
- return json.loads(Path(path).read_text(encoding="utf-8"))
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.0",
3
+ "version": "0.1.1",
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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebase-tour",
3
- "version": "0.1.0",
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
- cmd = line.split("=", 1)[1].strip()
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.0",
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",]+)@[^:]*"?:\s*$', text)
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["name"].lower() in t or (len(short) > 3 and re.search(rf"\b{re.escape(short)}\b", t) is not None)
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.4",
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
- data = json.load(open(path, encoding="utf-8"))
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.0",
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.0",
3
+ "version": "0.2.1",
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": {
@@ -1023,7 +1023,7 @@ def parse_browserslist(queries):
1023
1023
  if len(m.groups()) == 3:
1024
1024
  v = ver_tuple(m.group(3))
1025
1025
  if m.group(2) == ">":
1026
- v = (v[0] + 1,)
1026
+ v = v[:-1] + (v[-1] + 1,)
1027
1027
  else:
1028
1028
  v = ver_tuple(m.group(2))
1029
1029
  b = BROWSER_ALIAS[m.group(1)]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-performance-review",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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"])
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "risk-register",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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,6 +144,8 @@ 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()
@@ -169,7 +171,10 @@ def main(argv=None):
169
171
  seen_ids = {}
170
172
  total_rows = 0
171
173
 
172
- for line_no, raw in enumerate(reader, start=2):
174
+ for raw in reader:
175
+ # reader.line_num counts blank lines and ends on this record's last line; step back over
176
+ # newlines inside quoted cells to the line the record starts on.
177
+ line_no = reader.line_num - sum(v.count("\n") for v in raw.values() if isinstance(v, str))
173
178
  row = dict((k.strip(), (v or "").strip()) for k, v in raw.items() if k is not None)
174
179
  if not any(row.values()):
175
180
  continue
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terraform-plan-review",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lean-canvas",
3
- "version": "0.1.0",
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.2",
3
+ "version": "0.1.3",
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": {
@@ -66,6 +66,9 @@ def load(path):
66
66
  fail("the CSV needs at least two hypothesis columns; ACH compares rivals")
67
67
  rows = []
68
68
  for i, raw in enumerate(reader, start=1):
69
+ if None in raw:
70
+ fail("row {}: more cells than the header has columns; put double quotes around any "
71
+ "cell that contains a comma".format(i + 1))
69
72
  row = {(k or "").strip(): (v or "").strip() for k, v in raw.items()}
70
73
  lrow = {k.lower(): v for k, v in row.items()}
71
74
  ev = lrow.get("evidence", "")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "source-synthesis",
3
- "version": "0.2.0",
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.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.0",
3
+ "version": "0.1.1",
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": [] },
@@ -376,7 +376,8 @@ def collect_messages(doc):
376
376
  if id(m) not in seen:
377
377
  seen[id(m)] = key
378
378
  out.append((m.get("name") or key, f"message {key}", m))
379
- for cname, channel in (doc.get("channels") or {}).items():
379
+ channels = doc.get("channels") or {}
380
+ for cname, channel in (channels.items() if isinstance(channels, dict) else ()):
380
381
  ch = deref(doc, channel)
381
382
  for key, msg in (ch.get("messages") or {}).items():
382
383
  m = deref(doc, msg)
@@ -429,6 +430,9 @@ def check(doc):
429
430
  if not version.startswith("3."):
430
431
  rep.fail("asyncapi", f"version is '{version or 'missing'}'; this checks AsyncAPI 3.x")
431
432
  info = doc.get("info") or {}
433
+ if not isinstance(info, dict):
434
+ rep.fail("info", "must be an object with title and version")
435
+ info = {}
432
436
  for field in ("title", "version"):
433
437
  if not info.get(field):
434
438
  rep.fail("info", f"missing info.{field}")
@@ -437,7 +441,10 @@ def check(doc):
437
441
  check_refs(doc, doc, rep)
438
442
 
439
443
  channels = doc.get("channels") or {}
440
- if not channels:
444
+ if not isinstance(channels, dict):
445
+ rep.fail("channels", "must be an object keyed by channel name, not a list")
446
+ channels = {}
447
+ elif not channels:
441
448
  rep.fail("channels", "no channels")
442
449
  for name, channel in channels.items():
443
450
  check_channel(doc, name, deref(doc, channel), rep)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-diagrams",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Work with Figma and FigJam files via the Figma REST API — read existing file structure, list frames/pages, post comments, and produce FigJam-importable diagram specs. Use when the user mentions a Figma URL, asks to inspect/audit a Figma file, wants to comment on a design programmatically, or wants their architecture diagram in Figma.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -82,7 +82,7 @@ def main(argv: list[str]) -> int:
82
82
  node_id = argv[1]
83
83
 
84
84
  data = get(f"/v1/files/{file_key}/nodes", params={"ids": node_id})
85
- node = data.get("nodes", {}).get(node_id, {}).get("document")
85
+ node = (data.get("nodes", {}).get(node_id) or {}).get("document")
86
86
  if not node:
87
87
  sys.stderr.write(f"error: node {node_id} not found in {file_key}\n")
88
88
  return 1
@@ -28,7 +28,7 @@ def main(argv: list[str]) -> int:
28
28
 
29
29
  if args.node_id:
30
30
  nodes = get(f"/v1/files/{file_key}/nodes", params={"ids": args.node_id})
31
- node = nodes.get("nodes", {}).get(args.node_id, {}).get("document", {})
31
+ node = (nodes.get("nodes", {}).get(args.node_id) or {}).get("document") or {}
32
32
  box = node.get("absoluteBoundingBox")
33
33
  if not box:
34
34
  sys.stderr.write(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "interactive-diagram",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Turn a codebase, a system description, a process or a database schema into one self-contained interactive HTML diagram (architecture, workflow, data flow, lifecycle, ERD, sequence, class, tree, timeline or waterfall) where every box and arrow cites the file and line or the words it came from, anything inferred is drawn dashed as an assumption, and each cited line is checked against the repository before the page is written. Laid out automatically, opens offline with no CDN, and exports SVG, PNG and Mermaid. Use when the user asks to \"visualize\", \"map\" or \"make an interactive diagram of\" a repo, a request path, a pipeline, an approval process, a set of tables, an incident's events or a slow trace, or wants a diagram they can click through and share as one file.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -1287,7 +1287,7 @@ def timeline_findings(spec, evs, kind):
1287
1287
  out = []
1288
1288
  order = sorted(evs, key=lambda e: e["_t"][0])
1289
1289
  if len(order) >= 2:
1290
- first = order[0]["when"]
1290
+ first = order[0]["when"].split(" to ")[0]
1291
1291
  if kind.startswith("abs") and ":" in first and not re.match(r"^~?\d{2} [A-Z][a-z]{2} ", first):
1292
1292
  first = fmt_when(order[0]["_t"][0], kind, 86400) + " " + first # one day: give its date once
1293
1293
  out.append("From %s to %s: %s." % (first, order[-1]["when"].split(" to ")[0], fmt_span(order[-1]["_t"][0] - order[0]["_t"][0], kind)))
@@ -2495,7 +2495,10 @@ def build_html(spec, svg, mermaid, issues, theme):
2495
2495
 
2496
2496
  def load(path):
2497
2497
  with open(path, encoding="utf-8") as fh:
2498
- return json.load(fh)
2498
+ spec = json.load(fh)
2499
+ if not isinstance(spec, dict):
2500
+ raise ValueError("the spec must be a JSON object at the top level, not a %s" % type(spec).__name__)
2501
+ return spec
2499
2502
 
2500
2503
 
2501
2504
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloud-cost-review",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Review actual cloud spend from a cost export (AWS CUR or Data Exports, GCP billing export, Azure cost details) and produce a ranked savings report — idle and unattached resources, oversized instances backed by utilisation evidence, storage tiering, data transfer hot spots, commitment coverage and untagged spend — each finding with a monthly saving computed from the bill, a confidence level and an ease rating. Use when the user shares a cost CSV and says \"where are we wasting money\", \"review our AWS bill\", \"why did the cloud bill jump this month\", \"find savings in our GCP/Azure spend\", or \"should we buy savings plans\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -110,7 +110,7 @@ FORMATS = {
110
110
  },
111
111
  }
112
112
 
113
- COMPUTE_RE = re.compile(r"BoxUsage|DedicatedUsage|HostUsage|Fargate-(vCPU|GB)-Hours|Lambda-GB-Second|InstanceUsage|NodeUsage|"
113
+ COMPUTE_RE = re.compile(r"BoxUsage|SpotUsage|DedicatedUsage|HostUsage|Fargate-(vCPU|GB)-Hours|Lambda-GB-Second|InstanceUsage|NodeUsage|"
114
114
  r"Instance Core|Instance Ram|vCPU", re.I)
115
115
  GENERIC_COMPUTE_SERVICE_RE = re.compile(r"compute|virtual machine|\bvm\b|ec2|fargate|lambda|functions", re.I)
116
116
  HOTSPOTS = [
@@ -603,7 +603,10 @@ def main(argv=None):
603
603
  raise InputError("--month must be YYYY-MM, got %r" % args.month)
604
604
  if args.top < 1:
605
605
  raise InputError("--top must be at least 1")
606
- fmt, used, rows = load(Path(args.csv), args.format)
606
+ try:
607
+ fmt, used, rows = load(Path(args.csv), args.format)
608
+ except UnicodeDecodeError:
609
+ raise InputError("%s is not UTF-8 text; save it as UTF-8 (CSV UTF-8) and retry" % args.csv)
607
610
  a = analyse(rows, args.tag, args.month, args.top, fmt)
608
611
  except InputError as e:
609
612
  print("error: %s" % e, file=sys.stderr)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-metrics-report",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Produce a delivery metrics report for one service from deployment, incident and pull-request exports — the DORA metrics (deployment frequency, change lead time, change fail rate, failed deployment recovery time) plus flow metrics (WIP, cycle time p50/p85, throughput), computed by a script with per-week trends and explicit data gaps, then interpreted for what changed and why. Use when the user wants DORA metrics, says \"how are we doing on the four keys\", \"what's our change failure rate\", \"compute lead time and deploy frequency from this CSV\", or \"build our delivery metrics report\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -50,6 +50,8 @@ def parse_offset(text: str) -> timezone:
50
50
  return timezone.utc
51
51
  if not m:
52
52
  raise InputError("--assume-tz must look like +00:00, -05:00 or Z, got %r" % text)
53
+ if int(m.group(2)) > 23 or int(m.group(3)) > 59:
54
+ raise InputError("--assume-tz offset out of range (hours 00-23, minutes 00-59), got %r" % text)
53
55
  sign = 1 if m.group(1) == "+" else -1
54
56
  return timezone(sign * timedelta(hours=int(m.group(2)), minutes=int(m.group(3))))
55
57
 
@@ -179,6 +181,8 @@ def compute(args) -> dict:
179
181
  dep_rows = read_csv(Path(args.deployments), ["deploy_id", "service", "deployed_at", "status"], "deployments")
180
182
  inc_rows = read_csv(Path(args.incidents), ["incident_id", "service", "started_at", "resolved_at"], "incidents") if args.incidents else None
181
183
  pr_rows = read_csv(Path(args.prs), ["pr_id", "opened_at", "merged_at"], "prs") if args.prs else None
184
+ if pr_rows is not None and not pr_rows:
185
+ raise InputError("prs file %s has a header but no rows; add PR rows or drop --prs" % args.prs)
182
186
 
183
187
  services_seen = sorted({r["service"] for r in dep_rows if r["service"]})
184
188
  svc = args.service
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-triage",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Triage a whole backlog export from Jira, Linear or GitHub Projects (CSV, JSON or an MCP tool) — duplicate and near-duplicate pairs with the reason, items missing acceptance criteria or an owner, stale items, oversized items to split, priority conflicts, and a proposed ordering with a reason per item — ending in a change list the user can apply. A stdlib script computes the mechanical checks. Use when the user wants to clean up, groom or refine a backlog, find duplicate tickets, or says \"triage our backlog\", \"what should we pull next?\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -102,6 +102,8 @@ def load_rows(path: Path) -> list:
102
102
  text = path.read_text(encoding="utf-8-sig")
103
103
  except OSError as e:
104
104
  die("cannot read {}: {}".format(path, e))
105
+ except UnicodeDecodeError:
106
+ die("{} is not UTF-8 text; save it as UTF-8 and retry".format(path))
105
107
  if suffix in (".csv", ".tsv"):
106
108
  delim = "\t" if suffix == ".tsv" else ","
107
109
  reader = csv.reader(io.StringIO(text, newline=""), delimiter=delim)
@@ -175,6 +177,8 @@ def parse_date(s: str):
175
177
  if not s:
176
178
  return None
177
179
  s2 = s[:-1] + "+00:00" if s.endswith("Z") else s
180
+ # Python < 3.11 needs a colon in the offset (Jira REST writes +0000).
181
+ s2 = re.sub(r"(\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?[+-]\d{2})(\d{2})$", r"\1:\2", s2)
178
182
  try:
179
183
  return datetime.fromisoformat(s2).date()
180
184
  except ValueError:
@@ -283,8 +287,9 @@ def main(argv=None) -> int:
283
287
  })
284
288
  open_items = [it for it in items if not it["done"]]
285
289
 
286
- no_owner = [it for it in open_items if not it["owner"] or it["owner"].lower() in ("unassigned", "none")]
287
- no_ac = [it for it in open_items if not it["has_ac"]]
290
+ no_owner = [it for it in open_items if not it["owner"] or it["owner"].lower() in ("unassigned", "none")] \
291
+ if "owner" in mp else []
292
+ no_ac = [it for it in open_items if not it["has_ac"]] if ("description" in mp or "acceptance" in mp) else []
288
293
  no_date = [it for it in open_items if it["updated"] is None]
289
294
  stale = []
290
295
  for it in open_items:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-status-report",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Write a weekly team status report from tracker data (Jira, Linear or GitHub Projects, via CSV, JSON or an MCP tool) — shipped, in progress, blocked with the blocker and owner, slipped against plan, risks and asks — with every number computed from the data by a stdlib script, never estimated, and a RAG status that states the rule that set it. Use when the user wants a weekly status update, sprint report or stakeholder update from the tracker, or says \"write this week's status report\", \"are we on track?\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -96,6 +96,8 @@ def load_rows(path: Path) -> list:
96
96
  text = path.read_text(encoding="utf-8-sig")
97
97
  except OSError as e:
98
98
  die("cannot read {}: {}".format(path, e))
99
+ except UnicodeDecodeError:
100
+ die("{} is not UTF-8 text; save it as UTF-8 and retry".format(path))
99
101
  if suffix in (".csv", ".tsv"):
100
102
  delim = "\t" if suffix == ".tsv" else ","
101
103
  reader = csv.reader(io.StringIO(text, newline=""), delimiter=delim)
@@ -309,7 +311,9 @@ def main(argv=None) -> int:
309
311
  planned = [by_key[k] for k in keys if k in by_key]
310
312
  plan_source = "plan file {} ({} keys)".format(args.plan_file.name, len(keys))
311
313
  elif args.sprint:
312
- planned = [it for it in items if args.sprint.lower() in it["sprint"].lower()]
314
+ # Whole-name match; repeated Sprint columns are joined with ", " (carried-over items).
315
+ want = args.sprint.strip().lower()
316
+ planned = [it for it in items if want in [x.strip().lower() for x in it["sprint"].split(",")]]
313
317
  plan_source = "items in sprint '{}'".format(args.sprint)
314
318
  elif "due" in mp:
315
319
  planned = [it for it in items if it["due"] and start <= it["due"] <= end]