🚧 Early alpha Under active construction. Everything here may change without notice — except the appcast URL. That one's forever.

Aetower

Docs

--- type: how-to status: active review_cycle: 180 last_reviewed: 2026-05-28 ---

Pre-commit and Pre-push Quality Gates

Aetower uses Git's core.hooksPath as the hook entry point. The hooks are thin shell wrappers under .githooks/; the real quality system is the unified local runner at scripts/ci-local.sh.

The contract is:

Aetower does not use GitHub Actions for CI. The source of truth is the local hook-driven runner, so the same gates run before code leaves the machine.

Installation


sh scripts/install-hooks.sh

This runs:


git config core.hooksPath .githooks
chmod +x .githooks/pre-commit .githooks/pre-push scripts/ci-local.sh scripts/install-hooks.sh scripts/smoke-package.sh

Entry Points

ActionCommand pathRunner mode
Commit.githooks/pre-commit -> sh scripts/ci-local.sh --mode pre-commitpre-commit
Push.githooks/pre-push -> sh scripts/ci-local.sh --mode pre-push --push-stdin-file <tmp>pre-push
Manual pre-commitsh scripts/ci-local.sh --mode pre-commitpre-commit
Manual pre-pushsh scripts/ci-local.sh --mode pre-pushpre-push
Full local batterysh scripts/ci-local.sh --mode fullfull

Compatibility aliases:

Scope Model

Pre-commit scope

pre-commit uses:


git diff --cached --name-only --diff-filter=ACMR

Only added, copied, modified, or renamed staged files are considered for changed-file gates. Generated bindings, local caches, dist/, SwiftPM build output, and Rust target output are excluded by a mix of .gitignore, path-specific gate filters, and quality-guard.

Pre-push scope

.githooks/pre-push captures the update lines Git provides on stdin and passes them to the runner through --push-stdin-file. The runner uses the pushed remote SHA as the commit-range base for commit-range checks such as gitleaks.

When push stdin is unavailable, the runner resolves a fallback base in this order:

  1. The current upstream branch.
  2. origin/main.
  3. main.
  4. HEAD~1.
  5. HEAD.

Pre-push intentionally runs the full local integration surface. Aetower is a native system monitor with Swift, Rust, FFI, packaging, Sparkle, local MCP, and runtime observability surfaces; small changes can break integration behavior outside the directly changed file set.

Dirty worktree policy

Before pre-push and full, the runner checks whether the working tree or index is dirty.

This prevents a push gate from validating a state that is not actually what is being pushed.

What Pre-commit Checks

Pre-commit is intentionally strict and staged-file focused. Warnings block the commit.

GateRuns whenWhat it checks
quality guardAlwaysDiff-level anti-slop and safety checks: secrets, placeholder markers, unsafe Swift patterns, hardcoded SwiftUI colors outside design tokens, production Rust panic shortcuts, risky dependency additions, suspicious dumping-ground filenames, and shell syntax for changed scripts/hooks.
gitleaks (staged)AlwaysStaged secret scan with redacted output.
swiftlint (<file>)Swift source files are stagedStrict SwiftLint on changed Swift files, excluding generated bindings.
semgrep (changed files)Swift/Rust/script/hook files are staged and Semgrep is healthyLocal Semgrep quality rules against changed files.
cargo fmt --checkRust files or Rust formatting/toolchain files are stagedWorkspace Rust formatting.
cargo clippy (<pkg>)A Rust crate is affectedTargeted crate clippy with warnings denied.
cargo test (<pkg>)A Rust crate is affectedTargeted crate tests.
cargo clippy (workspace)Rust manifests, lockfile, toolchain, or bridge scripts changeFull workspace clippy with warnings denied.
cargo test (workspace)High-impact Rust files changeFull Rust workspace tests excluding helper first.
cargo test (aetower-helper)High-impact Rust files changeHelper tests serialized because helper behavior can touch system-facing assumptions.
build Rust bridgeFFI, generated bindings, bridge code, or Rust build scripts changeRebuilds Rust FFI and Swift bindings.
swift buildSwift package files are stagedSwiftPM build against the macOS package.
benchmark smokeHot-path runtime crates changeShort enforced performance budget smoke.
shell hook checksScripts or hooks are stagedPlaceholder for shell-specific checks; currently covered by quality-guard syntax checks.

What Pre-push Checks

Pre-push is intentionally full-surface. It is the local equivalent of CI.

GateWhat it checks
quality guardFull-repo quality guard, including duplicate-block detection against changed files.
gitleaks (commits)Commit-range secret scan from push base to HEAD.
swiftlintFull Swift source lint, excluding generated bindings.
semgrepFull Semgrep scan across Swift UI/app/bridge code, Rust crates, and scripts when Semgrep is healthy.
cargo fmt --checkWorkspace Rust formatting.
cargo clippyWorkspace Rust clippy with warnings denied.
cargo test (workspace)Workspace Rust tests excluding helper.
cargo test (aetower-helper)Helper tests with one test thread.
build Rust bridgeRust bridge and generated Swift binding rebuild.
swift buildSwiftPM debug build.
benchmark budgetEnforced synthetic runtime budget.
telemetry smokeReal OTLP/HTTP loopback export verification.
package smokeRebuilds and verifies packaged dist/Aetower.app.
cargo denyRust dependency advisories, bans, and source policy.

What Full Adds

full runs everything in pre-push, then adds:

GateWhat it checks
local operator smokePackaged-app MCP/operator smoke against the bundled helper.
cargo auditAdvisory cross-check when cargo-audit is installed.

Techniques That Keep It Fast, Strong, and Predictable

Single runner

All gates live in scripts/ci-local.sh. New checks should be added there rather than hidden inside hook files. The hook files should stay thin and only select the correct runner mode.

Explicit scope decisions

Pre-commit is scoped to staged files. Pre-push is full-surface by design.

That trade-off is intentional: pre-commit should stay fast enough to run often, while pre-push should catch native integration failures that only appear after Rust, Swift, FFI, packaging, Sparkle, and MCP surfaces are assembled together.

Fail-fast execution

The runner executes gates sequentially and stops at the first failure. Later gates depend on earlier contracts; for example, package smoke should not run if the Rust bridge cannot be rebuilt.

Verbose command logging

Every executed gate prints:

Skipped scope gates print an explicit skip reason.

Summary table

Every run ends with a quality summary:

Forensic logs

pre-push and full tee output to:


logs/quality-<mode>-<timestamp>.log

pre-commit does not create logs to avoid flooding the repository during frequent commit cycles. The logs/ directory is ignored by Git.

Targeted artifact reuse

The package smoke gate uses a narrow local cache under:


.aetower-cache/quality/package-smoke/

The cache stores the packaged Aetower.app and Aetower.zip after a successful package smoke. A later run can restore them when the package key still matches. The key includes:

Even on a cache hit, scripts/smoke-package.sh still verifies the restored app with codesign --verify --deep --strict. The cache only skips rebuilding; it does not skip bundle validation.

The cache is disabled automatically for notarization/stapling runs because those should always produce fresh artifacts. To disable it manually:


AETOWER_QUALITY_DISABLE_CACHE=1 sh scripts/ci-local.sh --mode pre-push

The cache keeps the five most recent package entries by default. Override this when needed:


AETOWER_PACKAGE_SMOKE_CACHE_KEEP=2 sh scripts/ci-local.sh --mode pre-push

Runtime filters

The runner supports narrow reproductions:


sh scripts/ci-local.sh --mode pre-push --include="swift build"
sh scripts/ci-local.sh --mode pre-push --skip="package smoke"
sh scripts/ci-local.sh --mode pre-push --iterations 4

Filters match gate labels exactly and are for debugging. The default hook path should stay strict.

Rerun hints

When a gate fails, the summary includes the command that can be rerun directly. This makes hook failures reproducible without searching through shell code.

Packaging Smoke

Run package verification directly:


sh scripts/smoke-package.sh --rebuild

Run the quality-gate package smoke with cache support:


sh scripts/quality-package-smoke.sh

To also launch the packaged app briefly and verify the process comes up:


sh scripts/smoke-package.sh --rebuild --launch

Local Operator Smoke

To launch the packaged app if needed and verify the bundled MCP helper end to end:


sh scripts/local-operator-smoke.sh --rebuild

This validates:

Telemetry Smoke

To exercise OTLP export against a real local loopback collector:


sh scripts/telemetry-smoke.sh

This starts a temporary in-process HTTP receiver through aetower-bench, verifies that Aetower sends a real OTLP/HTTP payload to /v1/metrics, and fails if the exporter cannot deliver or the payload is malformed.

Runtime Profiling

For live packaged-app CPU and memory profiling outside the synthetic harness, see Runtime Profiling.

For a long-running enforced soak with telemetry smoke and optional release preflight:


sh scripts/soak-local.sh --rebuild --launch

Release Packaging

For Developer ID signing and optional notarization, see Distribution and Direct Download Release.

Reclaiming Local Disk

Cargo, SwiftPM, local quality caches, the packaged app, and profiling tmp dirs can accumulate several GB. Inspect or delete them with:


sh scripts/clean-local.sh          # dry-run, reports sizes
sh scripts/clean-local.sh --yes    # actually delete

The script targets tmp/, .aetower-cache/, dist/, rust/target/, and macos/.build/. It never touches user state under ~/Library/Application Support/Aetower/.

Tooling Expectations

The local gates assume these tools are installed:

Install them with:


brew install gitleaks swiftlint semgrep
cargo install cargo-deny --locked

cargo-deny is the blocking Rust dependency-policy gate. Its ignore list in deny.toml is intentionally short and documents the migration reason for each allowed advisory.

semgrep is wired into the hooks, but the script skips it explicitly if the local binary cannot start cleanly. That keeps the gate strict when the tool is healthy without making commits fail for unrelated local certificate/runtime breakage.

Operational Guidance

Use the default hooks for normal work:


git add .
git commit -m "feat: ..."
git push

Run gates manually when debugging:


sh scripts/ci-local.sh --mode pre-commit
sh scripts/ci-local.sh --mode pre-push
sh scripts/ci-local.sh --mode full

Bypass Git hooks only for emergencies:


git commit --no-verify -m "wip: emergency fix"
git push --no-verify

The expected policy is to fix the underlying quality failure and rerun the same gate, not to make --no-verify part of the normal workflow.

Recommendation

Keep the cache policy targeted. Package-smoke artifact reuse is currently the right boundary because the restored bundle is still verified and the cache key is package-specific. Avoid a broad, generic "skip any passing gate by hash" cache until the runner has a richer registry with per-gate input declarations.

The next efficiency work should focus on Swift build reuse and clearer split points between "fresh package from source" and "verify existing package":

  1. preserve SwiftPM build output when bridge/package inputs did not change
  2. keep full as the place for an explicit fresh-from-source package rebuild
  3. report cache hit/miss counts in the quality summary

That will reduce local push time without weakening the integration guarantees that matter for a native macOS system monitor.

Aetower is a free early-alpha download for macOS 14+ (Apple silicon).

Download for macOS