skilldrop-cli 0.16.42 → 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.
- 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 +7 -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 +11 -5
- 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 +3 -1
- 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 +1 -1
- package/packs/frontend/skills/web-performance-review/manifest.json +1 -1
- package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +6 -1
- package/packs/grc/skills/risk-register/manifest.json +1 -1
- package/packs/grc/skills/risk-register/scripts/check_register.py +6 -1
- 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 +3 -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 +3 -0
- 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 +9 -2
- 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 +17 -3
- package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -1
- package/packs/sre-oncall/skills/cloud-cost-review/scripts/cost_summary.py +5 -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 +7 -2
- package/packs/trackers/skills/team-status-report/manifest.json +1 -1
- package/packs/trackers/skills/team-status-report/scripts/status_counts.py +5 -1
- package/packs/trackers/skills/tracker-brief-sync/manifest.json +1 -1
- package/packs/trackers/skills/tracker-brief-sync/scripts/brief_sync.py +10 -2
|
@@ -79,9 +79,14 @@ def data_uri(path, label):
|
|
|
79
79
|
|
|
80
80
|
def load_json(path):
|
|
81
81
|
try:
|
|
82
|
-
|
|
82
|
+
data = json.loads(Path(path).read_text(encoding="utf-8-sig"))
|
|
83
83
|
except (OSError, json.JSONDecodeError) as e:
|
|
84
84
|
die(f"cannot read {path}: {e}")
|
|
85
|
+
except UnicodeDecodeError:
|
|
86
|
+
die(f"cannot read {path}: it is not UTF-8 text; save it as UTF-8")
|
|
87
|
+
if not isinstance(data, dict):
|
|
88
|
+
die(f"cannot read {path}: expected a JSON object {{...}}, got a {type(data).__name__}")
|
|
89
|
+
return data
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
def resolve(base, ref):
|
|
@@ -164,9 +169,13 @@ def build(spec_path, out_path, brand_ref=None):
|
|
|
164
169
|
if isinstance(cta, list):
|
|
165
170
|
warn("more than one call to action; a flyer gets one — using the first")
|
|
166
171
|
cta = cta[0] if cta else None
|
|
172
|
+
if isinstance(cta, str):
|
|
173
|
+
cta = {"text": cta}
|
|
167
174
|
if not cta:
|
|
168
175
|
warn("no call to action — what should a reader do next?")
|
|
169
176
|
img = spec.get("image") or {}
|
|
177
|
+
if isinstance(img, str):
|
|
178
|
+
img = {"path": img}
|
|
170
179
|
hero = data_uri(resolve(base, img.get("path")), "image") if isinstance(img, dict) else data_uri(resolve(base, img), "image")
|
|
171
180
|
if hero and isinstance(img, dict) and not img.get("alt"):
|
|
172
181
|
warn("image has no alt text")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "behaviour-equivalence",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Get a newcomer (a new engineer, or the agent itself) oriented in an unfamiliar repository before they make one specific change, with a short tour scoped to that change. It gives the install, run and test commands taken from CI, package scripts and the Makefile; the path the change touches, traced through the code with file:line citations; where the change goes; the conventions the code actually follows; the traps (generated files, a second copy of the logic, untested code, env vars, flags); and a first step that can be checked. Use when someone says \"I'm new to this repo\", \"get me oriented\", \"where would I make this change\", \"walk me through the code for X\", \"onboard me before I start this ticket\", or \"how do I run and test this\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -164,7 +164,12 @@ def tox_nox(rel, name, lines, out, targets):
|
|
|
164
164
|
if sec.group(2):
|
|
165
165
|
targets["tox"].add(sec.group(2))
|
|
166
166
|
elif env and re.match(r"^\s*commands\s*=", line):
|
|
167
|
-
|
|
167
|
+
cmds = [line.split("=", 1)[1].strip()]
|
|
168
|
+
for nxt in lines[i + 1:]:
|
|
169
|
+
if not nxt.strip() or not nxt[0].isspace():
|
|
170
|
+
break
|
|
171
|
+
cmds.append(nxt.strip())
|
|
172
|
+
cmd = "; ".join(c for c in cmds if c and not c.startswith("#"))
|
|
168
173
|
out.append({"command": "tox -e %s" % env, "runs": cmd, "where": "%s:%d" % (rel, i + 1)})
|
|
169
174
|
else:
|
|
170
175
|
for i, line in enumerate(lines):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dependency-upgrade-review",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Give a merge verdict on a dependency-upgrade pull request (Dependabot, Renovate or a hand-made bump) across npm, pip, Go, Cargo and Maven lockfiles and GitHub Actions pins. Reads the release notes for every skipped version, lists what the lockfile changes beyond the PR title (transitive bumps, new and removed packages, install scripts, registry changes), checks each SHA pin against its release tag and against the PR's other pins, checks raised runtime floors against the images and runners that install it, says whether CI ran the changed code, traces what each caller of a changed API now does and writes the rollback. The reply leads with MERGE, MERGE AFTER CHECKS or DO NOT MERGE and the reasons that decide it. Use when the user says \"review this Dependabot PR\", \"is this Renovate bump safe to merge\", \"can I merge this dependency upgrade\", \"check this lockfile diff\", or \"is this action SHA pin right\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -333,7 +333,7 @@ def h_package_lock(rep, path, hunks):
|
|
|
333
333
|
|
|
334
334
|
def h_yarn_lock(rep, path, hunks):
|
|
335
335
|
def key(text):
|
|
336
|
-
m = re.match(r'^"?((?:@[^@/\s"]+/)?[^@\s",]+)@[
|
|
336
|
+
m = re.match(r'^"?((?:@[^@/\s"]+/)?[^@\s",]+)@[^"]*"?:\s*$', text)
|
|
337
337
|
return m.group(1) if m and not text.startswith((" ", "#")) else None
|
|
338
338
|
fields = {"version": re.compile(r'^\s+version:?\s+"?([^"\s]+)"?'),
|
|
339
339
|
"resolved": re.compile(r'^\s+resolved:?\s+"?([^"\s#]+)'),
|
|
@@ -599,7 +599,7 @@ def analyse(files, title):
|
|
|
599
599
|
t = title.lower()
|
|
600
600
|
for r in rep.changes:
|
|
601
601
|
short = r["name"].split("/")[-1].lower()
|
|
602
|
-
r["in_title"] = r[
|
|
602
|
+
r["in_title"] = re.search(rf"(?<![\w@/.-]){re.escape(r['name'].lower())}(?![\w-])", t) is not None or (len(short) > 3 and re.search(rf"\b{re.escape(short)}\b", t) is not None)
|
|
603
603
|
for (repo, tag_), shas in rep.sha_pins.items():
|
|
604
604
|
if len(shas) > 1:
|
|
605
605
|
where = "; ".join(f"{s} in {', '.join(dict.fromkeys(ps))}" for s, ps in shas.items())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pre-merge-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Gate an existing change for merge — run one skill that fires the whole production-readiness pass. First a deterministic mechanical gate (lint + typecheck + tests, via a script whose exit code decides), then the three-reviewer panel dispatched in parallel — devils-advocate (correctness), security-reviewer (exploitability), code-quality (craft) — then a single READY / NOT READY verdict. Use before opening a PR or merging, when you want the full review fleet run on a diff you already have. Do NOT use to implement a feature from a spec (that's feature-implement-loop) or to review a document (that's doc-critique).",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -64,7 +64,13 @@ def detect(root):
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def load_config(path):
|
|
67
|
-
|
|
67
|
+
try:
|
|
68
|
+
with open(path, encoding="utf-8-sig") as f:
|
|
69
|
+
data = json.load(f)
|
|
70
|
+
except UnicodeDecodeError:
|
|
71
|
+
raise SystemExit(f"gate: --config {path} is not UTF-8 — save it as UTF-8 and retry")
|
|
72
|
+
except (OSError, ValueError) as exc:
|
|
73
|
+
raise SystemExit(f"gate: cannot read --config {path}: {exc}")
|
|
68
74
|
cmds = data.get("commands") if isinstance(data, dict) else data
|
|
69
75
|
if not isinstance(cmds, list) or not all(isinstance(c, str) for c in cmds):
|
|
70
76
|
raise SystemExit("gate: --config must hold a list of command strings (or {\"commands\": [...]})")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rollout-plan",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Plan the progressive release of a code change behind a feature flag or canary — the exposure ladder (internal, beta cohort, 1%, 5%, 25%, 50%, 100%, or what the traffic supports), and for each step who is exposed, decision and guardrail metrics with a baseline and a threshold against a control cohort, the minimum traffic and time before the numbers mean anything, a promote / hold / roll back rule set in advance, who watches and what aborts automatically; then what turning the flag off does not undo, and who removes the flag by when. Use when the user asks for a rollout plan, canary plan, ramp schedule, progressive delivery or percentage rollout, \"how long should the canary run\", \"when do we go from 5% to 25%\", or how to turn on a flagged feature safely.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -161,8 +161,8 @@ def main():
|
|
|
161
161
|
p2 = p1 + d if args.direction == "up" else p1 - d
|
|
162
162
|
if not 0 < p2 < 1:
|
|
163
163
|
fail("baseline plus the regression falls outside 0-100%")
|
|
164
|
-
if args.traffic <= 0:
|
|
165
|
-
fail("--traffic must be greater than zero")
|
|
164
|
+
if not math.isfinite(args.traffic) or args.traffic <= 0:
|
|
165
|
+
fail("--traffic must be a finite number greater than zero")
|
|
166
166
|
if not 0 < args.alpha < 1 or not 0 < args.power < 1:
|
|
167
167
|
fail("--alpha and --power must be between 0 and 1")
|
|
168
168
|
if args.max_hours <= 0:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bundle-size-review",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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[
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn a startup or internal-product idea into a lean canvas that finds what could kill it — the nine boxes filled only from what the user said, each entry tagged evidence-backed (with its source) or assumption, the assumptions ranked by how likely each is wrong times how badly the idea fails if it is, the cheapest test with a pass/fail line set in advance for the top ones, a computed unit-economics check when price and costs are given, and a plain call when the idea should change before anything is built. Use when the user says \"lean canvas\", \"help me think through my startup idea\", \"what are the riskiest assumptions\", \"how do I validate this idea\", \"is this idea worth building\", or \"poke holes in my business model\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
@@ -155,7 +155,7 @@ def compute(inp: dict) -> tuple[list[str], list[str]]:
|
|
|
155
155
|
f"Break-even needs {be_whole:,} active customers but the stated market holds {int(market):,}."
|
|
156
156
|
)
|
|
157
157
|
else:
|
|
158
|
-
share = be_whole / market * 100
|
|
158
|
+
share = be_whole / market * 100 if market else 0.0 # 0 needed of a 0 market
|
|
159
159
|
lines.append(f"Break-even share of the stated market: {share:.1f}%")
|
|
160
160
|
if share >= 25:
|
|
161
161
|
flags.append(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypothesis-comparison",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Answer a question from a set of sources the user supplied — papers, reports, articles, interview notes, emails — saying only what the material supports, labelling anything that comes from elsewhere where it appears, counting a source that repeats another once, and giving a labelled estimate when the user needs a figure the sources don't settle. Written in the length and format the user asked for; a full cited literature review only when that is the ask. Use when the user hands over several sources and asks to \"synthesise these\", \"what do these papers say\", \"pull together what we know\", \"literature review\", or \"where do the sources agree and disagree\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -44,6 +44,7 @@ SOURCE_LINE = re.compile(r"^\s*(?:[-*+]\s+)?\[?(S\d+)\]?\s*[:.)\]\-—–]\s*(\S
|
|
|
44
44
|
BRACKET = re.compile(r"\[([^\[\]]+)\](?!\()")
|
|
45
45
|
CITE_PART = re.compile(r"^\s*(S\d+)\s*(?::\s*(.*?))?\s*$")
|
|
46
46
|
LOOKS_LIKE_CITE = re.compile(r"^\s*S\d+\b")
|
|
47
|
+
COMMA_CITE = re.compile(r",\s*S\d+\s*(?::|,|$)") # [S1:p4, S2:p7]: parts joined by a comma, not ";"
|
|
47
48
|
OUTSIDE = re.compile(r"\[outside\]", re.IGNORECASE)
|
|
48
49
|
NO_CITE = "<!-- no-cite -->"
|
|
49
50
|
HEADING = re.compile(r"^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$")
|
|
@@ -218,7 +219,7 @@ def main():
|
|
|
218
219
|
continue
|
|
219
220
|
for part in inner.split(";"):
|
|
220
221
|
pm = CITE_PART.match(part)
|
|
221
|
-
if not pm:
|
|
222
|
+
if not pm or COMMA_CITE.search(pm.group(2) or ""):
|
|
222
223
|
malformed.append((n, "[" + inner + "]"))
|
|
223
224
|
continue
|
|
224
225
|
sid, loc = pm.group(1), (pm.group(2) or "").strip()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skill-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Audit an existing Agent Skill folder (SKILL.md plus its scripts, references and evals) for Claude Code, Codex, Copilot, Cursor, Kiro or Antigravity, and return a READY / FIX FIRST / REWRITE verdict with severity-tagged findings across five areas — whether a router would pick it and whether it collides with a sibling, portability, safety (mapped to the OWASP Agentic Skills Top 10), structure, and eval coverage — backed by a stdlib lint script for the mechanical checks. Use when the user says \"review this skill\", \"is my SKILL.md any good\", \"why doesn't my skill trigger\", \"is this skill safe to install\", or wants a skill checked before sharing or publishing it.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -263,7 +263,7 @@ def read_lines(path):
|
|
|
263
263
|
|
|
264
264
|
|
|
265
265
|
def check_structure(rep, skill_dir, text):
|
|
266
|
-
n = text.count("\n") + 1
|
|
266
|
+
n = text.count("\n") + (1 if text and not text.endswith("\n") else 0)
|
|
267
267
|
rep.facts["skill_md_lines"] = n
|
|
268
268
|
if n > MAX_LINES:
|
|
269
269
|
rep.add("WARN", "structure", "SKILL.md is %d lines; past %d, split material into reference files" % (n, MAX_LINES), "SKILL.md")
|
|
@@ -322,6 +322,9 @@ def check_evals(rep, skill_dir, name):
|
|
|
322
322
|
if not isinstance(e, dict) or not e.get("prompt"):
|
|
323
323
|
rep.add("ERROR", "evals", "eval #%d has no prompt" % i, "evals/evals.json")
|
|
324
324
|
continue
|
|
325
|
+
if not isinstance(e["prompt"], str):
|
|
326
|
+
rep.add("ERROR", "evals", "eval #%d prompt is not a string; write it as the text a user would type" % i, "evals/evals.json")
|
|
327
|
+
continue
|
|
325
328
|
if not isinstance(e.get("assertions"), list) or not e["assertions"]:
|
|
326
329
|
rep.add("ERROR", "evals", "eval #%d has no assertions; nothing says what a passing output looks like" % i, "evals/evals.json")
|
|
327
330
|
continue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "event-contract-draft",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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
|
-
|
|
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.
|
|
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,8 +2495,22 @@ 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
|
|
2502
|
+
|
|
2503
|
+
|
|
2499
2504
|
|
|
2505
|
+
def shown(path):
|
|
2506
|
+
"""The output path for the reply's first line: relative when it is under the current folder,
|
|
2507
|
+
absolute otherwise. On Windows a path on another drive has no relative form at all."""
|
|
2508
|
+
full = os.path.abspath(path)
|
|
2509
|
+
try:
|
|
2510
|
+
rel = os.path.relpath(full)
|
|
2511
|
+
except ValueError:
|
|
2512
|
+
return full
|
|
2513
|
+
return full if rel.startswith("..") else rel
|
|
2500
2514
|
|
|
2501
2515
|
def main(argv=None):
|
|
2502
2516
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
@@ -2569,7 +2583,7 @@ def main(argv=None):
|
|
|
2569
2583
|
receipt["output"] = os.path.abspath(a.out)
|
|
2570
2584
|
receipt["bytes"] = len(out.encode("utf-8"))
|
|
2571
2585
|
receipt["edges"] = len([e for e in spec["edges"] if not e.get("tree")]) or len(spec["edges"])
|
|
2572
|
-
shown_path =
|
|
2586
|
+
shown_path = shown(a.out)
|
|
2573
2587
|
receipt["reply_first_line"] = "[%s](%s)" % (spec["title"].replace("]", ")"), shown_path)
|
|
2574
2588
|
receipt["findings"] = spec.get("_findings", [])
|
|
2575
2589
|
if a.mermaid:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloud-cost-review",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
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.
|
|
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.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
|
-
|
|
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.
|
|
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": {
|