diff --git a/.agents/skills/ship/SKILL.md b/.agents/skills/ship/SKILL.md new file mode 100644 index 0000000..c8d6c7e --- /dev/null +++ b/.agents/skills/ship/SKILL.md @@ -0,0 +1,84 @@ +--- +name: ship +description: Ship the current work to main via a squash-merged PR. Branches off latest main (unless a branch is given), pushes, opens a PR using the repo template, then squash-merges with admin bypass. Use when the user says "ship it", "/ship", or wants to land changes on main end-to-end. +--- + +# /ship + +End-to-end "land this on main" workflow for Cotabby. The goal is a **single linear +commit on main** — squash merge, never a merge commit. Cotabby's `main` ruleset +rejects merge commits, so squashing is what keeps history clean; `--admin` bypasses +the required-status-check protection so the owner can merge directly. + +`$ARGUMENTS` is optional: +- empty → branch off the latest `origin/main` with a derived name +- a branch name (e.g. `feat/foo`) → use/create that branch instead of deriving one +- `from ` → base the new branch on `` instead of `origin/main` + +## Steps + +1. **Establish the branch.** + - `git fetch origin`. + - If the user is already on a feature branch that holds the work, keep it. + - Otherwise (on `main`/detached, or a branch name was given), create the branch + off the latest base: `git checkout -b origin/main` (or the `from ` + base). Derive `` from the change: `feat/`, `fix/`, or `chore/` + a short + kebab slug. Never do the work directly on `main`. + +2. **Commit the work.** Stage and commit anything pending. Follow the repo's + GitHub rules in `.Codex/AGENTS.md`: **no `Co-Authored-By` trailers.** Write a + concise, real commit message. + +3. **Validate before pushing.** Run the narrowest useful checks, broaden if shared + behavior changed: + ```bash + swiftlint lint --quiet + xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build + ``` + For test-affecting changes also run `build-for-testing`. Local `test` execution + often fails on a **Team ID / signing mismatch** — that's an environment issue, not + a code failure; report it and rely on `build-for-testing` succeeding. + - **XcodeGen:** `project.yml` is the source of truth and `Cotabby.xcodeproj` is + generated. New files under `Cotabby/` and `CotabbyTests/` are auto-discovered — + no project edit needed. Only structural changes (targets, build settings, + packages, scheme) require editing `project.yml` then `xcodegen generate` and + committing the regenerated project. Fix all lint/build errors before continuing. + +4. **Push.** `git push -u origin `. + +5. **Open the PR using the repo template.** Read `.github/PULL_REQUEST_TEMPLATE.md` + and fill in **every** section — Summary (what + why), Validation (what you + actually ran and saw), Linked issues (`Fixes #N` / `Refs #N`), Risk / rollout + notes. Do not invent a format. Use a heredoc body: + ```bash + gh pr create --base main --head --title "" --body "$(cat <<'EOF' + ## Summary + ... + EOF + )" + ``` + +6. **Confirm, then squash-merge with admin bypass.** Merging to `main` is an + irreversible outward action — show the PR URL and the one-line summary, and get an + explicit go-ahead unless the user already said to merge in this turn. Then: + ```bash + gh pr merge <branch-or-#> --squash --admin --delete-branch + ``` + `--squash` keeps main linear (no merge commit → satisfies the ruleset); + `--admin` bypasses required checks; `--delete-branch` cleans up the remote branch. + +7. **Sync local main.** + ```bash + git checkout main && git pull --ff-only origin main + ``` + Confirm `main` now contains the squashed commit and report the result. + +## Guardrails + +- **Never force-push `main` or rebase published history to "fix" merges.** If + `origin/main` moved while you worked, integrate it (rebase the branch onto the new + `origin/main`) and re-validate — don't clobber others' commits. +- **Never delete or overwrite work you didn't create** without checking it first. +- If validation fails, stop and surface the failure — don't merge red. +- If the user named a target other than `main`, ship there instead, but keep the + squash-merge shape. diff --git a/Cotabby.xcodeproj/project.pbxproj b/Cotabby.xcodeproj/project.pbxproj index 5eeb512..0dbfe2a 100644 --- a/Cotabby.xcodeproj/project.pbxproj +++ b/Cotabby.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + A1C0FFEEA1C0FFEEA1C0F002 /* AccessibilityCaptureSuppressionPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0FFEEA1C0FFEEA1C0F001 /* AccessibilityCaptureSuppressionPolicy.swift */; }; + A1C0FFEEA1C0FFEEA1C0F004 /* AccessibilityCaptureSuppressionPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0FFEEA1C0FFEEA1C0F003 /* AccessibilityCaptureSuppressionPolicyTests.swift */; }; 000EBFCBA8CE49537690613B /* SymSpellCorrectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C850141146422A132B2B3516 /* SymSpellCorrectorTests.swift */; }; 0187EAA1D37B92DD5B264016 /* PermissionDragSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D00A031C0D9CF2A7A2330D9 /* PermissionDragSourceView.swift */; }; 02DA43985CDAE6859014F14F /* SuggestionOverlayPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D598CC3134879999D567455 /* SuggestionOverlayPresenter.swift */; }; @@ -321,6 +323,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + A1C0FFEEA1C0FFEEA1C0F001 /* AccessibilityCaptureSuppressionPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityCaptureSuppressionPolicy.swift; sourceTree = "<group>"; }; + A1C0FFEEA1C0FFEEA1C0F003 /* AccessibilityCaptureSuppressionPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityCaptureSuppressionPolicyTests.swift; sourceTree = "<group>"; }; 003594B09C83EF2DF35577D5 /* SuggestionDebugLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionDebugLogger.swift; sourceTree = "<group>"; }; 00824BDD8D0E9B3063827C78 /* MenuBarPresentationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarPresentationObserver.swift; sourceTree = "<group>"; }; 01B72736E416910878E8E493 /* OnboardingTemplateRecommenderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTemplateRecommenderTests.swift; sourceTree = "<group>"; }; @@ -874,6 +878,7 @@ A8A141E1F2137943476D0C82 /* CotabbyTests */ = { isa = PBXGroup; children = ( + A1C0FFEEA1C0FFEEA1C0F003 /* AccessibilityCaptureSuppressionPolicyTests.swift */, A168A7B6A7AD11559B60C56B /* ApplicationBundleMetadataTests.swift */, 78AFA4586C82E92D7FBF381B /* ArithmeticEvaluatorTests.swift */, C046CB4F3CB4BFE9391DB5DE /* AXTextGeometryResolverTests.swift */, @@ -1059,6 +1064,7 @@ isa = PBXGroup; children = ( 67C78D77B58388B15AC8B954 /* Macros */, + A1C0FFEEA1C0FFEEA1C0F001 /* AccessibilityCaptureSuppressionPolicy.swift */, 352AF5B2834FEE1F597394E4 /* ApplicationBundleMetadata.swift */, AC70775535A3428991025AB8 /* AXHelper.swift */, 85EF79E6144D6C6AD062B569 /* BaseCompletionPromptRenderer.swift */, @@ -1257,6 +1263,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1C0FFEEA1C0FFEEA1C0F002 /* AccessibilityCaptureSuppressionPolicy.swift in Sources */, 30F3F2B6D13CD583136CD787 /* AXHelper.swift in Sources */, D9C51DEDF01033E276A479CE /* AXTextGeometryResolver.swift in Sources */, F31B343F9C935A5421A526DE /* AXTreeDumpWriter.swift in Sources */, @@ -1469,6 +1476,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1C0FFEEA1C0FFEEA1C0F004 /* AccessibilityCaptureSuppressionPolicyTests.swift in Sources */, 6D0E79CF3C1A8CE53046FCE5 /* AXTextGeometryResolverTests.swift in Sources */, A36481222BB5B2A67349D389 /* ApplicationBundleMetadataTests.swift in Sources */, 4D583CB3DA253FB795EE54F9 /* ArithmeticEvaluatorTests.swift in Sources */, diff --git a/Cotabby/App/Core/CotabbyAppEnvironment.swift b/Cotabby/App/Core/CotabbyAppEnvironment.swift index 654ef7d..400a769 100644 --- a/Cotabby/App/Core/CotabbyAppEnvironment.swift +++ b/Cotabby/App/Core/CotabbyAppEnvironment.swift @@ -75,6 +75,11 @@ final class CotabbyAppEnvironment { permissionProvider: { permissionManager.accessibilityGranted }, ignoredBundleIdentifier: Bundle.main.bundleIdentifier, isCaptureSuppressedForBundle: { bundleIdentifier in + if AccessibilityCaptureSuppressionPolicy.shouldSuppressCapture( + bundleIdentifier: bundleIdentifier + ) { + return true + } guard suggestionSettings.isGloballyEnabled else { return true } if let bundleIdentifier, suggestionSettings.isApplicationDisabled(bundleIdentifier: bundleIdentifier) { diff --git a/Cotabby/Support/AccessibilityCaptureSuppressionPolicy.swift b/Cotabby/Support/AccessibilityCaptureSuppressionPolicy.swift new file mode 100644 index 0000000..3b09665 --- /dev/null +++ b/Cotabby/Support/AccessibilityCaptureSuppressionPolicy.swift @@ -0,0 +1,30 @@ +import Foundation + +/// File overview: +/// Centralizes app-level exceptions where Cotabby must not inspect the focused Accessibility tree. +/// +/// Most app compatibility rules should live in the normal availability pipeline so users can still +/// choose where Cotabby runs. This policy is narrower: it protects host apps whose transient UI is +/// destabilized by AX attribute enumeration itself. The caller should consult it before any deep +/// candidate walk, because once that walk starts the host popover may already have closed. +enum AccessibilityCaptureSuppressionPolicy { + /// Bundle identifiers whose focused AX tree is not safe to enumerate continuously. + /// + /// Apple Calendar's event-detail popover can dismiss itself when Cotabby polls text capability + /// on its temporary editor hierarchy. Suppressing capture at the app boundary is conservative, + /// but it keeps Calendar's own editing controls usable while leaving keyboard monitoring and the + /// rest of Cotabby untouched. + private static let unsafeBundleIdentifiers: Set<String> = [ + "com.apple.iCal" + ] + + /// Returns true when focus polling should stop after the cheap focused-element query and before + /// `FocusSnapshotResolver` performs AX candidate enumeration. + static func shouldSuppressCapture(bundleIdentifier: String?) -> Bool { + guard let bundleIdentifier else { + return false + } + + return unsafeBundleIdentifiers.contains(bundleIdentifier) + } +} diff --git a/CotabbyTests/AccessibilityCaptureSuppressionPolicyTests.swift b/CotabbyTests/AccessibilityCaptureSuppressionPolicyTests.swift new file mode 100644 index 0000000..c763763 --- /dev/null +++ b/CotabbyTests/AccessibilityCaptureSuppressionPolicyTests.swift @@ -0,0 +1,26 @@ +import XCTest +@testable import Cotabby + +final class AccessibilityCaptureSuppressionPolicyTests: XCTestCase { + func testCalendarCaptureIsSuppressedByDefault() { + XCTAssertTrue( + AccessibilityCaptureSuppressionPolicy.shouldSuppressCapture( + bundleIdentifier: "com.apple.iCal" + ) + ) + } + + func testOrdinaryAppCaptureIsNotSuppressed() { + XCTAssertFalse( + AccessibilityCaptureSuppressionPolicy.shouldSuppressCapture( + bundleIdentifier: "com.apple.Safari" + ) + ) + } + + func testMissingBundleIdentifierIsNotSuppressed() { + XCTAssertFalse( + AccessibilityCaptureSuppressionPolicy.shouldSuppressCapture(bundleIdentifier: nil) + ) + } +}