Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .agents/skills/ship/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 <ref>` → base the new branch on `<ref>` 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 <name> origin/main` (or the `from <ref>`
base). Derive `<name>` 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 <branch>`.

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 <branch> --title "<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.
8 changes: 8 additions & 0 deletions Cotabby.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -874,6 +878,7 @@
A8A141E1F2137943476D0C82 /* CotabbyTests */ = {
isa = PBXGroup;
children = (
A1C0FFEEA1C0FFEEA1C0F003 /* AccessibilityCaptureSuppressionPolicyTests.swift */,
A168A7B6A7AD11559B60C56B /* ApplicationBundleMetadataTests.swift */,
78AFA4586C82E92D7FBF381B /* ArithmeticEvaluatorTests.swift */,
C046CB4F3CB4BFE9391DB5DE /* AXTextGeometryResolverTests.swift */,
Expand Down Expand Up @@ -1059,6 +1064,7 @@
isa = PBXGroup;
children = (
67C78D77B58388B15AC8B954 /* Macros */,
A1C0FFEEA1C0FFEEA1C0F001 /* AccessibilityCaptureSuppressionPolicy.swift */,
352AF5B2834FEE1F597394E4 /* ApplicationBundleMetadata.swift */,
AC70775535A3428991025AB8 /* AXHelper.swift */,
85EF79E6144D6C6AD062B569 /* BaseCompletionPromptRenderer.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
5 changes: 5 additions & 0 deletions Cotabby/App/Core/CotabbyAppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
30 changes: 30 additions & 0 deletions Cotabby/Support/AccessibilityCaptureSuppressionPolicy.swift
Original file line number Diff line number Diff line change
@@ -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"
]
Comment on lines +17 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hard-coded suppression has no user opt-out

unsafeBundleIdentifiers is a private constant, so there is no way for a user to re-enable AX capture for Calendar (e.g., when they are typing in a plain text area outside the event-editor popover). The policy fires before all user-preference checks in isCaptureSuppressedForBundle, meaning even an explicit per-app user preference cannot override it. The doc comment acknowledges this is "conservative," but unlike the isApplicationDisabled path, no corresponding UI toggle exists. Consider surfacing this as a per-app override in the accessibility settings pane, or at minimum document the intentional no-override behaviour in a follow-up issue so the decision is tracked.

Fix in Codex Fix in Claude Code


/// 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)
}
}
26 changes: 26 additions & 0 deletions CotabbyTests/AccessibilityCaptureSuppressionPolicyTests.swift
Original file line number Diff line number Diff line change
@@ -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)
)
}
}