skilldrop-cli 0.16.44 → 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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/packs/converters/skills/md-to-html/manifest.json +1 -1
  3. package/packs/converters/skills/md-to-html/scripts/md_to_html.py +3 -0
  4. package/packs/design/skills/brand-kit/manifest.json +1 -1
  5. package/packs/design/skills/brand-kit/scripts/check_brand.py +11 -1
  6. package/packs/dev-team/skills/behaviour-equivalence/manifest.json +1 -1
  7. package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +11 -1
  8. package/packs/frontend/skills/bundle-size-review/manifest.json +1 -1
  9. package/packs/frontend/skills/bundle-size-review/scripts/bundle_stats.py +13 -1
  10. package/packs/frontend/skills/web-performance-review/manifest.json +1 -1
  11. package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +12 -2
  12. package/packs/grc/skills/risk-register/manifest.json +1 -1
  13. package/packs/grc/skills/risk-register/scripts/check_register.py +9 -1
  14. package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +1 -1
  15. package/packs/infra-as-code/skills/terraform-plan-review/scripts/plan_summary.py +12 -2
  16. package/packs/product-manager/skills/feedback-synthesis/manifest.json +1 -1
  17. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +6 -1
  18. package/packs/research/skills/hypothesis-comparison/manifest.json +1 -1
  19. package/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +10 -2
  20. package/packs/solution-architect/skills/event-contract-draft/manifest.json +1 -1
  21. package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +14 -2
  22. package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -1
  23. package/packs/sre-oncall/skills/cloud-cost-review/scripts/cost_summary.py +2 -0
  24. package/packs/trackers/skills/backlog-triage/manifest.json +1 -1
  25. package/packs/trackers/skills/backlog-triage/scripts/triage_backlog.py +4 -1
  26. package/packs/trackers/skills/team-status-report/manifest.json +1 -1
  27. package/packs/trackers/skills/team-status-report/scripts/status_counts.py +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilldrop-cli",
3
- "version": "0.16.44",
3
+ "version": "0.16.45",
4
4
  "description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
5
5
  "author": {
6
6
  "name": "Sanjay Ananthanarayan",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-to-html",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Turn a Markdown draft into one self-contained, print-friendly HTML file that survives being emailed or attached, with headings that have anchor links, tables, code, quotes, an optional table of contents, local images embedded, brand colours and fonts from a brand.json, Mermaid diagrams kept as text or drawn from a pinned CDN, and raw HTML escaped so nothing in the source runs. Use when the user wants Markdown \"as a web page\", \"as an HTML file I can send\", \"something people can open in a browser and print\", or says \"convert this to HTML\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -552,6 +552,9 @@ def build_toc(headings):
552
552
 
553
553
 
554
554
  def convert(src_text, base_dir, brand_path=None, toc=False, mermaid="none", allow_html=False):
555
+ # \x00 and \x01 mark the converter's own placeholders (code spans, line breaks); a source
556
+ # that contains them would collide with those markers, and neither belongs in a document.
557
+ src_text = src_text.replace("\x00", "").replace("\x01", "")
555
558
  ctx = Ctx(base_dir, allow_html, mermaid)
556
559
  brand = load_brand(brand_path, ctx)
557
560
  lines = src_text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brand-kit",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Capture a brand once as a reusable brand.json — logo files (and an on-dark version), colour roles as hex codes, heading and body fonts, voice, imagery rules, a PowerPoint template, contact details and the legal footer — plus a one-page BRAND.md, with text contrast checked and every value traced to where it came from. Use when the user wants decks, flyers or documents in their branding, says \"use our brand\", \"here's our logo and colours\", \"set up our brand kit\", or hands over brand guidelines.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -35,7 +35,7 @@ def contrast(a, b):
35
35
  return (la + 0.05) / (lb + 0.05)
36
36
 
37
37
 
38
- def main(argv):
38
+ def _main(argv):
39
39
  if len(argv) != 1:
40
40
  print(__doc__)
41
41
  return 2
@@ -116,5 +116,15 @@ def main(argv):
116
116
  return 1 if fails else 0
117
117
 
118
118
 
119
+
120
+ def main(argv):
121
+ """Run, and turn an input that parses but has the wrong shape into one clear error."""
122
+ try:
123
+ return _main(argv)
124
+ except (KeyError, TypeError, AttributeError, IndexError, ValueError) as e:
125
+ print("error: couldn't read the input as a brand file (%s: %s). If it is one, this is a bug: "
126
+ "please report it with the file." % (type(e).__name__, e), file=sys.stderr)
127
+ return 2
128
+
119
129
  if __name__ == "__main__":
120
130
  sys.exit(main(sys.argv[1:]))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "behaviour-equivalence",
3
- "version": "0.1.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": [] },
@@ -495,7 +495,7 @@ def render(rep, max_diffs):
495
495
  return "\n".join(out) + "\n"
496
496
 
497
497
 
498
- def main(argv=None):
498
+ def _main(argv=None):
499
499
  ap = argparse.ArgumentParser(description="Golden-master diff of old vs new recorded outputs.")
500
500
  ap.add_argument("old", help="directory or JSONL file of the old system's outputs")
501
501
  ap.add_argument("new", help="directory or JSONL file of the new system's outputs")
@@ -522,5 +522,15 @@ def main(argv=None):
522
522
  return 0 if rep["verdict"] != "NOT EQUIVALENT" else 1
523
523
 
524
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
+
525
535
  if __name__ == "__main__":
526
536
  sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bundle-size-review",
3
- "version": "0.2.1",
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:
@@ -1574,7 +1576,7 @@ def to_jsonable(o):
1574
1576
  raise TypeError(type(o))
1575
1577
 
1576
1578
 
1577
- def main(argv=None):
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.2",
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": {
@@ -702,7 +702,7 @@ def cmp_md(c):
702
702
  return "\n".join(L)
703
703
 
704
704
 
705
- def main():
705
+ def _main():
706
706
  ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
707
707
  ap.add_argument("reports", nargs="+", help="Lighthouse JSON, PSI JSON or CrUX JSON path, or - for stdin (max 2)")
708
708
  ap.add_argument("--field", help="extra field data: a PSI response or CrUX API record JSON")
@@ -744,5 +744,15 @@ def main():
744
744
  print(text)
745
745
 
746
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
+
747
757
  if __name__ == "__main__":
748
- main()
758
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "risk-register",
3
- "version": "0.1.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": {
@@ -152,8 +152,16 @@ def main(argv=None):
152
152
  except (IOError, OSError) as exc:
153
153
  print("error: cannot read %s: %s" % (args.csv, exc), file=sys.stderr)
154
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
155
158
 
156
- reader = csv.DictReader(io.StringIO(text))
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=""))
157
165
  if not reader.fieldnames:
158
166
  print("error: %s has no header row" % args.csv, file=sys.stderr)
159
167
  return 2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terraform-plan-review",
3
- "version": "0.1.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": {
@@ -338,7 +338,7 @@ def to_markdown(r):
338
338
  return "\n".join(lines) + "\n"
339
339
 
340
340
 
341
- def main():
341
+ def _main():
342
342
  ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
343
343
  ap.add_argument("plan", help="path to `terraform show -json` output, or - for stdin")
344
344
  ap.add_argument("--scope", action="append", default=[],
@@ -359,5 +359,15 @@ def main():
359
359
  sys.stdout.write(text)
360
360
 
361
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
+
362
372
  if __name__ == "__main__":
363
- main()
373
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedback-synthesis",
3
- "version": "0.1.0",
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
- rows, segs, warnings = load(a.csv, a.person_col, ignore, a.segment)
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": "hypothesis-comparison",
3
- "version": "0.1.3",
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
- with p.open(newline="", encoding="utf-8-sig") as f:
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")
@@ -123,7 +128,10 @@ def main():
123
128
  ap.add_argument("-o", "--output", help="write the report here instead of stdout")
124
129
  args = ap.parse_args()
125
130
 
126
- hyps, rows = load(args.matrix)
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))
127
135
  s = scores(hyps, rows)
128
136
  order = ranking(hyps, s)
129
137
  top = leaders(hyps, s)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "event-contract-draft",
3
- "version": "0.1.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": [] },
@@ -113,9 +113,11 @@ class Report:
113
113
 
114
114
  def load(path):
115
115
  try:
116
- text = sys.stdin.read() if path == "-" else Path(path).read_text(encoding="utf-8")
116
+ text = sys.stdin.read() if path == "-" else Path(path).read_text(encoding="utf-8-sig")
117
117
  except OSError as exc:
118
118
  raise SystemExit(f"error: cannot read {path}: {exc.strerror}")
119
+ except UnicodeDecodeError:
120
+ raise SystemExit(f"error: {path} is not UTF-8 text; save it as UTF-8")
119
121
  stripped = text.lstrip()
120
122
  if not stripped:
121
123
  raise SystemExit(f"error: {path} is empty")
@@ -484,7 +486,7 @@ def check(doc):
484
486
  return rep, counts
485
487
 
486
488
 
487
- def main(argv=None):
489
+ def _main(argv=None):
488
490
  parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
489
491
  parser.add_argument("path", help="AsyncAPI 3.0 document as JSON, or - for stdin")
490
492
  parser.add_argument("-o", "--output", help="write the report to this path as well as stdout")
@@ -505,5 +507,15 @@ def main(argv=None):
505
507
  return 1 if rep.fails else 0
506
508
 
507
509
 
510
+
511
+ def main(argv=None):
512
+ """Run, and turn an input that parses but has the wrong shape into one clear error."""
513
+ try:
514
+ return _main(argv)
515
+ except (KeyError, TypeError, AttributeError, IndexError, ValueError) as e:
516
+ print("error: couldn't read the input as an AsyncAPI document (%s: %s). If it is one, this is a bug: "
517
+ "please report it with the file." % (type(e).__name__, e), file=sys.stderr)
518
+ return 2
519
+
508
520
  if __name__ == "__main__":
509
521
  sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloud-cost-review",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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": {
@@ -607,6 +607,8 @@ def main(argv=None):
607
607
  fmt, used, rows = load(Path(args.csv), args.format)
608
608
  except UnicodeDecodeError:
609
609
  raise InputError("%s is not UTF-8 text; save it as UTF-8 (CSV UTF-8) and retry" % args.csv)
610
+ except csv.Error as e: # e.g. a NUL byte, which Python before 3.11 rejects
611
+ raise InputError("%s is not a readable CSV: %s" % (args.csv, e))
610
612
  a = analyse(rows, args.tag, args.month, args.top, fmt)
611
613
  except InputError as e:
612
614
  print("error: %s" % e, file=sys.stderr)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-triage",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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": {
@@ -254,7 +254,10 @@ def main(argv=None) -> int:
254
254
 
255
255
  if not args.export.exists():
256
256
  die("{} does not exist".format(args.export))
257
- rows = load_rows(args.export)
257
+ try:
258
+ rows = load_rows(args.export)
259
+ except csv.Error as e: # e.g. a NUL byte, which Python before 3.11 rejects
260
+ die("{} is not a readable export: {}".format(args.export, e))
258
261
  if not rows:
259
262
  die("{} has no issues in it".format(args.export))
260
263
  headers = []
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-status-report",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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": {
@@ -234,7 +234,10 @@ def main(argv=None) -> int:
234
234
 
235
235
  if not args.export.exists():
236
236
  die("{} does not exist".format(args.export))
237
- rows = load_rows(args.export)
237
+ try:
238
+ rows = load_rows(args.export)
239
+ except csv.Error as e: # e.g. a NUL byte, which Python before 3.11 rejects
240
+ die("{} is not a readable export: {}".format(args.export, e))
238
241
  if not rows:
239
242
  die("{} has no issues in it".format(args.export))
240
243
  headers = []