Imported from kargnas/mac-optimizing-looper (
AGENTS.md). Install upstream withnpx skills add kargnas/mac-optimizing-looper. Copyright stays with the author.
AGENTS.md
Guidance for AI agents working on Mac Optimizing Looper — a macOS menu-bar app that periodically analyzes system load with Claude and surfaces prioritized advice.
Build / Run
- Package manager: SwiftPM.
swift build,swift test(run both before finishing). - Always launch via the app bundle, not the bare binary:
This buildsbash script/build_and_run.sh rundist/MacOptimizingLooper.app, codesigns it ad-hoc, andopen -ns it. The bundle id isas.kargn.MacOptimizingLooper; it isLSUIElement(no Dock icon). - Why the bundle matters:
UNUserNotificationCenterneeds a real bundle proxy. A bare.build/.../MacOptimizingLooperbinary has no bundle id, so notifications silently fail there. Code guards onBundle.main.bundleIdentifier != niland falls back to opening the result window directly — but for real testing, run the bundle. - Config lives at
~/.config/mac-optimizing-looper/config.jsonand is read once at launch (AppConfig.loadDefault()). After editing config by hand, restart the app. provider(config, defaultclaude) selects the LLM backend (claude|codex); an absent/unknown value resolves toclaudeso old configs keep working. See LLM providers below.claudeCommand/codexCommand(defaultsclaude/codex) are argv prefixes for every invocation of that provider. Settings accepts wrappers such asag claude agpandag codex agp; the app appends its native CLI arguments. Commands are parsed as executable + fixed arguments, not evaluated by a shell. The oldCLAUDE_CLI_PATH/CODEX_CLI_PATHoverrides are intentionally unsupported.thinkingLevel(config) = the reasoning level for the analysis pass, provider-relative (claude--effort, codexmodel_reasoning_effort; low/medium/high/xhigh/max, defaultmax; invalid values clamp tomax). The claude JSON formatter and the command risk-check stay atlowby design (mechanical / fast gate).fastMode(config, default false) requests the provider's faster service tier when the selected model supports it (codex priority tier). No-op for claude (the CLI has no fast flag).monitorSeconds(config, default 30, clamped 0–600) = how long the mac-optimizer sustained monitor samples before evaluating.MacOptimizerScript.runIfAvailableruns the one-shot snapshot AND, whenmonitorSeconds > 0, a second--monitor Npass (separate script mode) appended to the report.0disables the monitor pass. The scan script (mac-optimize.sh) is bundled intoContents/Resources/by both build scripts from the tracked source at.agents/skills/mac-optimizer/.MacOptimizerScriptresolves it fromBundle.mainonly (plus a repo-CWD dev fallback and theMAC_OPTIMIZER_SCRIPToverride) and never from$HOME— so binary-install and Homebrew-cask users are self-contained, not dependent on a separately-installed skill.
Release pipeline
script/build-app.zsh packages a distributable dist/MacOptimizingLooper.app (release
build + version-stamped Info.plist). Local default is ad-hoc sign; CI sets
CODE_SIGN_IDENTITY="Developer ID Application" + HARDENED_RUNTIME=1 for a
notarizable bundle. Keep it separate from build_and_run.sh (that is the fast
debug dev loop). .github/workflows/auto-release.yml owns the whole release: every
push to main bumps the patch version, then builds, signs, notarizes, creates the
DMG and GitHub Release, and updates the kargnas/homebrew-tap cask. Manual runs can
choose a minor or major bump. The pipeline is inert until signing secrets exist
— see docs/release-setup.md.
Sparkle in-app auto-update is wired (release builds set SPARKLE_AUTO=1; the feed is
the latest release's appcast.xml). The EdDSA key is one-time — never regenerate it.
Terminal Launching — single entry point
All terminal-opening features (Show Command in Terminal, Claude review) go through
TerminalLauncher (Sources/MacOptimizingLooper/TerminalLauncher.swift). Do not open
terminals directly from AppDelegate or anywhere else — extend TerminalLauncher.
- Terminal resolution:
TerminalAppCatalog.application(bundleIdentifier:). - The configured terminal (
config.terminalAppBundleIdentifier) is honored exactly. When a specific terminal is configured but cannot be matched, we resolve it directly viaNSWorkspace, and if it is genuinely not installed we returnnilso the caller shows an error. Never silently substitute a different terminal (e.g. Apple Terminal) — that hid real misconfiguration and opened the wrong app. - Per-terminal launch quirks live in
TerminalApplication.LaunchMode(appleTerminal/iTermvia AppleScript,openWithArgumentsfor Ghostty,openCommandFilefor the rest). Unknown-but-installed terminals useopenCommandFile. - Scripts are written to a
.commandfile and the path only is injected into AppleScript — never the multi-line/UTF-8 body (Terminal turns embedded newlines into Return presses, corruptingif/elif/fiand CJK text).
Claude CLI invocation — headless vs interactive
claude starts an interactive session by default; -p/--print is non-interactive.
There is no claude run subcommand.
- Headless
-pfor anything the app parses programmatically:- Advice generation —
ClaudeCLIClient(-p --output-format text, JSON parsed downstream). - Command risk check —
CommandRiskAssessor(-p, parsesRISK: SAFE|DANGEROUS).
- Advice generation —
- Interactive
claude "<prompt>"for user-facing terminals:- "Review with Claude" —
TerminalScriptBuilder.claudeReviewScriptopens an interactive session seeded with the prompt via"$(cat <promptfile>)"so the user can read the assessment and keep chatting. Do not regress this back to-p. The review prompt (claudeReviewPrompt) is a proactive performance assistant: it assesses the command, then offers to inspect and clean up the system with the user's confirmation — not a narrow command-only reviewer.
- "Review with Claude" —
LLM providers — abstraction & adding one
The app drives an LLM CLI, not an API. Backends sit behind LLMProviderKind
(claude|codex) and ProviderRegistry (makeClient / catalog). Every in-app LLM
call (analysis, risk-check, terminal review) routes through the selected provider; the
default is claude. Design doc: docs/superpowers/specs/2026-06-20-multi-provider-llm-design.md.
-
Command prefixes route through
CLICommandandCLIProcessRunner. The same saved command covers analysis, risk-check, Claude's JSON formatter, and interactive terminal review. Runs have no artificial timeout:Process.waitUntilExit()observes the real CLI exit, then checks normal exit vs signal termination, exit status, and required output. Wrapper commands shouldexectheir final CLI so termination propagates exactly;agdoes this. -
Bundled formatter/guide scripts use
/bin/bashexplicitly. Do not switch the guide back to/usr/bin/env bash: GUI/test environments can lack locale variables, causing Homebrew Bash to hang while emitting the guide's Unicode heredoc. -
Capabilities live on
LLMProviderKind.supportsStructuredOutputdecides the advice pipeline: codex returns schema-constrained JSON via--output-schemain one pass (PromptBuilder.adviceJSONSchema+responseFormatGuide); claude returns free-form text that the two-passformat-json.shturns into JSON. KeepPromptBuilder.responseFormatGuideandscript/mac-optimizing-looper-response-guide.shin sync — they encode the same rules for the two paths. -
Catalog is dynamic:
CodexModelCatalogparses~/.codex/models_cache.json($CODEX_HOMEhonored);ClaudeModelCatalogis curated (the claude CLI has no list command). Missing data → empty catalog → settings offers free-text "Custom…". Never hardcode codex model names. -
codex invocation (
CodexCLIClient):codex exec -m <model> -c model_reasoning_effort="<effort>" [-c service_tier="priority"] --skip-git-repo-check -s read-only [--output-schema <file>] -o <file> "<system+user>" </dev/null. codex has no--systemflag (prompts are concatenated) and ignorestemperature/maxTokens. stdin MUST be/dev/nullor codex blocks. Theservice_tierconfig key was verified with--strict-config; do not guess codex config keys. -
Adding a provider: new
LLMClient+ProviderCataloging, oneLLMProviderKindcase with its capability flags, and aProviderRegistrybranch. Settings cascades Provider → Model → Effort → Fast Mode automatically from the catalog.
Command execution & safety model
ActionPolicy.current == .userInitiatedWithSafeguards. Advice is inert data; the model
can never make the app run anything. The single execution path is the explicit
"Run Command Now" menu action, gated by:
CommandRiskAssessor(claude -p) classifies the command;unknownis treated as dangerous (fail safe).- Anything not clearly
safe→ confirmation dialog (default button = Cancel). CommandExecutor.runexecutes in the background. Commands containing asudotoken are routed through a GUI admin-password prompt (osascript ... with administrator privileges) because a background process has no TTY.- Result → macOS notification (✅/❌ + exit code); tapping it opens the full output window. If notifications are unavailable, the window opens directly (never lose output).
Suggestion carries only data (suggestedCommand: String?), no executable closures —
enforced by GuardrailTests.
House rules (project-specific)
- No silent fallbacks / no silent failure. Surface errors; never substitute behavior the user didn't choose without telling them.
- i18n: UI chrome is fully localized via
<locale>.lproj/Localizable.stringsunderSources/MacOptimizingLooperCore/Resources(10 languages: en, ko, zh-Hans, zh-Hant, ja, es, de, fr, pt-BR, ru).enis the source of truth; every key MUST exist in every locale (LocalizationTestsenforces parity).AppStrings(languageIdentifier:)is the ONLY access point — it loads the forced locale's.lprojsub-bundle viaLocalizationBundle(region/script collapse + English fallback). Never hardcode UI text or branch onisKorean. Add a key toen.lprojfirst, then every other locale (same%@count/order). The Settings "Language" popup is driven byAppConfig.supportedUILanguagesand writesoutputLanguageIdentifier, which drives BOTH the UI language and the analysis output language. To add a locale: new<id>.lproj, asupportedUILanguagesentry, done. - Tests:
Tests/MacOptimizingLooperCoreTests. KeepGuardrailTestsgreen — it encodes the safety contract. Update it deliberately when the contract intentionally changes. - Bundle id prefix for any new bundles: keep
as.kargn.*consistent with the existing app bundle.
Project structure
Sources/
MacOptimizingLooper/ # App layer (menu bar, NSApp, terminal launch)
main.swift # entry point — AppDelegate lifecycle
AppDelegate.swift # menu, loops, notifications, command actions
AdvisorLoop.swift # periodic analysis tick
SettingsWindowController.swift
TerminalLauncher.swift # single entry point for opening terminals
TerminalAppCatalog.swift
CommandResultWindowController.swift
MacOptimizingLooperCore/ # Headless library (fully unit-tested)
Models.swift # LoadMetrics, Suggestion, AdviceResponse
Config.swift # AppConfig + JSON load
MetricsCollector.swift # CPU/GPU/memory thread metrics
LoadAnalyzer.swift # bottleneck classification
AdviceProvider.swift # routes analysis → LLMClient
PromptBuilder.swift # advice prompt + JSON schema
LLMClient.swift / LLMProvider.swift # provider abstraction
CLICommand.swift # custom argv prefix parsing + real process termination
ClaudeCLIClient.swift / ClaudeModelCatalog.swift
CodexCLIClient.swift / CodexModelCatalog.swift
CommandExecutor.swift # the ONE execution path (background)
CommandRiskAssessor.swift # claude -p risk gate
AdviceFormatter.swift
ResponseFormatterProvider.swift
TerminalScriptBuilder.swift
MacOptimizerScript.swift # runs bundled mac-optimize.sh
AppStrings.swift # i18n entry (forced-locale sub-bundle)
Resources/<locale>.lproj/ # 10-language Localizable.strings
Tests/MacOptimizingLooperCoreTests/ # 13 files incl. GuardrailTests
script/
build_and_run.sh # fast dev loop: build → bundle → ad-hoc sign → open -n
build-app.zsh # release packager (version stamp, Sparkle embed)
mac-optimizing-looper-response-guide.sh
mac-optimizing-looper-format-json.sh
.agents/skills/mac-optimizer/mac-optimize.sh # bundled scan script source
docs/ # screenshots + release-setup + design specs
VS Code / Cursor workflow
.vscode/ holds the one-click dev loop. Install recommended extensions first
(extensions.json — sswg swift-lang, vadimcn.lldb, zhouronghua.swift-format,
actboy168.tasks for the status-bar task button).
- Tasks (
tasks.json, no daemons):🚀 Build & Run Bundle— default build task;script/build_and_run.sh run(debug build, bundle, ad-hoc sign,open -n). Prefer over the bare executable so notifications + Sparkle are live.🧪 Run Tests—swift test.📦 Build Release Bundle—script/build-app.zsh(release + Sparkle embed).📋 Stream Logs—log streamfiltered to the app process.🗂️ Open Config Folder—~/.config/mac-optimizing-looper/.
- Launch (
launch.json):🐞 Debug MacOptimizingLooper— swift-lang ext, debug config.Debug MacOptimizingLooper (Release)— release config.🐞 Debug App Bundle (LLDB)— launches the packaged bundle under LLDB so breakpoints hit with the real bundle id live (notifications + Sparkle).🚀 Full Dev Session— compound; the 2 most-used runs may carry an emoji.
- Settings (
settings.json): swift-format on save (100-col ruler), excludes.build/dist/.codegraph, enables the tasks status bar (actboy168.tasks).