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
|
@@ -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")
|
|
@@ -376,7 +378,8 @@ def collect_messages(doc):
|
|
|
376
378
|
if id(m) not in seen:
|
|
377
379
|
seen[id(m)] = key
|
|
378
380
|
out.append((m.get("name") or key, f"message {key}", m))
|
|
379
|
-
|
|
381
|
+
channels = doc.get("channels") or {}
|
|
382
|
+
for cname, channel in (channels.items() if isinstance(channels, dict) else ()):
|
|
380
383
|
ch = deref(doc, channel)
|
|
381
384
|
for key, msg in (ch.get("messages") or {}).items():
|
|
382
385
|
m = deref(doc, msg)
|
|
@@ -429,6 +432,9 @@ def check(doc):
|
|
|
429
432
|
if not version.startswith("3."):
|
|
430
433
|
rep.fail("asyncapi", f"version is '{version or 'missing'}'; this checks AsyncAPI 3.x")
|
|
431
434
|
info = doc.get("info") or {}
|
|
435
|
+
if not isinstance(info, dict):
|
|
436
|
+
rep.fail("info", "must be an object with title and version")
|
|
437
|
+
info = {}
|
|
432
438
|
for field in ("title", "version"):
|
|
433
439
|
if not info.get(field):
|
|
434
440
|
rep.fail("info", f"missing info.{field}")
|
|
@@ -437,7 +443,10 @@ def check(doc):
|
|
|
437
443
|
check_refs(doc, doc, rep)
|
|
438
444
|
|
|
439
445
|
channels = doc.get("channels") or {}
|
|
440
|
-
if not channels:
|
|
446
|
+
if not isinstance(channels, dict):
|
|
447
|
+
rep.fail("channels", "must be an object keyed by channel name, not a list")
|
|
448
|
+
channels = {}
|
|
449
|
+
elif not channels:
|
|
441
450
|
rep.fail("channels", "no channels")
|
|
442
451
|
for name, channel in channels.items():
|
|
443
452
|
check_channel(doc, name, deref(doc, channel), rep)
|
|
@@ -477,7 +486,7 @@ def check(doc):
|
|
|
477
486
|
return rep, counts
|
|
478
487
|
|
|
479
488
|
|
|
480
|
-
def
|
|
489
|
+
def _main(argv=None):
|
|
481
490
|
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
482
491
|
parser.add_argument("path", help="AsyncAPI 3.0 document as JSON, or - for stdin")
|
|
483
492
|
parser.add_argument("-o", "--output", help="write the report to this path as well as stdout")
|
|
@@ -498,5 +507,15 @@ def main(argv=None):
|
|
|
498
507
|
return 1 if rep.fails else 0
|
|
499
508
|
|
|
500
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
|
+
|
|
501
520
|
if __name__ == "__main__":
|
|
502
521
|
sys.exit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-diagrams",
|
|
3
|
-
"version": "0.1.
|
|
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
|
|
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
|
|
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
|
+
"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
|
-
|
|
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.
|
|
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": {
|
|
@@ -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,12 @@ 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
|
-
|
|
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)
|
|
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))
|
|
607
612
|
a = analyse(rows, args.tag, args.month, args.top, fmt)
|
|
608
613
|
except InputError as e:
|
|
609
614
|
print("error: %s" % e, file=sys.stderr)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delivery-metrics-report",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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": {
|
|
@@ -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:
|
|
@@ -250,7 +254,10 @@ def main(argv=None) -> int:
|
|
|
250
254
|
|
|
251
255
|
if not args.export.exists():
|
|
252
256
|
die("{} does not exist".format(args.export))
|
|
253
|
-
|
|
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))
|
|
254
261
|
if not rows:
|
|
255
262
|
die("{} has no issues in it".format(args.export))
|
|
256
263
|
headers = []
|
|
@@ -283,8 +290,9 @@ def main(argv=None) -> int:
|
|
|
283
290
|
})
|
|
284
291
|
open_items = [it for it in items if not it["done"]]
|
|
285
292
|
|
|
286
|
-
no_owner = [it for it in open_items if not it["owner"] or it["owner"].lower() in ("unassigned", "none")]
|
|
287
|
-
|
|
293
|
+
no_owner = [it for it in open_items if not it["owner"] or it["owner"].lower() in ("unassigned", "none")] \
|
|
294
|
+
if "owner" in mp else []
|
|
295
|
+
no_ac = [it for it in open_items if not it["has_ac"]] if ("description" in mp or "acceptance" in mp) else []
|
|
288
296
|
no_date = [it for it in open_items if it["updated"] is None]
|
|
289
297
|
stale = []
|
|
290
298
|
for it in open_items:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-status-report",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|
|
@@ -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)
|
|
@@ -232,7 +234,10 @@ def main(argv=None) -> int:
|
|
|
232
234
|
|
|
233
235
|
if not args.export.exists():
|
|
234
236
|
die("{} does not exist".format(args.export))
|
|
235
|
-
|
|
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))
|
|
236
241
|
if not rows:
|
|
237
242
|
die("{} has no issues in it".format(args.export))
|
|
238
243
|
headers = []
|
|
@@ -309,7 +314,9 @@ def main(argv=None) -> int:
|
|
|
309
314
|
planned = [by_key[k] for k in keys if k in by_key]
|
|
310
315
|
plan_source = "plan file {} ({} keys)".format(args.plan_file.name, len(keys))
|
|
311
316
|
elif args.sprint:
|
|
312
|
-
|
|
317
|
+
# Whole-name match; repeated Sprint columns are joined with ", " (carried-over items).
|
|
318
|
+
want = args.sprint.strip().lower()
|
|
319
|
+
planned = [it for it in items if want in [x.strip().lower() for x in it["sprint"].split(",")]]
|
|
313
320
|
plan_source = "items in sprint '{}'".format(args.sprint)
|
|
314
321
|
elif "due" in mp:
|
|
315
322
|
planned = [it for it in items if it["due"] and start <= it["due"] <= end]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tracker-brief-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Turn a brief or PRD (such as prd-draft output) into tracker-ready epics and issues — title, description, acceptance criteria, labels and a link back to the doc section — as a CSV for Jira, a mapped CSV for other importers, or gh commands for GitHub; and in reverse, report drift between the doc and the tracker (issues with no section, sections with no issue, issues for Won't-have requirements). Use when the user wants to load a PRD into Jira, Linear or GitHub, says \"create tickets from this brief\", or asks \"does the tracker still match the PRD?\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -194,7 +194,7 @@ def parse_doc(path: Path, id_pattern: str, headings: bool) -> dict:
|
|
|
194
194
|
continue
|
|
195
195
|
if s.startswith("|"):
|
|
196
196
|
cells = [c.strip().strip("*").strip() for c in s.strip("|").split("|")]
|
|
197
|
-
if cells and broad.match(cells[0]):
|
|
197
|
+
if cells and broad.match(cells[0]) and not headings:
|
|
198
198
|
moscow = ""
|
|
199
199
|
for c in cells[2:]:
|
|
200
200
|
if c.lower() in MOSCOW:
|