From 141e59291b1c0109655c42bc83eae54abe50f40f Mon Sep 17 00:00:00 2001 From: flamiinngo Date: Sun, 17 May 2026 02:10:50 +0100 Subject: [PATCH 1/3] fix(scripts): fix UnicodeEncodeError in footgun checker on Windows The check-windows-footguns.py script outputs a checkmark (U+2713) and cross (U+2717) to report results. Windows terminals default to cp1252, which cannot encode these characters, so running the script on Windows threw a UnicodeEncodeError before any results were printed. This made the tool completely unusable on the exact platform it exists to help -- a developer on Windows trying to check their code for Windows-safety issues would just get a crash instead. Fix: reconfigure stdout and stderr to UTF-8 at the start of main(), before any output is produced. Verified on Windows 11 Home with Python 3.13 (terminal defaulting to cp1252). --- scripts/check-windows-footguns.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/check-windows-footguns.py b/scripts/check-windows-footguns.py index f424be90710e2..7ae7ca50c4e7e 100644 --- a/scripts/check-windows-footguns.py +++ b/scripts/check-windows-footguns.py @@ -551,6 +551,14 @@ def print_rules() -> None: def main(argv: list[str]) -> int: + # Windows terminals default to cp1252, which can't encode the ✓/✗ + # characters used in the output. Reconfigure streams to UTF-8 so the + # script works correctly on the very platform it is designed to help. + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8") + args = parse_args(argv) if args.list: From 3e9db764033ac72b6cc76f69e86589806cea60fc Mon Sep 17 00:00:00 2001 From: flamiinngo Date: Sun, 17 May 2026 02:36:24 +0100 Subject: [PATCH 2/3] fix(skills): make --label and --assignee actually work in Linear skill When creating or updating a Linear issue, the --label and --assignee flags were listed in the help text but never wired up. You could pass them and the command would succeed, but the issue would be created without either. No error, no warning -- they were just silently dropped. This adds the missing name-to-ID lookups so the flags work as advertised. If a label or assignee name does not exist in the team, you now get a clear error message instead of a quiet no-op. Tested against a live Linear workspace on Windows 11. --- .../productivity/linear/scripts/linear_api.py | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/skills/productivity/linear/scripts/linear_api.py b/skills/productivity/linear/scripts/linear_api.py index cb8c5d846dd09..f2b4777a937ee 100644 --- a/skills/productivity/linear/scripts/linear_api.py +++ b/skills/productivity/linear/scripts/linear_api.py @@ -131,6 +131,32 @@ def _resolve_team_id(key_or_name: str) -> str | None: return None +def _resolve_label_id(name: str, team_id: str) -> str | None: + """Map a label name to UUID within a team (case-insensitive).""" + q = """query($id: String!) { + team(id: $id) { labels(first: 100) { nodes { id name } } } + }""" + nodes = gql(q, {"id": team_id}).get("team", {}).get("labels", {}).get("nodes", []) + nl = name.lower() + for label in nodes: + if label["name"].lower() == nl: + return label["id"] + return None + + +def _resolve_member_id(name: str, team_id: str) -> str | None: + """Map a member display name or username to UUID within a team (case-insensitive).""" + q = """query($id: String!) { + team(id: $id) { members(first: 100) { nodes { id name displayName } } } + }""" + nodes = gql(q, {"id": team_id}).get("team", {}).get("members", {}).get("nodes", []) + nl = name.lower() + for member in nodes: + if member["name"].lower() == nl or member["displayName"].lower() == nl: + return member["id"] + return None + + def cmd_list_projects(args: argparse.Namespace) -> None: if args.team: tid = _resolve_team_id(args.team) @@ -229,7 +255,18 @@ def cmd_create_issue(args: argparse.Namespace) -> None: inp["priority"] = args.priority if args.parent: inp["parentId"] = args.parent - # TODO: label + assignee name->id lookup (omitted for v1 brevity) + if args.label: + lid = _resolve_label_id(args.label, tid) + if lid is None: + sys.stderr.write(f"Label '{args.label}' not found in team '{args.team}'.\n") + sys.exit(1) + inp["labelIds"] = [lid] + if args.assignee: + aid = _resolve_member_id(args.assignee, tid) + if aid is None: + sys.stderr.write(f"Assignee '{args.assignee}' not found in team '{args.team}'.\n") + sys.exit(1) + inp["assigneeId"] = aid q = """mutation($input: IssueCreateInput!) { issueCreate(input: $input) { @@ -247,6 +284,26 @@ def cmd_update_issue(args: argparse.Namespace) -> None: inp["description"] = args.description if args.priority is not None: inp["priority"] = args.priority + if args.label or args.assignee: + # Need the team id to resolve label/member names + get_q = """query($id: String!) { issue(id: $id) { team { id } } }""" + issue = gql(get_q, {"id": args.identifier}).get("issue") + if not issue: + sys.stderr.write(f"Issue not found: {args.identifier}\n") + sys.exit(1) + tid = issue["team"]["id"] + if args.label: + lid = _resolve_label_id(args.label, tid) + if lid is None: + sys.stderr.write(f"Label '{args.label}' not found in this issue's team.\n") + sys.exit(1) + inp["labelIds"] = [lid] + if args.assignee: + aid = _resolve_member_id(args.assignee, tid) + if aid is None: + sys.stderr.write(f"Assignee '{args.assignee}' not found in this issue's team.\n") + sys.exit(1) + inp["assigneeId"] = aid if not inp: sys.stderr.write("No update fields provided.\n") sys.exit(1) From 344d45955ed8f213738f6903e8574bc88d20aad1 Mon Sep 17 00:00:00 2001 From: flamiinngo Date: Sun, 17 May 2026 03:09:07 +0100 Subject: [PATCH 3/3] fix(linear): handle pagination, null displayName, and null team in resolvers Three crash bugs found in code review: 1. _resolve_member_id crashed with AttributeError when the GraphQL API returned null for a member displayName field. Fixed by using (member.get('displayName') or '').lower() instead of direct access. 2. cmd_update_issue crashed with TypeError when issue['team'] was null. Fixed by using (issue.get('team') or {}).get('id') with an explicit error message if the team id cannot be determined. 3. Both _resolve_label_id and _resolve_member_id fetched only the first 100 results with no pagination. Teams with more than 100 labels or members would silently return None, causing unnecessary failures. Fixed by paginating with hasNextPage/endCursor until all results are checked. --- .../productivity/linear/scripts/linear_api.py | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/skills/productivity/linear/scripts/linear_api.py b/skills/productivity/linear/scripts/linear_api.py index f2b4777a937ee..27035b3ab2508 100644 --- a/skills/productivity/linear/scripts/linear_api.py +++ b/skills/productivity/linear/scripts/linear_api.py @@ -132,28 +132,50 @@ def _resolve_team_id(key_or_name: str) -> str | None: def _resolve_label_id(name: str, team_id: str) -> str | None: - """Map a label name to UUID within a team (case-insensitive).""" - q = """query($id: String!) { - team(id: $id) { labels(first: 100) { nodes { id name } } } + """Map a label name to UUID within a team (case-insensitive), paginating if needed.""" + q = """query($id: String!, $after: String) { + team(id: $id) { + labels(first: 250, after: $after) { + nodes { id name } + pageInfo { hasNextPage endCursor } + } + } }""" - nodes = gql(q, {"id": team_id}).get("team", {}).get("labels", {}).get("nodes", []) nl = name.lower() - for label in nodes: - if label["name"].lower() == nl: - return label["id"] + cursor = None + while True: + labels = gql(q, {"id": team_id, "after": cursor}).get("team", {}).get("labels", {}) + for label in labels.get("nodes", []): + if label["name"].lower() == nl: + return label["id"] + page_info = labels.get("pageInfo", {}) + if not page_info.get("hasNextPage"): + break + cursor = page_info.get("endCursor") return None def _resolve_member_id(name: str, team_id: str) -> str | None: - """Map a member display name or username to UUID within a team (case-insensitive).""" - q = """query($id: String!) { - team(id: $id) { members(first: 100) { nodes { id name displayName } } } + """Map a member display name or username to UUID within a team (case-insensitive), paginating if needed.""" + q = """query($id: String!, $after: String) { + team(id: $id) { + members(first: 250, after: $after) { + nodes { id name displayName } + pageInfo { hasNextPage endCursor } + } + } }""" - nodes = gql(q, {"id": team_id}).get("team", {}).get("members", {}).get("nodes", []) nl = name.lower() - for member in nodes: - if member["name"].lower() == nl or member["displayName"].lower() == nl: - return member["id"] + cursor = None + while True: + members = gql(q, {"id": team_id, "after": cursor}).get("team", {}).get("members", {}) + for member in members.get("nodes", []): + if member["name"].lower() == nl or (member.get("displayName") or "").lower() == nl: + return member["id"] + page_info = members.get("pageInfo", {}) + if not page_info.get("hasNextPage"): + break + cursor = page_info.get("endCursor") return None @@ -291,7 +313,10 @@ def cmd_update_issue(args: argparse.Namespace) -> None: if not issue: sys.stderr.write(f"Issue not found: {args.identifier}\n") sys.exit(1) - tid = issue["team"]["id"] + tid = (issue.get("team") or {}).get("id") + if not tid: + sys.stderr.write(f"Could not determine team for issue: {args.identifier}\n") + sys.exit(1) if args.label: lid = _resolve_label_id(args.label, tid) if lid is None: