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.
Files changed (74) 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 +10 -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 +22 -6
  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 +14 -2
  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 +14 -2
  42. package/packs/frontend/skills/web-performance-review/manifest.json +1 -1
  43. package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +18 -3
  44. package/packs/grc/skills/risk-register/manifest.json +1 -1
  45. package/packs/grc/skills/risk-register/scripts/check_register.py +15 -2
  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 +15 -3
  48. package/packs/product-manager/skills/feedback-synthesis/manifest.json +1 -1
  49. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +6 -1
  50. package/packs/product-manager/skills/lean-canvas/manifest.json +1 -1
  51. package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +1 -1
  52. package/packs/research/skills/hypothesis-comparison/manifest.json +1 -1
  53. package/packs/research/skills/hypothesis-comparison/scripts/ach_matrix.py +13 -2
  54. package/packs/research/skills/source-synthesis/manifest.json +1 -1
  55. package/packs/research/skills/source-synthesis/scripts/citations.py +2 -1
  56. package/packs/skill-engineering/skills/skill-review/manifest.json +1 -1
  57. package/packs/skill-engineering/skills/skill-review/scripts/lint_skill.py +4 -1
  58. package/packs/solution-architect/skills/event-contract-draft/manifest.json +1 -1
  59. package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +23 -4
  60. package/packs/solution-architect/skills/figma-diagrams/manifest.json +1 -1
  61. package/packs/solution-architect/skills/figma-diagrams/scripts/frame_to_mermaid.py +1 -1
  62. package/packs/solution-architect/skills/figma-diagrams/scripts/post_comment.py +1 -1
  63. package/packs/solution-architect/skills/interactive-diagram/manifest.json +1 -1
  64. package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +5 -2
  65. package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -1
  66. package/packs/sre-oncall/skills/cloud-cost-review/scripts/cost_summary.py +7 -2
  67. package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -1
  68. package/packs/sre-oncall/skills/delivery-metrics-report/scripts/delivery_metrics.py +4 -0
  69. package/packs/trackers/skills/backlog-triage/manifest.json +1 -1
  70. package/packs/trackers/skills/backlog-triage/scripts/triage_backlog.py +11 -3
  71. package/packs/trackers/skills/team-status-report/manifest.json +1 -1
  72. package/packs/trackers/skills/team-status-report/scripts/status_counts.py +9 -2
  73. package/packs/trackers/skills/tracker-brief-sync/manifest.json +1 -1
  74. 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
- for cname, channel in (doc.get("channels") or {}).items():
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 main(argv=None):
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.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.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
- 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)
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.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.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
- 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))
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
- no_ac = [it for it in open_items if not it["has_ac"]]
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.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
- 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))
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
- planned = [it for it in items if args.sprint.lower() in it["sprint"].lower()]
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.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: