agentleFS
r0shir0shi/harborclerk/macos/AGENTS.md

harborclerk / macos

AGENTS.md3 starsChanged 31 days agoMITView on GitHub

Two Swift apps: Harbor Clerk Server (menubar agent managing every backend service) and Harbor Clerk (WKWebView shell around the SPA). Build scripts in scripts/, orchestrated by Makefile. - Never call proc.waitUntilExit() on the MainActor, nor unbounded anywhere. It stalls every service behind it, has parked forever on an exited child, and in a test tearDown escapes every XCTest timeout (#890). Use await withCheckedContinuation (as MigrationRunner does); in tests, killAndReap. - Nil the readabilityHandler on EOF. Log.createPipe() installs a readability handler;…

  • Installs packages
  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository9 other files it gives its agents.

The file

We split this AGENTS.md into the rules it gives an agent

1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When bumping, re-test on the target OS: the WKWebView loopback load, watched- folder access from the background watcher, and every spawned …

    line 86

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md — macos

Two Swift apps: Harbor Clerk Server (menubar agent managing every backend service) and Harbor Clerk (WKWebView shell around the SPA). Build scripts in scripts/, orchestrated by Makefile.

Process management — the rules that cost real outages

  • Never call proc.waitUntilExit() on the MainActor, nor unbounded anywhere. It stalls every service behind it, has parked forever on an exited child, and in a test tearDown escapes every XCTest timeout (#890). Use await withCheckedContinuation (as MigrationRunner does); in tests, killAndReap.
  • Nil the readabilityHandler on EOF. Log.createPipe() installs a readability handler; if it is never cleared, waitUntilExit() deadlocks because the pipe's read end never closes. Set handle.readabilityHandler = nil inside the guard !data.isEmpty else branch. This presented as Alembic finishing while the runner hung forever; sample <pid> showed the thread parked in mach_msg2_trap.
  • @MainActor async functions still interleave. They can suspend at any await, so state read before an await may be stale after it. Guard every state transition against concurrent mutation — startService() must re-check .shutdownPending rather than trusting an earlier check.
  • Kill process groups, not PIDs. Tika forks JVMs and workers spawn pdftoppm, tesseract, magick. kill(pid, SIGKILL) orphans the grandchildren to launchd, where they survive relaunch. Use killpg.

ServiceState has seven states: stopped, startupPending, starting, running, shutdownPending, stopping, errored. Pending states render with blue dots.

Postgres and Tika may be launchd-managed rather than direct subprocesses, so they survive a menubar crash.

Environment for subprocesses

ServiceManager builds one shared environment dict handed to every subprocess, which is why macOS gets variables like LLAMA_SERVER_URL for free. Docker Compose has no equivalent — each service opts in individually, and forgetting one there caused summarize to silently produce extractive summaries. When adding a variable a worker needs, check whether the Compose side needs it too. Nothing a launched process runs may write into the bundle: it breaks the code seal (numba did, #846). Every launch path ends with applyingRuntimeCacheRules in ServiceManager, whose comments hold the cache rules; never bypass it.

Networking

The WKWebView app loads the SPA over plain HTTP on loopback at api_port (default 8100). The HTTPS gateway is a separate Caddy surface on 8443 for local MCP clients requiring TLS, with internal or operator-supplied certificates and a loopback-versus-LAN exposure check. These are different surfaces; do not conflate them.

Port 80 is frequently occupied on macOS (AirPlay Receiver and friends) and is not needed for local development.

The client Info.plist declares no ATS keys, so the loopback cleartext load depends on WebKit's implicit localhost allowance rather than a declared exception. That is a known gap, not a decision — see #561.

Do not bump the SDK without re-testing

Many of macOS's stricter privacy and security defaults are gated "linked-on-or-after" the SDK a binary was built against. Rebuilding against a newer SDK is what flips the stricter rules on — ATS, TCC/file access, hardened-runtime and JIT policy can all change behavior with no source change at all. So an SDK bump is a behavioral change, not housekeeping.

The bump has already happened, and nothing recorded it: the deployment target is still 15.0, but the SDK follows whatever Xcode is on the build machine: Xcode 26 moved it, then the release machine's Xcode 27.1 (SDK 27.0): vtool -show-build on both apps' executables, built 2026-10-17, gives minos 15.0, sdk 27.0. The stricter rules are already live in every build: SDK 26.5 builds passed a full field test (11,436 documents, IMAP sync, search, Research), 27.0 builds have not had one yet. The #561 ATS gap runs under the stricter regime today.

Two consequences:

  • The risk is inverted. Building with an older Xcode now loosens the shipped binary. The CI macos job runs on macos-15, whose SDK is older than the release machine's, so CI validates a more permissive configuration than ships and cannot catch a regression only the stricter SDK triggers.
  • Nobody decided to bump; a routine Xcode upgrade did. vtool -show-build on the built binary is the check; pinning Xcode in CI and at release time is the fix, and is not yet done.

When bumping, re-test on the target OS: the WKWebView loopback load, watched- folder access from the background watcher, and every spawned helper (Postgres, the Tika JVM, llama-server, bundled Python). See the macOS 27 readiness assessment in docs/superpowers/plans/ and epic #563.

Entitlements: restricted keys kill a Developer ID app silently

keychain-access-groups (and anything under com.apple.developer.*) is a restricted entitlement. A Developer ID build that declares one without an embedded provisioning profile is SIGKILLed by the kernel at exec, before main(). Nothing in the pipeline notices: codesign --verify --deep --strict passes, spctl --assess says accepted, and the notary service returns Accepted — none of them run the binary. This shipped as a stapled DMG whose app exited 137 with no stderr. notarize.sh now exec-probes both apps before submitting.

Probe a copy, never the artifact. Launching a notarized bundle in place puts it under App Management protection: from then on nothing — not even the Terminal that signed it — may write inside it, and the next make sign on that output dies re-signing tika-server.jar with "Operation not permitted". The fix is to mv the bundle aside (a rename on the parent is allowed) and regenerate it with package.sh. notarize.sh probes a ditto copy for exactly this reason.

Dev builds hid it for months: Xcode's automatic signing embeds a development profile that authorises the key. The release path never had one.

Also: on the file-based login keychain (the default without kSecUseDataProtectionKeychain) kSecAttrAccessGroup is ignored on read and write — measured — so the group never delivered the cross-rebuild ACL anchoring it was added for. Stable Developer ID signing is what does that.

No native dialogs in the web view

window.confirm and window.alert silently return false in WKWebView. Swift retains only two JS bridges — pickFolder (NSOpenPanel) and revealInFinder (NSWorkspace). Folder management UI lives at /folders in the web app; resist adding bridges for things the web app can do.

Naming

Bundle IDs, keychain service IDs, access-group suffixes, and Logger subsystems all use com.harborclerk.* — never com.bitblot.*.

Worker presets (C = logical cores)

Presetiocpullm
Quiet111
Balancedmax(2, C//4)21
Fastmax(2, C//2)31

Hard caps: io ≤ 8, cpu ≤ 4. The llm queue runs a single worker in every preset. These are defined in ServiceManager.workerCounts.

Builds

build-venv.sh installs exactly what the root uv.lock pins (exported with hashes), then the two local packages with --no-deps --upgrade, and rebuilds the venv when the lock changes. Never add a bare pip install <name>: the app once shipped torch two minors behind the lock, and it cost 43 GB of GPU memory (#685).

Every version string comes from the release tag. package.sh derives them from git describe (scripts/build-version.sh): tag before you build, and never bump the pbxproj's 0.0.0-dev sentinel (a test fails if you do). The procedure and what each stamp means: docs/releasing.md.

The llama.cpp pin lives in two places

scripts/build-llama.sh (LLAMA_CPP_TAG) and the image in docker-compose.yml; tests/test_llama_cpp_pin.py holds them together and to a stable release. Bump both in one PR, on its own, and re-baseline every model after it. The build replaces a clone an older pin left behind, fails unless the binary's --version names the pinned commit, and fails on any link outside the bundle and the OS (Homebrew's OpenSSL shipped that way for months: both build machines have it).

A model server does not run out of memory. The Mac does.

Unified memory lets llama-server allocate more than the machine has; the host swaps until the kernel watchdog panics. The mini (32 GB) did so on 2026-09-17 when a hand-launched smoke test passed the YaRN-extended context (-c 131072 -np 2: ~19 GB of KV cache for Qwen3-8B at ~144 KB per token) beside the app's own server holding gpt-oss-20b. Before launching any llama-server by hand, add the model file to the KV cache, subtract what is already resident (the app's server, anything else on the host that loads models), and stay well inside what is free. YaRN is off by default; use the app's defaults.

cc can pick up the Command Line Tools SDK, not Xcode's

make apps compiles PostgreSQL and pgvector from source. Where a macOS beta updated the Command Line Tools, the bare cc that configure runs resolves the CLT SDK (/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk) even though xcode-select -p points at Xcode.app. When that SDK is newer than Xcode's linker (CLT 27.0 against Xcode 26.5, pre-Xcode 27) every link fails with

ld: tapi error: malformed file
.../MacOSX27.0.sdk/usr/lib/libSystem.B.tbd:4:20: error: unknown architecture

and the build stops at configure: error: C compiler cannot create executables. Nothing in the repo changed; the OS update did it. Pin the SDK to Xcode's:

SDKROOT=$(xcrun --sdk macosx --show-sdk-path) make apps

xcrun --show-sdk-path without --sdk macosx returns the CLT path there, so use the explicit form. make sign compiles nothing and needs no override.

make sign over SSH: errSecInternalComponent

codesign finds the Developer ID identity and then fails on the first thing it signs (a dylib inside tika-server.jar), because an SSH session has the login keychain locked and cannot show the key-access prompt. A reboot clears any earlier unlock. It is not the jar: the owner runs security unlock-keychain ~/Library/Keychains/login.keychain-db in that session, then make sign. If the bundle stamp notarize.sh prints differs from the checkout's commit, rebuild (#687).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.