From 5bfd4d7e89cce0823aaea93e6dfd377b4b1101aa Mon Sep 17 00:00:00 2001 From: sherwinski Date: Thu, 22 Oct 2026 17:55:37 -0700 Subject: [PATCH 1/6] chore: exempt fenced code blocks from the parent-path gate A pbxproj snippet in a skill carries an rpath with ../../ in it. That text goes into the user's project and is not a link the plugin resolves, so the gate now skips fenced code blocks for the parent-segment check only. A new self-test case keeps a ../ in prose rejected. --- AGENT.md | 4 +++- scripts/package_directory_bundle.py | 32 +++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/AGENT.md b/AGENT.md index a49698e..427bd6a 100644 --- a/AGENT.md +++ b/AGENT.md @@ -87,7 +87,9 @@ Do not break these without a decision from the team: single call; a compound command prompts. - **Shared files have one spelling in skill text.** A skill refers to a shared script only as `/scripts/`, and to a reference only as `[…](../../references/.md)`. - No other path in a skill may contain `../`. `scripts/package_directory_bundle.py --check` + No other path in a skill may contain `../`; a fenced code block is exempt, because its + content goes into the user's project (a pbxproj `rpath`, for example) and is not a link + the plugin resolves. `scripts/package_directory_bundle.py --check` enforces this convention, and any other spelling fails the check. The convention is what lets the packager build the self-contained bundle with one rewrite (see "The OpenAI directory bundle" below). diff --git a/scripts/package_directory_bundle.py b/scripts/package_directory_bundle.py index c660856..6b89d14 100755 --- a/scripts/package_directory_bundle.py +++ b/scripts/package_directory_bundle.py @@ -212,6 +212,25 @@ def allowed_tools_lines(text): return numbers +def fenced_code_lines(text): + """Return the 1-based line numbers inside fenced code blocks. + + Only the parent-segment check skips these lines: a `../` inside a fence is + content the agent writes into the user's project (a pbxproj `rpath`, for + example), not a link the plugin resolves. Every other check still reads + fenced lines, because the `/scripts/` calls live in them. + """ + numbers = set() + in_fence = False + for lineno, line in enumerate(text.splitlines(), 1): + if line.lstrip().startswith("```"): + in_fence = not in_fence + continue + if in_fence: + numbers.add(lineno) + return numbers + + # --------------------------------------------------------------------------- # Version # --------------------------------------------------------------------------- @@ -357,12 +376,13 @@ def check_source(source): rel = relpath(path, source) text = read_text(path) allowed_tools = allowed_tools_lines(text) if path == skill_md else set() + fenced = fenced_code_lines(text) for lineno, line in enumerate(text.splitlines(), 1): if BARE_SCRIPT_RE.search(line) and lineno not in allowed_tools: findings.append("%s:%d: script path without the / prefix" % (rel, lineno)) if BARE_REFERENCE_RE.search(line): findings.append("%s:%d: reference path without the ../../ prefix" % (rel, lineno)) - if PARENT_SEGMENT_RE.search(line.replace("../../references/", "")): + if lineno not in fenced and PARENT_SEGMENT_RE.search(line.replace("../../references/", "")): findings.append("%s:%d: relative path leaves the skill folder (only ../../references/ is allowed)" % (rel, lineno)) if HOST_PATH_RE.search(line) and lineno not in allowed_tools: findings.append("%s:%d: host-specific path or environment variable" % (rel, lineno)) @@ -498,8 +518,9 @@ def check_bundle(bundle_dir): in_references = relpath(path, skill_dir).startswith("references/") text = read_text(path) allowed_tools = allowed_tools_lines(text) if path == skill_md else set() + fenced = fenced_code_lines(text) for lineno, line in enumerate(text.splitlines(), 1): - if PARENT_SEGMENT_RE.search(line): + if lineno not in fenced and PARENT_SEGMENT_RE.search(line): findings.append("%s:%d: a relative path still leaves the skill folder" % (rel, lineno)) if HOST_PATH_RE.search(line) and lineno not in allowed_tools: findings.append("%s:%d: host-specific path or environment variable" % (rel, lineno)) @@ -648,6 +669,13 @@ def self_test(source): ok = expect_failure("reference link with a single ../", bad, os.path.join(tmp, "bad-reference-link-work"), "reference path without the ../../ prefix") and ok + bad = os.path.join(tmp, "bad-parent-path-in-prose") + copy_source_subset(source, bad) + with open(os.path.join(bad, "skills", "setup", "SKILL.md"), "a", encoding="utf-8") as handle: + handle.write("\nCopy the file from ../shared/template.swift.\n") + ok = expect_failure("a ../ path in prose outside a code fence", bad, os.path.join(tmp, "bad-parent-path-in-prose-work"), + "relative path leaves the skill folder") and ok + bad = os.path.join(tmp, "bad-missing-reference") copy_source_subset(source, bad) with open(os.path.join(bad, "skills", "credentials", "SKILL.md"), "a", encoding="utf-8") as handle: From da8aea4a21c82c2712dc9fc284054f0e0272613a Mon Sep 17 00:00:00 2001 From: sherwinski Date: Thu, 22 Oct 2026 17:55:37 -0700 Subject: [PATCH 2/6] feat: install the iOS SDK from OneSignal-XCFramework with the NSE and App Group The resolver's SPM line now names the OneSignal-XCFramework package and the product-to-target map; the Podfile output gains the in-app messages subspec and a line for the extension target. The default product set is OneSignalFramework and OneSignalInAppMessages on the app target and OneSignalExtension on the extension; OneSignalLocation is added only on request. The Notification Service Extension and the App Group are now part of the default native iOS install. ios-nse.md holds the recipe, 3 new templates hold the extension files, compile_check_ios.sh compiles the Swift one against a stub of OneSignalExtension.h, and verify_integration.py checks the target, the link, and the shared App Group. The safety and telemetry contracts, the platform matrix, and the prior-install detection follow. --- references/platform-matrix.md | 2 +- references/safety-contract.md | 4 +- references/telemetry-contract.md | 13 +- scripts/compile_check_ios.sh | 46 +++- scripts/detect_platform.py | 2 +- scripts/resolve_sdk_version.py | 19 +- scripts/verify_integration.py | 56 ++++- skills/setup/SKILL.md | 2 +- .../assets/ios/NotificationService.swift.tmpl | 43 ++++ ...tificationServiceExtension-Info.plist.tmpl | 31 +++ ...ficationServiceExtension.entitlements.tmpl | 10 + skills/setup/ios-nse.md | 237 ++++++++++++++++++ skills/setup/ios.md | 63 +++-- skills/verify/platform-verification.md | 2 +- 14 files changed, 494 insertions(+), 36 deletions(-) create mode 100644 skills/setup/assets/ios/NotificationService.swift.tmpl create mode 100644 skills/setup/assets/ios/OneSignalNotificationServiceExtension-Info.plist.tmpl create mode 100644 skills/setup/assets/ios/OneSignalNotificationServiceExtension.entitlements.tmpl create mode 100644 skills/setup/ios-nse.md diff --git a/references/platform-matrix.md b/references/platform-matrix.md index 59adc92..f1a0a80 100644 --- a/references/platform-matrix.md +++ b/references/platform-matrix.md @@ -9,7 +9,7 @@ What the agent can do vs. what stays human, per platform. Verified against SDK r | **Web** (most automatable) | `