Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
"hooks": [
{
"type": "command",
"command": "if [ -n \"${CLAUDECODE}${CLAUDE_CODE}\" ] && [ -f .saneprocess ]; then ruby ~/SaneApps/infra/SaneProcess/scripts/hooks/session_start.rb; fi",
"command": "~/SaneApps/infra/SaneProcess/scripts/hooks/run_hook.sh session_start.rb",
"timeout": 15
}
]
Expand All @@ -168,7 +168,7 @@
"hooks": [
{
"type": "command",
"command": "if [ -n \"${CLAUDECODE}${CLAUDE_CODE}\" ] && [ -f .saneprocess ]; then ruby ~/SaneApps/infra/SaneProcess/scripts/hooks/saneprompt.rb; fi",
"command": "~/SaneApps/infra/SaneProcess/scripts/hooks/run_hook.sh saneprompt.rb",
"timeout": 5
}
]
Expand All @@ -188,7 +188,7 @@
"hooks": [
{
"type": "command",
"command": "if [ -n \"${CLAUDECODE}${CLAUDE_CODE}\" ] && [ -f .saneprocess ]; then ruby ~/SaneApps/infra/SaneProcess/scripts/hooks/sanetools.rb; fi",
"command": "~/SaneApps/infra/SaneProcess/scripts/hooks/run_hook.sh sanetools.rb",
"timeout": 5
}
]
Expand All @@ -209,7 +209,7 @@
"hooks": [
{
"type": "command",
"command": "if [ -n \"${CLAUDECODE}${CLAUDE_CODE}\" ] && [ -f .saneprocess ]; then ruby ~/SaneApps/infra/SaneProcess/scripts/hooks/sanetrack.rb; fi",
"command": "~/SaneApps/infra/SaneProcess/scripts/hooks/run_hook.sh sanetrack.rb",
"timeout": 5
}
]
Expand Down Expand Up @@ -252,7 +252,7 @@
"hooks": [
{
"type": "command",
"command": "if [ -n \"${CLAUDECODE}${CLAUDE_CODE}\" ] && [ -f .saneprocess ]; then ruby ~/SaneApps/infra/SaneProcess/scripts/hooks/sanestop.rb; fi",
"command": "~/SaneApps/infra/SaneProcess/scripts/hooks/run_hook.sh sanestop.rb",
"timeout": 5
}
]
Expand Down
6 changes: 2 additions & 4 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@
"url": "https://developers.openai.com/mcp"
},
"xcode": {
"command": "xcrun",
"args": [
"mcpbridge"
]
"type": "http",
"url": "http://127.0.0.1:37915/mcp"
}
}
}
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ agent overlay, not the full runbook. Detailed implementation, release, Mini,
and operator setup notes live in `DEVELOPMENT.md`, `ARCHITECTURE.md`,
`DEVELOPER_SETUP.md`, `templates/RELEASE_SOP.md`, and `scripts/`.

Regular daily work is Grok, Grokbot, and Cursor. Grok uses native
`~/.grok/hooks` (git source `scripts/hooks/grok/hooks.json`). Cursor uses
`~/.cursor/hooks.json`. Keep Codex/Claude hook adapters working. Do not send
regular jobs to OpenAI or Anthropic unless the owner asks.

Speak plainly and briefly. Use singular voice for SaneApps communications:
`I`, `me`, `my`; never `we`, `us`, or `our`.

Expand Down Expand Up @@ -152,7 +157,7 @@ the wrapper.
| Work email | `ruby scripts/SaneMaster.rb check_inbox` or `~/SaneApps/infra/scripts/check-inbox.sh` |
| Sales/download/funnel | `sales`, `downloads`, `events` |
| Tool discovery | `ruby scripts/SaneMaster.rb tool_discovery --query "..."` |
| Cleanup | `ruby scripts/SaneMaster.rb machine_cleanup --host mini --apply --preserve-apps AppName` |
| Cleanup | `ruby scripts/SaneMaster.rb machine_cleanup --host local --apply --preserve-apps AppName` (Air) or `--host mini --apply` / `--server` (Mini). Plans generated junk by kind, not free space. |
| Verification scope plan | `ruby scripts/SaneMaster.rb proof_plan --task "..."` |
| Process health | `process_eval`, `sop_review`, `near_miss_review`, `verify_failure_review` |
| Route cost review | `ruby scripts/SaneMaster.rb route_cost_review --json` |
Expand Down
1 change: 1 addition & 0 deletions DEVELOPER_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ shared public key above (release preflight checks this).
| `lemonsqueezy` | `api_key` | sales/license tooling |
| `resend` | `api_key` | email automation |
| notarytool profile `notarytool` | — | `xcrun notarytool --keychain-profile notarytool` |
| `claude_hook` / `hmac_secret` | same value on Air and Mini | Production release-receipt signing. Both machines must derive the pinned Ed25519 public key. If Air cannot sign receipts, copy Mini `~/.claude_hook_secret` into this keychain item and `~/.claude_hook_secret` (chmod 600). |

Fetch each secret once per run and reuse it — no `security` calls in loops
(see `AGENTS.md` Secrets).
Expand Down
70 changes: 69 additions & 1 deletion SESSION_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,77 @@
# SaneProcess Session Handoff

As of: 2026-08-17 America/New_York
As of: 2026-08-21 America/New_York
Owner host: Mac Mini = tree truth; Air = controller.
Repo: `~/SaneApps/infra/SaneProcess`

## 2026-08-21 regular clients: Grok, Grokbot, Cursor

- Owner: daily work is Grok, Grokbot, and Cursor. SaneProcess stays compatible
with Codex and Claude. Do not route regular jobs or new recurring work through
OpenAI/Anthropic. Recurring work uses SaneMaster/launchd plus Mini Grok
headless heartbeats. All Air and Mini Codex heartbeats are PAUSED after the
replacements were proven.
- Proven 2026-08-21: Mini Grok heartbeat smoke (`PONG`); App+CWS review watch
GET-only (UTF-8 fix); SaneCite Monday sweep HTTP (Air, 0 failures); SaneBar
macOS 27 watch (still beta, no notify). X scout is now a Grok heartbeat, not
the paid X API. Launch-ops / Prophecy resume use the same Grok runner; not
executed fully this pass because they mutate inbox/batches.
- NVIDIA weekly scout was not moved: `nvidia_eval` is not in SaneMaster and the
NVIDIA-agent rule forbids it unless the owner asks again. SaneClip 8am
release and SaneLot 1.2.1 live-auction gate stay retired/paused.
- D-U-N-S reminder was a one-shot Codex nag; paused. Still an owner task if
SaneLot Google verification needs it.

## 2026-08-21 keep-current: pins apply themselves, Grok wrappers stop drifting

- Weekly Air LaunchAgent `com.saneapps.keep-current` (Sunday 09:15) applies npm
pins, auto-bumps `firecrawl-cli` within the same major, and notifies only on
drift. Mini nightly applies Mini pins. Homebrew/Codex/Claude are not
auto-upgraded; Claude `autoUpdates` is now on; Grok already auto-updates.
- Grok wrappers live in git `scripts/grok-bin/` (`cloudflare-mcp-remote.sh`,
`xcode-mcp.sh`, `xcode-mcp-frame.py`). `sync_grok` overlays them and no longer
`--delete`s `~/.grok/bin` (that was wiping the Grok CLI).
- Air Grok apple-docs is HTTP `http://127.0.0.1:37911/mcp` through the existing
AgentMemory tunnel, which also forwards 37911/37913/37915. Xcode is the Mini
HTTP singleton at `http://127.0.0.1:37915/mcp`, not a fresh SSH stdio spawn.
mcpbridge still needs Xcode open on Mini. Proven 2026-08-21: Mini `/healthz`
ok, Air initialize HTTP 200, live Grok `XcodeListWindows` returned Mini
SaneHosts. Leftover Mini `com.saneapps.x-opportunity-scout` plist was
removed; the live 10:00 job is the Grok heartbeat.
- Firecrawl CLI is 1.23.1 with `firecrawl developer` and the
`firecrawl-developer-index` skill. No Firecrawl MCP.

## 2026-08-21 native Grok/Cursor hooks, Codex/Claude stay adapters

- Grok was importing Claude `settings.json` hooks. Those scripts read
`tool_name == "Bash"` and no-op on `GROK_HOOK_EVENT`, so Grok shell guards
never saw `toolName: run_terminal_command`. Native Grok hooks live in
`~/.grok/hooks/sane-guards.json` (git source `scripts/hooks/grok/hooks.json`).
Grok `compat.claude` / `compat.cursor` hook import is off so the Claude SOP
no-ops do not paint every tool. Shared payload adapter:
`scripts/hooks/core/hook_payload.rb`. Cursor `~/.cursor/hooks.json` still
runs the Cursor adapters. Claude `.claude/settings.json` and Codex stay on
their own registrations.

## 2026-08-16 machine_cleanup hunts junk by kind, not free space

- Owner correction: Air `machine_cleanup` was skipping generated junk because
the disk was marked healthy (451G free). Hygiene now plans unnecessary
generated dumps on any host regardless of free space. Disk pressure still
gates only expensive-to-restore caches (Playwright, HuggingFace,
`codex-runtimes`, npm/npx, simulator runtime images).
- Air apply reclaimed the planned set (19.84G planned, 117/117 actions, Trash
emptied). SaneLot dropped from 14G to 1.3G after
`outputs/mini-storage-archive`, loose verify xcresults, and old run
xcresults were removed. SaneVideo container `tmp`, setapp_review, uv stale
archives, pnpm cache, and memory-sync backups are gone. Codex sessions,
SaneVideo Documents, Logos, Photos, and sim runtimes were left alone.
- Nightly: Air `com.saneapps.machine-cleanup` at 05:40 runs
`machine_cleanup --host local --apply --quiet`. Mini
`com.saneapps.memory-guard` at 05:40 still runs the server reset. Planner
files were copied to the Mini checkout so tonight's Mini pass uses the new
rules.

## 2026-08-17 locked Mini screenshot evidence lane

- `capture-mini-screenshot.sh --locked-evidence` now preserves nonzero helper
Expand Down
20 changes: 16 additions & 4 deletions scripts/SaneMaster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,9 @@ class SaneMaster
'restore' => { args: '', desc: 'Fix Xcode/Launch Services issues' },
'install_provisioning_profiles' => { args: '[--delete-source] [glob ...]', desc: 'Install downloaded provisioning profiles deterministically by UUID' },
'dedupe_apps' => { args: '[--host local|mini] [--apps App1,App2] [--dry-run] [--json]', desc: 'Keep one canonical app bundle per Sane app' },
'machine_cleanup' => { args: '[--host local|mini] [--server] [--apply] [--empty-trash] [--json] [--preserve-apps A,B]', desc: 'Prune disposable caches and generated build/test artifacts without touching active app work' },
'machine_cleanup' => { args: '[--host local|mini] [--server] [--apply] [--empty-trash] [--json] [--preserve-apps A,B]', desc: 'Prune unnecessary generated junk by kind, not free space, without touching active app work' },
'mcp_watchdog' => { args: '[status|doctor|clean|install|uninstall] [--max N] [--interval SEC] [--json] [--quiet]', desc: 'Detect and clean duplicate MCP daemons' },
'keep_current' => { args: '[--apply] [--npm-only] [--latest] [--apply-safe-latest] [--notify] [--install-agent] [--role air|mini]', desc: 'Apply pinned CLI/MCP versions, auto-bump Firecrawl, and install the weekly keep-current agent' },
'universal_control_reset' => { args: '[--status] [--dry-run] [--local-only|--mini-only] [--cleanup-mini] [--reboot-mini]', desc: 'Recover Air↔Mini Universal Control / pointer handoff' },
'work_session_on' => { args: '', desc: 'Start keep-awake + no-lock work session guard' },
'work_session_off' => { args: '', desc: 'Restore previous lock settings and stop work-session guard' },
Expand Down Expand Up @@ -708,6 +709,8 @@ def maybe_route_to_mini!(command, args)
SANEPROCESS_APPROVE_FAST_RELEASE
SANEPROCESS_APPROVE_OPEN_REGRESSION_RELEASE
SANEPROCESS_APPROVE_UNCONFIRMED_REGRESSION_CLOSE
SANEPROCESS_RELEASE_POLICY_ONLY
SANEBAR_RELEASE_POLICY_ONLY
SANEBAR_APPROVE_FAST_RELEASE
SANEBAR_APPROVE_OPEN_REGRESSION_RELEASE
SANEBAR_APPROVE_UNCONFIRMED_REGRESSION_CLOSE
Expand Down Expand Up @@ -2124,6 +2127,8 @@ def dispatch_command(command, args)
run_sync_mini(args)
when 'sync_grok', 'sync-grok'
run_sync_grok(args)
when 'sync_control_plane', 'sync-control-plane'
run_mini_sync_script('sync-control-plane.sh', args)
when 'setapp_status', 'setapp-status'
system('ruby', File.join(__dir__, 'setapp_status.rb'), *args)
exit($CHILD_STATUS.exitstatus || 1) unless $CHILD_STATUS&.success?
Expand Down Expand Up @@ -2159,6 +2164,13 @@ def dispatch_command(command, args)
exit(success ? 0 : 1)
when 'mcp_watchdog', 'mcpw', 'mcp'
mcp_watchdog(args)
when 'keep_current', 'keep-current'
system(
'/opt/homebrew/opt/ruby/bin/ruby',
File.join(__dir__, 'automation', 'dependency_baseline.rb'),
*args
)
exit($CHILD_STATUS.exitstatus || 1)
when 'universal_control_reset', 'uc_reset', 'ucr'
universal_control_reset(args)
when 'work_session_on', 'wson'
Expand Down Expand Up @@ -2750,15 +2762,15 @@ def print_category_help(category)
},
'machine_cleanup' => {
usage: 'machine_cleanup [--host local|mini] [--server] [--apply] [--empty-trash] [--json] [--preserve-apps A,B]',
description: 'Prune disposable caches, stale generated evidence, simulators, DerivedData, and optional Mini server artifacts; Trash stays recoverable by default.',
description: 'Prune unnecessary generated junk by kind on any host. Free space only gates expensive-to-restore caches (Playwright, HuggingFace, sim runtimes, npm). Trash stays recoverable by default.',
flags: {
'--host local|mini' => 'Inspect this machine or route the cleanup command to the Mini',
'--server' => 'Mini-only aggressive server reset: prune generated repo artifacts, routed workspaces, simulator runtimes, Codex residue, bulk outputs, and disposable app containers',
'--apply' => 'Perform the planned safe cleanup; default is dry-run',
'--empty-trash' => 'Permanently empty Trash after reversible cleanup; explicit approval only',
'--preserve-apps A,B' => 'Additional app names to preserve even if no process is currently visible',
'--min-free-gb N' => 'Disk pressure threshold used in the report',
'--cache-threshold-gb N' => 'Minimum disposable-cache total before cache pruning is planned',
'--min-free-gb N' => 'Disk-pressure floor for expensive-to-restore caches only',
'--cache-threshold-gb N' => 'Minimum size of one cache before it is planned (default 0.25G)',
'--deriveddata-age-days N' => 'Only prune inactive DerivedData older than this many days',
'--json' => 'Emit machine-readable output'
},
Expand Down
12 changes: 4 additions & 8 deletions scripts/app_test_mode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -881,14 +881,10 @@ set_app_mode_keychain_local() {

case "$mode" in
pro)
run_keychain_swift_local "$(swift_keychain_upsert_script)" \
APP_TEST_SERVICE="$service" \
APP_TEST_LICENSE_KEY_NAME="$key_name" \
APP_TEST_LICENSE_KEY_VALUE="$pro_value" \
APP_TEST_LICENSE_EMAIL_NAME="$email_name" \
APP_TEST_LICENSE_EMAIL_VALUE="$email_value" \
APP_TEST_LICENSE_DATE_NAME="$date_name" \
APP_TEST_LAST_VALIDATION="$now"
# Do not write login-keychain items from unsigned `swift -`.
# That binds the ACL to the Swift interpreter, so the real app
# prompts after every OS update. Defaults fallback is enough.
echo "$app: skipping unsigned keychain seed (defaults fallback only)"
;;
basic)
run_keychain_swift_local "$(swift_keychain_delete_script)" \
Expand Down
2 changes: 1 addition & 1 deletion scripts/appstore_submit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
def parse_env_file(path)
return unless File.file?(path)

File.foreach(path) do |line|
File.read(path, mode: 'r:UTF-8', invalid: :replace, undef: :replace).each_line do |line|
next if line.strip.empty? || line.lstrip.start_with?('#')

text = line.sub(/\A\s*export\s+/, '').strip
Expand Down
5 changes: 4 additions & 1 deletion scripts/automation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,13 @@ utility onto the server or bulk-upgrading release lockfiles.
ruby scripts/automation/dependency_baseline.rb --check --role mini
ruby scripts/automation/dependency_baseline.rb --apply --role mini
ruby scripts/automation/dependency_baseline.rb --apply --role air
ruby scripts/SaneMaster.rb keep_current --apply --npm-only --latest --apply-safe-latest --notify --install-agent
```

`keep_current` is the unattended lane: weekly Air LaunchAgent `com.saneapps.keep-current` (Sunday 09:15) applies npm pins, auto-bumps `firecrawl-cli` within the same major, and notifies only on drift. Mini nightly applies Mini pins. It does not float `@latest`, does not auto-upgrade Codex/Claude/Grok clients, and does not bulk-upgrade Homebrew.

The baseline keeps Node 24 LTS, Homebrew Ruby, the shared build/release tools,
and shared MCP packages current. It installs a restart-safe `.zshenv` PATH,
shared MCP packages, and Firecrawl CLI current. It installs a restart-safe `.zshenv` PATH,
preserves unrelated shell configuration with a timestamped backup, keeps
role-specific npm tools separate, and removes the unpinned global Wrangler in
favor of each repo's explicit release version. It does not upgrade SwiftPM,
Expand Down
109 changes: 109 additions & 0 deletions scripts/automation/agent-heartbeat.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/bin/bash
# Run one Grok headless heartbeat from a prompt file on the Mac Mini.
# Primary replacement for retired Codex heartbeat prompts that need agent judgment.

set -euo pipefail

usage() {
cat <<USAGE
Usage: $(basename "$0") --id ID --prompt-file PATH [--cwd DIR] [--timeout SECONDS]

Examples:
$(basename "$0") --id saneapps-launch-ops --prompt-file scripts/automation/heartbeats/saneapps-launch-ops.md
USAGE
}

ID=""
PROMPT_FILE=""
CWD="$HOME/SaneApps/infra/SaneProcess"
TIMEOUT_SECONDS="${AGENT_HEARTBEAT_TIMEOUT_SECONDS:-5400}"
GROK_BIN="${GROK_BIN:-$HOME/.grok/bin/grok}"
OUT_ROOT="$HOME/SaneApps/outputs/agent-heartbeats"

while [[ $# -gt 0 ]]; do
case "$1" in
--id)
ID="$2"
shift 2
;;
--prompt-file)
PROMPT_FILE="$2"
shift 2
;;
--cwd)
CWD="$2"
shift 2
;;
--timeout)
TIMEOUT_SECONDS="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown option: $1" >&2
usage >&2
exit 64
;;
esac
done

[[ -n "$ID" ]] || { echo "ERROR: --id is required" >&2; exit 64; }
[[ -f "$PROMPT_FILE" ]] || { echo "ERROR: prompt file not found: $PROMPT_FILE" >&2; exit 64; }
[[ -x "$GROK_BIN" || -n "$(command -v grok 2>/dev/null || true)" ]] || {
echo "ERROR: grok not found (expected $GROK_BIN or PATH)" >&2
exit 127
}

command -v grok >/dev/null 2>&1 || GROK_BIN="$HOME/.grok/bin/grok"
OUT_DIR="$OUT_ROOT/$ID"
LOCK_DIR="$OUT_DIR/.lock"
mkdir -p "$OUT_DIR"

if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "$(date -Iseconds) skip: prior $ID run still holds lock" >>"$OUT_DIR/run.log"
exit 0
fi

cleanup() {
rmdir "$LOCK_DIR" 2>/dev/null || true
}
trap cleanup EXIT

STAMP="$(date +%Y%m%dT%H%M%S)"
LOG="$OUT_DIR/run-$STAMP.log"
RECEIPT="$OUT_DIR/latest.json"

RUBY_BIN="${SANEPROCESS_RUBY:-/opt/homebrew/opt/ruby/bin/ruby}"
{
echo "== $(date -Iseconds) agent-heartbeat id=$ID cwd=$CWD grok=$GROK_BIN =="
cd "$CWD"
# macOS has no GNU timeout(1). Bound the Grok process group in Ruby.
"$RUBY_BIN" -rtimeout -e '
timeout_seconds = Integer(ARGV.shift)
cmd = ARGV
pid = spawn(*cmd, pgroup: true)
begin
Timeout.timeout(timeout_seconds) { Process.wait(pid) }
exit(Process.last_status&.exitstatus || 1)
rescue Timeout::Error
Process.kill("TERM", -pid) rescue nil
sleep 2
Process.kill("KILL", -pid) rescue nil
Process.wait(pid) rescue nil
exit 124
end
' "$TIMEOUT_SECONDS" "$GROK_BIN" \
--prompt-file "$PROMPT_FILE" \
--cwd "$CWD" \
--output-format json \
--always-approve
} >"$LOG" 2>&1
STATUS=$?

printf '{"id":"%s","finished_at":"%s","exit_code":%s,"log":"%s"}\n' \
"$ID" "$(date -Iseconds)" "$STATUS" "$LOG" >"$RECEIPT"

exit "$STATUS"
Loading