From 28cc1c48bd54e9b59d583fc46f03bc526b5366ec Mon Sep 17 00:00:00 2001 From: daymade Date: Wed, 12 Aug 2026 11:56:52 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(macos-watchdog):=20new=20skill=20v1.0.?= =?UTF-8?q?0=20=E2=80=94=20design/deploy/discipline=20launchd=20watchdogs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recovered onto its own branch/commit from a shared checkout: this content was found fully staged (index only, never committed) on a branch already named feat/macos-watchdog by a concurrent session, but sitting in the working tree while a different, unrelated branch (docs/hooks-pitfalls-30-31-...) was checked out — so it rode along into that other branch's commit as a parallel-session sweep-in. This commit reproduces it here, on the branch its own author had already named for it, byte-identical (verified via JSON diff on marketplace.json and file-mode comparison) to what was staged, with one exclusion: macos-watchdog/dist/macos-watchdog.skill, a gitignored (.gitignore line 9: `dist/`) local packaging artifact not meant to be committed, matching the convention of every other skill's dist/ in this repo (0 tracked files under any dist/ in the whole tree). Per the CHANGELOG entry already written for it: design, deploy, and discipline macOS launchd watchdogs (LaunchAgents/LaunchDaemons that detect a recurring problem and auto-remediate), distilled from 15 production watchdogs and their incident history. Ships the quiet-watchdog contract (premise-state self-check, patient mode, escalating auto-cooldown, never-resurrect-what-the-user-quit), deploy mechanics, stop semantics, batch-loop throttling, and SRE alert layering. Bundles watchdog-cooldown.sh, new-launchagent.sh, and an annotated plist template. Claimed eval: with-skill 17/17 vs baseline 16/17 on three prompts (new-install / noisy-watchdog triage / config audit). IMPORTANT — I (Claude, this session) did not author, verify, run, or review any of this content. I have not re-run security_scan, quick_validate, or the eval harness on it myself, and I have not independently confirmed the eval numbers or any technical claim in it. This commit exists purely to give already-complete, already-staged work its own clean home instead of leaving it fragile in a shared index or bundled into an unrelated PR. Do not treat this commit as reviewed. Whoever owns this work should review/re-verify before merging — see PR description for the open question. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CudYBXrK5WyCiKoB2ugbAx --- .claude-plugin/marketplace.json | 26 +++ CHANGELOG.md | 1 + macos-watchdog/.security-scan-passed | 4 + macos-watchdog/SKILL.md | 91 ++++++++ .../assets/launchagent.template.plist | 59 +++++ macos-watchdog/evals/evals.json | 46 ++++ macos-watchdog/references/alert-discipline.md | 38 ++++ .../references/launchd-plist-reference.md | 98 +++++++++ .../references/quiet-watchdog-patterns.md | 59 +++++ macos-watchdog/scripts/new-launchagent.sh | 124 +++++++++++ macos-watchdog/scripts/watchdog-cooldown.sh | 206 ++++++++++++++++++ 11 files changed, 752 insertions(+) create mode 100644 macos-watchdog/.security-scan-passed create mode 100644 macos-watchdog/SKILL.md create mode 100644 macos-watchdog/assets/launchagent.template.plist create mode 100644 macos-watchdog/evals/evals.json create mode 100644 macos-watchdog/references/alert-discipline.md create mode 100644 macos-watchdog/references/launchd-plist-reference.md create mode 100644 macos-watchdog/references/quiet-watchdog-patterns.md create mode 100755 macos-watchdog/scripts/new-launchagent.sh create mode 100644 macos-watchdog/scripts/watchdog-cooldown.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ab37d48..0639043 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1183,6 +1183,32 @@ "设计", "风格" ] + }, + { + "name": "macos-watchdog", + "description": "Design, deploy, and discipline macOS launchd watchdogs — LaunchAgents/LaunchDaemons that detect a recurring problem and auto-remediate it. Use whenever creating or editing a persistent background monitor / daemon / agent on macOS, writing a launchd plist, scheduling a self-healing script, or when a watchdog has become a disturbance itself: re-launching apps the user quit, firing repeated notifications, re-running its full repair ladder every few minutes on an unfixable network, or hammering the system (crash loops, fork storms, runaway restarts). Also use for stop/disable semantics (bootout vs bootstrap vs disable vs unload), adding cooldown / backoff / notification throttling to a self-healer, binding a monitor's lifecycle to its premise state, or auditing existing LaunchAgents. 中文触发:launchd 守护进程、常驻任务、开机自启、后台监控、定时自愈脚本。 Covers KeepAlive/ThrottleInterval/domains/logging, premise self-checks, auto-cooldown, alert layering, batch throttling.", + "source": "./macos-watchdog", + "strict": false, + "version": "1.0.0", + "category": "developer-tools", + "keywords": [ + "macos", + "launchd", + "launchagent", + "launchdaemon", + "daemon", + "watchdog", + "plist", + "bootout", + "bootstrap", + "monitoring", + "self-healing", + "cooldown", + "backoff", + "守护进程", + "常驻任务", + "后台监控" + ] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 65de030..6a9bb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **devils-advocate** (`daymade-financial` v1.1.0): new skill — structured devil's-advocate pressure-testing of an investment thesis against user-supplied evidence materials. A local reimplementation of LinqAlpha's hedge-fund "Devil's Advocate" agent, built from the production prompt template and JSON schema the vendor published on the AWS ML blog (2026-02), and extended with three layers that implementation lacks: a Mauboussin base-rate outside view (materials-bounded — no invented statistics, no side retrieval), a RAND Assumption-Based-Planning signpost list that turns the one-shot critique into a monitoring routine, and an explicit materials-bias/coverage declaration. Flow: decompose the thesis into explicit assertions and implicit assumptions (A1/A2 ids; fact/forecast/mechanism typing; load-bearing test; opposite-conclusion sub-claims must split), retrieve per-assumption counter-evidence under a source-credibility ladder with verbatim citations plus ACH's absent-evidence question, emit an auditable JSON object (`run_metadata`/`findings`/`deferred_assumptions`, `citations` array, `rebuttal` field, risk-flag rubric with an anti-inflation guard) and render a theme-grouped analyst narrative with references and a survived-assumptions list. Evidence-anchoring is load-bearing by design: role-played dissent underperforms authentic dissent (Nemeth 2001/2018), so free-form contrarianism is banned and every counterpoint must cite. Shipped after one fresh-context independent review (P0×1/P1×5/P2×14 — the P0 was a step-renumbering with four dangling cross-references; all fixed and re-verified), plus a full production test by a context-free agent on a real optical-module thesis with 5 research reports: 8 assumptions, 38 mechanically verified verbatim citations, schema-conformant output, and two counterarguments the authoring session's own parallel analysis had missed; the nine ambiguities that test surfaced (risk-flag inflation 6/8 High without a rubric, bare-array output with nowhere to put the coverage declaration, single-citation slot breaking multi-fact counterarguments, and six more) were folded back into the skill in the same session. - **frontend-visual-qa** (v1.12.0): new reference **`reference-parity-decomposition.md`** — the reference-parity profile was the only profile in the skill with no method file attached, and a real engagement proved the cost: a login-page rebuild against a public product's sign-in screen, with this skill loaded, took five user-caught correction rounds because every round fixed exactly the one delta the user's side-by-side screenshot pointed out and then declared parity. The new file makes the measured structural inventory of the reference the first deliverable (anchoring pinned-vs-centered, container vs full-bleed, aspect-ratio ownership, scale ladder, material chrome, column ratios, intra-region alignment, spacing rhythm — each with an operational diagnostic), defines match criteria (categorical relationships match exactly; scalars match at the project's token granularity), and encodes four traps that outlive the inventory: a user-caught delta falsifies the inventory rather than just the pixel; self-authored geometry assertions are Level D *for the parity claim* (a 22-assertion suite stayed green through three consecutive structural misreads — claim-type scoping stated against the host's evidence table, which keeps project E2E at Level B for geometry/regression claims); a vetoed effect ("never crop the image") indicts the structural premise that forces the effect, not the parameter that picks its flavor (`cover`→`contain` swaps cropping for letterboxing inside the same wrong fixed-size container); and user-supplied assets render faithfully by default — a silently chosen crop focal point is editing the user's material. SKILL.md wires the file into the audit-contract step with the lifecycle boundary stated (decomposition is the first act of the audit, applied to the reference, which always already exists; greenfield visual direction still routes to design skills), adds a conditional reference-parity inventory block to the report schema, and states the division of labor with `data_viz_tier_and_token_audit.md` for data-page tier parity. Marketplace description gains the "compare a rendered artifact with a visual reference" clause SKILL.md already carried. Verified by historical-task replay (each of the five failure rounds now has a specific sentence that names it before it happens) plus two fresh-context independent review rounds: round one returned 10 findings (4 substantive — no measurement method/artifact home for static-screenshot decomposition, no matched-verdict tolerance, an inaccurate host evidence-table citation, and a load-window conflict with the skill's after-implementation scope), all 10 fixed; round two verified the fixes. - **claude-code-hooks** (`daymade-claude-code` v1.43.0): new pitfalls **#30** and **#31**, both incidental discoveries from live work on a private hooks repo this session (not synthesized on request). **#30 — `UserPromptSubmit` fires on a task-notification's own arrival, not just on a human keystroke, and the stdin JSON has no field that says which**: a keyword-scanning hook fired the moment a background subagent's completion report landed, because the report's own text happened to match the trigger regex — no human had typed anything nearby. The transcript JSONL distinguishes the two internally (`origin.kind: "human"` vs `"task-notification"`), but that metadata never reaches the hook; the official stdin schema (verified against the live docs, not memory) is exactly `session_id`/`transcript_path`/`cwd`/`permission_mode`/`hook_event_name`/`prompt_id`/`prompt` — nothing marks provenance. SKILL.md's pre-existing "`UserPromptSubmit` only ever sees user input" claim gets a precise footnote rather than a rewrite: the core argument (it can't see the model's own current-turn output) still holds, it just isn't proof `.prompt` always originated from a keystroke. **#31 — a compounding-artifact staleness tracker keyed on file *kind* re-flags files nobody touched, and a written justification can't clear it, because nothing reads prose**: the tracker's `kinds` array accumulates across a whole session-scoped "turn," so re-editing *any* file of an already-flagged kind re-triggers the whole group regardless of a per-file justification already written and committed — the escape hatch its own message describes is real for a human reader, but the mechanism doesn't parse markdown to check whether it was used correctly. An independent fresh-context review — dispatched to *re-derive*, not just read and trust, the three evidentiary claims (the docs schema via its own WebFetch, the transcript shape via its own direct JSONL parse, the tracker's ledger via its own file read) — found every specific factual claim accurate, but caught two real bugs in #30's *prescribed* Fix before merge: the gate condition `origin.kind == "human" and promptSource == "typed"` silently rejects genuine human input arriving mid-turn (`promptSource: "queued"` — confirmed against a real several-sentence human message in this session's own transcript, independently re-verified before applying the fix), corrected to gate on `origin.kind` alone; and the fix told readers to look up `prompt_id` in the transcript JSONL, a string that occurs there 0 times across 1745 records — the field is `promptId`, camelCase, while the hook's own stdin JSON carries snake_case `prompt_id`, the same twin-blind-spot shape pitfall #20 already warns about on a different field pair. +- **macos-watchdog** (v1.0.0): new skill — design, deploy, and discipline macOS launchd watchdogs (LaunchAgents/LaunchDaemons that detect a recurring problem and auto-remediate). Distilled from 15 production watchdogs running on the author's machine and their incident history — the recurring failure mode was never "how to install a plist" but the watchdog itself becoming the disturbance. Ships the **quiet-watchdog contract** (four clauses, each from a real incident): premise-state self-check (a monitor's lifecycle binds to its premise — a recovery watcher kept notifying "still broken" for 2h after the system healed); patient mode (defer disruption, not detection — one measured chain self-recovered in ≤3 min, so force-reconnect on blips was net-harmful); escalating auto-cooldown (a failed repair ladder on an unfixable network re-ran every 5 min forever — ThrottleInterval can't fix this, it throttles process respawn only and has no backoff); and never-resurrect-what-the-user-quit (`open ` launches apps, `open` without `-g` steals foreground — the watchdog read as "I quit it and it came back"). Also covers deploy mechanics that bite (gui vs system domain, StandardOut/ErrorPath, TCC/FDA on the actual interpreter), stop semantics (`unload` is deprecated and gets resurrected by `RunAtLoad` — bootout/bootstrap/disable only), batch-loop throttling by default (an unthrottled replay forked 1,041 procs/sec and pushed the die to 83 °C), and SRE alert layering (page vs ticket, fatigue numbers). Bundles two reusable scripts (`watchdog-cooldown.sh` — source-able escalating cooldown + manual pause state machine; `new-launchagent.sh` — idempotent installer with validation) and an annotated plist template. Eval'd against baseline on three realistic prompts (new-install / noisy-watchdog triage / config audit): with-skill 17/17 assertions vs baseline 16/17 (baseline's one miss: prescribed the deprecated `unload`). The eval harness itself caught a real bug in the cooldown library — the exhausted-round counter went stale during cool-down waits longer than the stale window, pinning backoff at tier 1 forever; fixed in the library and in the production watchdog it was distilled from. - **slides-creator** (v2.0.0): **DEPRECATED — skill retired to a stub, no longer maintained.** Same consolidation as ppt-creator (daymade-docs v1.6.0, same release): the PPT toolchain merge of 2026-08-07 folded its methodology — First Law (user's voice is primary), the ABCDEFG narrative-discussion framework, the baoyu-slide-deck delegation protocol, and the four-layer directory governance — into deck-creator's Route A · narrative in the author's private marketplace. For external users: this is the final version, kept for install compatibility, receiving no further updates; physical removal in a future major release. references/ and scripts/ removed here (git history preserves them); SKILL.md is now a deprecation notice. diff --git a/macos-watchdog/.security-scan-passed b/macos-watchdog/.security-scan-passed new file mode 100644 index 0000000..76c97b2 --- /dev/null +++ b/macos-watchdog/.security-scan-passed @@ -0,0 +1,4 @@ +Security scan passed +Scanned at: 2026-08-08T21:57:05.342073+00:00 +Tool: gitleaks + pattern-based validation +Content hash: 6e2dea22b996dd70b4737ea46790b1ba163d1f1d9af76e04d230b9d9f8be3879 diff --git a/macos-watchdog/SKILL.md b/macos-watchdog/SKILL.md new file mode 100644 index 0000000..ee02aa4 --- /dev/null +++ b/macos-watchdog/SKILL.md @@ -0,0 +1,91 @@ +--- +name: macos-watchdog +description: >- + Design, deploy, and discipline macOS launchd watchdogs — LaunchAgents/LaunchDaemons that detect a recurring problem and auto-remediate it. Use whenever creating or editing a persistent background monitor / daemon / agent on macOS, writing a launchd plist, scheduling a self-healing script, or when a watchdog has become a disturbance itself: re-launching apps the user quit, firing repeated notifications, re-running its full repair ladder every few minutes on an unfixable network, or hammering the system (crash loops, fork storms, runaway restarts). Also use for stop/disable semantics (bootout vs bootstrap vs disable vs unload), adding cooldown / backoff / notification throttling to a self-healer, binding a monitor's lifecycle to its premise state, or auditing existing LaunchAgents. 中文触发:launchd 守护进程、常驻任务、开机自启、后台监控、定时自愈脚本。 Covers KeepAlive/ThrottleInterval/domains/logging, premise self-checks, auto-cooldown, alert layering, batch throttling. +--- + +# macOS Watchdog + +A watchdog is a launchd job that periodically detects a recurring problem and remediates it without a human. The craft is not "how to install a plist" — it is **how to keep the watchdog from becoming a new disturbance**: every watchdog on this machine was born from an incident, and the recurring failure mode afterward is the watchdog itself (false "all good" reports, notification floods, re-launching apps the user quit, fork-bomb replays). + +The governing principle, learned the expensive way: **a watchdog's lifecycle is bound to its premise state**. When the condition it exists to fix cannot be fixed by it (broken WiFi, user quit the target app, prerequisite state gone), the watchdog must stand down *by itself* — not wait for a human to disable it. + +## Entry decision tree + +| The situation is… | Go to | +|---|---| +| Installing a NEW watchdog from scratch | § Deploy, then § The quiet-watchdog contract | +| An existing watchdog misbehaves (spam, re-launches apps, hammers) | § The quiet-watchdog contract, diagnose which clause it violates | +| Stopping / disabling / restarting a job | § Stop semantics | +| plist key details (KeepAlive forms, domains, logging, resource limits) | `references/launchd-plist-reference.md` | +| Cooldown/backoff/notification-throttle patterns + sanitized war stories | `references/quiet-watchdog-patterns.md` | +| SRE alert layering (page vs ticket, fatigue numbers) | `references/alert-discipline.md` | + +## The quiet-watchdog contract (the four clauses) + +Before shipping or blessing any watchdog, all four must hold. Each clause exists because a real watchdog violated it. + +### 1. Premise-state self-check — it knows when it has no job + +The script's first act on every run: verify the state that justifies its existence still holds. If not, exit silently — no remediation, no notification, no side effects. + +- A proxy-repair watchdog checks the proxy app is running first; user quit it → skip the cycle. +- A "did the config switch back" watcher checks the config state it watches; already switched → self-stop, not another round of misleading notifications. (Real case: a recovery watcher kept firing for 2h after its premise resolved, sending 3 spurious notices, because nothing told it to stop.) + +### 2. Remediate first, page only on sustained failure + +Detection stays honest on every cycle, but the *disruptive action* defers until the failure persists across N consecutive cycles (patient mode). Rationale: oscillating chains self-recover in minutes; a force-reconnect on a self-limiting blip is net-harmful. Measure your system's real self-recovery window before choosing N (one chain's 94-min observatory run showed ≤3 min self-recovery → N=2 cycles at 5-min interval). + +Escalation ladder (cheap → disruptive): refresh state → restart connection → remote repair. Each rung verifies before climbing. + +### 3. Escalating auto-cooldown — an unfixable environment means silence + +When the full repair ladder fails, the environment is unfixable by the watchdog (broken WiFi, captive portal, dead upstream). The naive behavior — re-run the entire ladder + notification every interval forever — is exactly "the watchdog keeps re-launching the app every 10 minutes." + +`ThrottleInterval` does **not** fix this: it throttles process respawn, is a fixed delay with no backoff, and does nothing for a job that exits 0 after spamming. Cool-down must live in the application layer: + +- Record consecutive exhausted rounds in a state file. +- After each exhausted round, stand down for an escalating tier (e.g. 30 min → 2 h → 6 h, last tier repeats). +- One notification when *entering* cool-down; zero during it. On tier expiry, retry one round; any real heal clears the counter and the cool-down state. +- A manual `pause [duration]` command with a TTL state file is the fallback — but the auto path must work with no human command at all. A disable mechanism that requires the user to remember a command is not a mechanism. + +Reusable implementation: `scripts/watchdog-cooldown.sh` (source it; provides `paused_any`, `record_exhausted`, `clear_exhausted`, `cmd_pause`/`cmd_resume`). + +### 4. Never resurrect what the user explicitly quit + +On macOS, `open ` **launches the app** when it isn't running, and `open` without `-g` steals foreground. A watchdog whose remediation uses URL schemes (or `open -a`, or restarting a GUI app) will read to the user as "I quit it and it came back." + +Gate every such action: check the target process is alive before invoking its scheme, and pass `-g` so a legitimate action never pops a window. If the user quit the app mid-remediation, abort the ladder — cleanup traps must honor the same gate, or the "ensure connected on exit" fallback becomes the resurrector. + +## Deploy (mechanics that bite) + +1. **Location**: user agent → `~/Library/LaunchAgents/` (GUI session context: can `open` apps, show notifications); system daemon → `/Library/LaunchDaemons/` (root, no GUI access). Choose by whether the job needs the user's GUI session, not by habit. +2. **plist**: start from `assets/launchagent.template.plist` (annotated: Label, ProgramArguments, StartInterval, StandardOutPath/StandardErrorPath, ThrottleInterval, Nice). Validate with `plutil -lint`. `ProgramArguments` element 0 = absolute path; never rely on PATH inheritance. +3. **Load/reload**: `launchctl bootstrap gui/$(id -u) `; after editing a plist, `bootout` then `bootstrap` again — launchd's active state must match disk. Force one run with `launchctl kickstart -k gui/$(id -u)/