Document · doc-0002

Wave operating model

This document carries only what is specific to GrotTrack. The campaign model itself — run contract, run modes, routing, authority, child lane briefs, contract freezing, the structural failure patterns, the unattended blocker contract, the goal-file template, the pre-flight checklist — lives in the Agent fan-out protocol (canonical) doc and is not repeated here. Read both before designing a wave. If a section below could be pasted into another project unchanged, it is in the wrong document.

Written 2026-08-14 at migration time, from the codebase and git history. Sections marked (unevidenced) are conventions set at migration rather than lessons already paid for; treat them as provisional and correct them the first time a run contradicts them.

The exclusive resource: this app can only be exercised on one Mac, serially

GrotTrack is a menu-bar app that reads the Accessibility API, captures the screen through ScreenCaptureKit, and talks to Chrome over native messaging. Every one of those is a machine-global, singleton resource. There is no simulator, no headless mode, and no second instance.

Therefore: at most one lane may run the app at a time, and running it is a main-thread action, not a lane action. Lanes write code, run swiftlint, run xcodebuild build and xcodebuild test — all of which are safe in parallel because they touch only DerivedData and the repo. Anything that launches GrotTrack, grants a permission, or exercises the Chrome bridge end-to-end is serialised through the root, on the one Mac, with Rob able to answer a prompt. A lane that needs this stops and returns the request; it does not try to automate a TCC dialog.

GrotTrack.xcodeproj is generated and untracked — project.yml is the integration file

.gitignore excludes *.xcodeproj. The project file does not exist in a fresh clone and is regenerated by xcodegen generate. Two consequences that decide lane shape:

Recurring defect #1: SwiftLint violations land, then get their own follow-up commit

The single most repeated failure in this repo’s history. At least eight commits exist whose entire content is fixing lint the previous commit introduced — b5ee874, bbd1af5, 71efe35, 0d61e90, cd7b58b, cf19601, c58eead, and b95101b (which bundles it with a CI upgrade). The cause is structural, not carelessness: swiftlint lint is a separate command from xcodebuild build, so a lane that builds and tests cleanly still ships violations, and .github/workflows/build.yml:45 runs Lint before Build — so the whole gate goes red on formatting while the code is fine.

.swiftlint.yml opts in to four rules beyond the defaults that catch people out — force_unwrapping, implicitly_unwrapped_optional, empty_count, empty_string — and sets line_length to warn at 150 / error at 200, file_length to warn at 500 / error at 1000. Test code is included, not excluded, so a table-driven test with a long literal row trips line_length like anything else.

Rule: swiftlint lint is part of a lane’s own definition of done, run before it reports, not left to the wiring pass. Aggregating eight lanes’ violations into one cleanup commit is how this repo got eight cleanup commits.

Recurring defect #2: Swift 6 strict concurrency breaks at Apple framework boundaries

SWIFT_STRICT_CONCURRENCY = complete. The failures are not in this code’s own actors — they are where this code touches a framework Apple has not fully annotated. 85d58a0 is the canonical instance: a CI build failure fixed by @preconcurrency import ScreenCaptureKit. ff215b5 is another pass of concurrency-safety fixes found in review rather than by the compiler.

Two properties make this expensive in a fan-out: the failure often appears only in a different build configuration than the one the lane ran, and the fix (@preconcurrency, nonisolated, an @unchecked Sendable wrapper) is a judgement about a boundary, not a mechanical edit.

Rule: a lane that adds a new Apple-framework dependency — a new import of a ScreenCaptureKit, AXUIElement, AppKit or SwiftData surface not already used in the file — freezes its isolation decision with the root before writing against it. Getting this wrong silently pushes work onto the main actor, which in a background tracker is a performance defect nobody notices until the app hitches.

Recurring defect #3: macOS actively fights a background menu-bar tracker

Three separate shipped bugs, all the same shape — the OS suspending or killing an app that has no window and no Dock icon:

None of these are reachable by xcodebuild test. They need the built app, running, ignored for minutes. Rule: any change to app lifecycle, MenuBarExtra wiring, AppCoordinator startup, or activity/screenshot scheduling carries a manual soak as its acceptance check — build, run, leave it alone for the relevant interval, then confirm data actually landed. A lane cannot do this (see the exclusive resource above); it states the soak it needs and the root runs it.

Recurring defect #4: timeline coordinate and index arithmetic

ef73c2c (index space mismatch), 98bf3f4 (scroll and zoom bugs), ff215b5 (zoom math), 8a1f1aa and ef513b1 (two rewrites of the scroll/selection model). The timeline maps three different spaces onto each other — wall-clock time, scroll offset in points, and array indices into filtered event lists — and the bugs are all in the conversions.

Rule: this is the one area of the codebase where test-first is not negotiable. The arithmetic has edge cases nobody holds in their head (empty ranges, a single event, zoom at the extremes, a filtered list whose indices no longer match the unfiltered one), and the failures are visual rather than crashing, so they survive review. GrotTrackTests/ already carries the pattern — see TimeBlockAggregatorTests, SessionDetectorTests.

The release pipeline is unverifiable locally, and has failed five separate ways

0d09fbd (re-sign Sparkle framework binaries for notarization), 6a5b1c7 (hardened runtime missing on non-primary targets), 5432bd0 (CFBundleVersion used the wrong version so Sparkle never saw updates), 6ba449e (added CI signing validation because there was none), becf5ed and 0fabfa8 (pipeline and stale-Xcode-selection fixes).

The common property: none of it can be tested without a signing identity and a real notarization round-trip, which lanes do not have. A change to .github/workflows/release.yml, to signing, entitlements, hardened runtime, Sparkle, or the version fields is therefore root-only work, verified by an actual release, and it is legitimate for such a task to sit Parked awaiting the next one.

Version lives in three files kept in sync by release-please (release-please-config.json extra-files): grot-track-extension/package.json $.version, grot-track-extension/wxt.config.ts (the x-release-please-version marker comment), and project.yml. Never hand-bump any of them.

Lane conventions (unevidenced — set at migration, correct on first contradiction)

Run-end against this tracker

Task state is the record. Landed work is Done with the SHA in its final summary; work stopped against the exclusive resource, a signing identity or a Chrome Web Store review is Parked with the concrete resume boundary (what was true when it stopped, and what has to become true to continue) — “waiting on Rob” alone is not a boundary. Discovered work is a new task labelled needs-triage.

The closing terminal message carries only what no single task captures. Nothing durable may live only there.

View the source file on GitHub