Repository navigation
Conversation
Adds DISTRIBUTION.md (the 5-phase plan to ship as a signed macOS app
backed by the agent-universe cloud control plane) and lands Phase 0:
the path/port/env/launchd indirection that lets the .app bundle redirect
runtime state out of the repo cwd without changing the dev workflow.
- statePath() / state_path() helpers in util_paths.{ts,py}: resolve to
$SUTANDO_HOME/<name> when set, else <REPO_DIR>/<name>. Callers updated
in task-bridge, voice-agent, web-client, inline-tools, voice-context,
agent-api, dashboard, health-check, screen-capture, init.sh, startup.sh.
- Port overrides: DASHBOARD_PORT, AGENT_API_PORT, SCREEN_CAPTURE_PORT,
SUTANDO_SCREENSHOT_DIR (defaults unchanged).
- Layered .env loader (src/load-env.ts): repo .env first, then
$SUTANDO_HOME/.env with override. voice-agent + conversation-server
switched to it; startup.sh sources $SUTANDO_HOME/.env so Python
services inherit it.
- launchd plist templates under app/LaunchAgents/ for the seven services
the .app bundle will install (voice-agent, web-client, dashboard,
agent-api, screen-capture, credential-proxy, phone-conversation).
Templates use {{REPO_DIR}}/{{NPX_BIN}}/{{PYTHON_BIN}}/{{SUTANDO_HOME}}
placeholders the Swift launcher substitutes at install time.
- Vendor bodhi-realtime-agent: github commit pin (#ffec0cd) → npm 0.1.5
for reproducibility under notarization.
Backwards compat: every change defaults to current behavior when
SUTANDO_HOME is unset, so `bash src/startup.sh` from a fresh clone
keeps working. Verified: tsc clean, 150/150 tests pass, smoke-tested
init.sh + util_paths + load-env in both unset and set-to-tmpdir modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the existing dev environment in a distributable macOS .app bundle
without changing any runtime behavior. Internal-release-ready: builds,
ad-hoc signs, packages into a 93MB drag-to-Applications DMG. Real
Developer ID signing + notarization are Phase 2.
- app/Info.plist: com.sutando.app, TCC usage descriptions for Screen
Recording / Accessibility / Microphone / Camera / Contacts / Calendar /
Reminders / Desktop+Documents+Downloads, custom URL scheme `sutando://`
for cloud-account login callback, LSUIElement (menu-bar only).
- app/Sutando.entitlements: hardened-runtime entitlements covering the
apple-events surface used by inline-tools/browser-tools, network
client+server for the seven local services, JIT + library-validation-off
for bundled Node + Python.
- app/build-app.sh: produces Sutando.app. Compiles main.swift +
LaunchAgentInstaller.swift; copies Info.plist, LaunchAgent templates,
icon; stages src/, skills/, package.json, tsconfig, CLAUDE.md, assets/,
and node_modules/ (~212MB) into Contents/Resources/repo/; ad-hoc signs
with hardened runtime + entitlements.
- app/build-dmg.sh: hdiutil-based UDZO compressed DMG (93MB) with
/Applications symlink for drag-to-install + READ ME FIRST.
- app/README.md: build + install + uninstall + prereqs (internal-release
v1 relies on Homebrew node 22 + fswatch + system python3 because
Homebrew node depends on Homebrew dylibs; LaunchAgentInstaller
resolves system fallback paths automatically).
- src/Sutando/LaunchAgentInstaller.swift: reads Resources/LaunchAgents/
*.plist.template, substitutes {{REPO_DIR}}/{{NPX_BIN}}/{{TSX_BIN}}/
{{NODE_BIN}}/{{NODE_BIN_DIR}}/{{PYTHON_BIN}}/{{SUTANDO_HOME}}/
{{LOG_DIR}}, writes to ~/Library/LaunchAgents/ and runs `launchctl
bootout && bootstrap`. Falls back to /opt/homebrew/bin → /usr/local/bin
→ /usr/bin when bundled runtime isn't present (covers the Phase 1
no-bundled-Node path).
- src/Sutando/main.swift: workspace prefers Bundle.main.resourcePath/repo
when running from .app, falls back to the existing 8-dir-walk for
raw-binary dev. New stateRoot lazy var reads $SUTANDO_HOME or
workspace. Updated `/results`, `/tasks`, `/state`, `/logs` call sites
to use stateRoot. Single-instance pgrep pattern broadened to
(Sutando|MacOS)/Sutando$ to match both .app and raw-binary layouts.
logToFile creates the log dir defensively. New menu items: Install /
Uninstall Background Services.
- src/startup.sh: swiftc invocation now compiles both Swift files
(main.swift + LaunchAgentInstaller.swift), with rebuild detection
across all source files, not just main.swift.
- src/health-check.py: pgrep pattern for Sutando.app updated to
(Sutando|MacOS)/Sutando.
Verified: tsc clean, 150/150 tests pass; .app boots both bundled
(workspace=Sutando.app/Contents/Resources/repo, SUTANDO_HOME honored)
and raw-binary (workspace=repo cwd); DMG mounts with correct layout;
LaunchAgent renderer round-trips templates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the production-release pipeline: Developer ID signing, Apple notarization, Sparkle 2 auto-update, EdDSA-signed appcast, and a GitHub Actions workflow that runs the whole flow on a macOS M-series runner. Default ad-hoc dev build is preserved — Sparkle and Developer ID are gated by env vars / GitHub secrets, so this PR doesn't change local-dev behavior. Phase 2.1 — signing-aware build: - app/build-app.sh accepts SIGNING_IDENTITY (default "-" for ad-hoc). Real Developer ID signs nested Sparkle helpers (Updater.app, Installer.xpc, Downloader.xpc, Autoupdate, Sparkle.framework itself) before the outer bundle. - app/notarize.sh: xcrun notarytool submit + wait + stapler. Accepts either NOTARYTOOL_PROFILE (stored keychain credentials) or the APPLE_ID + APPLE_TEAM_ID + APPLE_APP_SPECIFIC_PASSWORD trio. Phase 2.2 — Sparkle: - src/Sutando/SparkleUpdater.swift: thin façade with both real (#if ENABLE_SPARKLE) and no-op stub implementations sharing the same API. main.swift can reference SparkleUpdater unconditionally. - main.swift: instantiates SparkleUpdater + adds "Check for Updates…" menu item. - Info.plist: SUFeedURL (channel-templated at runtime), SUPublicEDKey (placeholder — replace with real key from generate-keys.sh before shipping), SUEnableAutomaticChecks, SUScheduledCheckInterval=3600, SUEnableInstallerLauncherService. - build-app.sh: ENABLE_SPARKLE=1 + SPARKLE_FRAMEWORK pointing at the vendored framework triggers -F + -framework Sparkle + rpath @executable_path/../Frameworks, copies Sparkle.framework into Contents/Frameworks/, signs nested helpers explicitly. - src/startup.sh: swiftc invocation now includes SparkleUpdater.swift alongside main.swift + LaunchAgentInstaller.swift. Phase 2.3 — appcast tooling: - app/sparkle/fetch-sparkle.sh: downloads Sparkle 2.9.1 (pinned) release tarball, lays out app/vendor/Sparkle.framework + helper binaries (sign_update, generate_keys, generate_appcast) in app/vendor/sparkle-bin/. - app/sparkle/generate-keys.sh: one-time EdDSA keypair generation. Private key stays in macOS keychain; prints the public key for Info.plist. - app/sparkle/generate-appcast.sh: produces signed appcast.xml for a release dir. Channel-aware (--channel internal|beta|stable). Reads SPARKLE_PRIVATE_KEY_FILE for CI use. - .gitignore: app/vendor/ + app/sparkle/sparkle.priv (key backup must never be committed; keychain is canonical). Phase 2.4 — release CI: - .github/workflows/release.yml: macos-14 (M-series) runner, triggered by tag push (v*) or manual workflow_dispatch with version + channel. Steps: checkout → Node 22 + npm ci → typecheck + tests → bump Info.plist version → fetch Sparkle → import Developer ID cert into ephemeral keychain → build .app → notarize .app → build .dmg → notarize .dmg → sign appcast → create GitHub Release. Required secrets documented in workflow header (currently all optional; build degrades gracefully to ad-hoc + unsigned-appcast warnings if absent). Verified: tsc clean, 150/150 tests pass, default ad-hoc build still produces a working .app, all shell scripts pass `bash -n` syntax check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Connects the local Sutando.app to the agent-universe cloud control plane shipped in Phase 3. Sign-in via the sutando:// URL scheme, token in macOS keychain, device heartbeat, and three live meters (voice / TTS / image-gen). 4.1 — src/Sutando/CloudAuth.swift: - Token + userId stored in macOS keychain (com.sutando.app.cloud). - Mirror written to $SUTANDO_HOME/cloud-auth.json (mode 0600) so Node + Python services can read the same auth state without keychain access. - Stable machineId persisted in $SUTANDO_HOME/device.json. - Sign-in flow: nonce → /cli-login?challenge=…&via=urlscheme; cloud redirects to sutando://auth?token=…&userId=…&challenge=…; CloudAuth validates challenge against the on-disk pending file (5-min expiry), stores creds, fires /api/devices heartbeat. - 401 from heartbeat auto-signs-out (token revoked server-side). 4.2 — src/cloud-client.ts: - isCloudSignedIn() / cloudFetch() / recordEvent() / flush() — same shape as the dashboard's API expectations. Batched POST to /api/usage every 60s or 200 events, whichever first. QUEUE_CAP=5000 so a network outage during a long voice session can't OOM the agent. 4.3 — src/cloud_metrics.py: - Synchronous record_event() for the lower-frequency Python meters. Fires on a daemon thread; failures silent. 4.4 — agent-universe /cli-login: - Switched callback from localhost POST to sutando://auth redirect. - Legacy ?port=… localhost path kept as a fallback. 4.5 — main.swift integration: - Sign in / Sign out menu items, wired to CloudAuth.shared. - NSAppleEventManager handler for the sutando:// scheme. - 5-min heartbeat timer (initial + recurring) calling /api/devices. - updateCloudMenuItems() flips visibility on each sign-in/out event. - Trailing-comma swiftc fixes. 4.6 — instrumentation: - voice-agent.ts: emit voice.gemini event from writeVoiceMetrics() with durationSeconds + sessionId + model + toolCalls metadata. - cartesia-tts.ts: emit tts.cartesia event with audio duration computed from PCM byte count / (sample_rate * channels * bytes/sample). - skills/image-generation/scripts/generate.py: emit image.gen or video.gen with units=1 + model name. Lazy import so the script still runs cleanly if cloud_metrics isn't on path. build-app.sh + startup.sh: include CloudAuth.swift in the Swift sources list. App still compiles + boots both bundled and raw-binary. Verified: tsc clean, 150/150 tests pass, .app boots with stub SUTANDO_CLOUD_URL, debug log shows clean startup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gent
Three post-plan additions surfaced by the first end-to-end test on a
fresh machine. Together they close the "non-technical user can install
and use" loop that the original plan punted on with "internal users
are devs".
Phase 4.7 — Settings + first-launch:
- src/Sutando/EnvFile.swift: round-tripping .env parser/writer that
preserves comments + unknown keys + line order. Single-write atomic
via tmp + replaceItemAt + chmod 0600.
- src/Sutando/Permissions.swift: TCC status checks (microphone via
AVCaptureDevice, accessibility via AXIsProcessTrusted, screen
recording via CGPreflightScreenCaptureAccess) + the
x-apple.systempreferences:com.apple.preference.security?Privacy_*
deeplinks for one-click "open the right pane".
- src/Sutando/SettingsWindow.swift: single-pane NSWindow with cloud
account row, API key form (Gemini required + Cartesia + Notification
email + 12 advanced fields under disclosure), permission rows with
✓/✗ indicators + Grant buttons, Background Services Install /
Uninstall. 1.5s polling timer refreshes status while window is open
so returning from System Settings reflects new grants.
- main.swift setupMainMenu(): builds an Application + Edit + Window
menu so ⌘V routes to text fields. Without this, LSUIElement apps
can't paste — found the hard way after the user couldn't paste
their Gemini API key.
- main.swift "Settings…" menu item with ⌘, accelerator. First-launch
detection: SettingsWindowController.needsFirstLaunchSetup checks for
no Gemini key + no $SUTANDO_HOME/.firstrun-complete marker; on
match, Settings auto-opens 300ms after launch.
Phase 4-bug — installer hardening:
- LaunchAgentInstaller.install() returned [String] and threw on the
first launchctl bootstrap failure, leaving partial state on disk.
Real-world: phone-conversation.plist has Disabled=true (paid tier),
launchctl bootstrap returns "5: Input/output error" on Disabled
plists, the throw aborted the loop after only 4 of 7 services
installed.
- Replaced with LaunchAgentInstallSummary { installed, skippedDisabled,
failed } collected per-plist; loop continues on individual errors.
- Added isDisabled() detector (regex on rendered template) — for
Disabled plists, write the file (so user can manually enable later)
but skip the bootstrap call.
- SettingsWindow + main.swift install handlers updated to display
per-failure detail rather than "first error".
Phase 4-core-agent — Claude Code as launchd service:
- Original 7-plist set didn't include the Claude Code core agent that
processes voice tasks. Voice agent's get_core_status returned "not
running" because nothing started claude.
- src/run-core-agent.sh: bash wrapper that creates a tmux session
detached (-d so no TTY required under launchd) running claude with
--dangerously-skip-permissions /proactive-loop, polls
tmux has-session every 30s, exits when session dies so launchd's
KeepAlive recreates it. Self-checks for tmux + claude on PATH and
sleeps 60s rather than crash-loop on missing prereqs.
- app/LaunchAgents/com.sutando.core-agent.plist.template: runs the
wrapper with PATH extended to include /opt/homebrew/bin + ANTHROPIC_BASE_URL
pointing at the credential proxy.
- LaunchAgents/README.md updated with the new entry.
- app/build-app.sh and src/startup.sh swiftc invocations include the
three new Swift files.
Cosmetic: voice-agent.ts data/voice-metrics.jsonl now uses statePath()
so it lands in $SUTANDO_HOME/data/ instead of failing with ENOENT
when the bundled (read-only) repo is the cwd.
DISTRIBUTION.md: added a Status section near the top with a table
summarizing each phase + the operational gaps blocking real internal
release. Annotated every phase header with [DONE]/[PARTIAL]/[PENDING]
+ commit hashes. Replaced the "concrete work" list under Phase 1 with
what actually shipped (deviations called out: no Xcode project, Node
bundling deferred, first-launch wizard superseded). Added new sections
for Phase 4.7, Phase 4-bug, Phase 4-core-agent. Updated sequencing
table to reflect actual progress.
Verified: tsc clean, .app builds, both raw-binary and bundle path
resolution work, Settings opens auto on first launch when no Gemini
key set, install reports "Installed N services. Skipped …" without
aborting on Disabled plists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ndows + guided setup Closes the gap between "internal release UX" and "public release UX": after this commit the .app drag-installs and walks the user through setup with zero Terminal touch except `claude auth login` (Anthropic owns that flow). Four changes: 1. Bundled runtime — Node 22 LTS + tmux + libevent/ncurses dylibs (rpaths rewritten to @executable_path/../lib) + ncurses terminfo, all under Resources/runtime/. New app/bundle-runtime.sh handles the download + stage; build-app.sh runs it before staging the .app. Drops the Homebrew prereq for end users. 2. fswatch removed entirely. New watch-tasks.mjs + watch-tasks-stream.mjs use Node fs.watch with the same on-the-wire contract (TASK_FILE: / TASK_DETECTED:); the .sh files are thin `exec node` wrappers so every existing caller (CLAUDE.md, SKILL.md, Monitor invocations, pgrep -f) keeps working unchanged. Sweep covers startup.sh, init.sh, verify-setup.sh, migrate.sh, READMEs, code comments. 3. Web UIs hosted in-app via WKWebView (new WebWindow.swift). openWebUI() and openDashboard() no longer AppleScript-drive Chrome — they spawn native Sutando windows that proxy onto the existing launchd-managed :8080 / :7844 services. Mic auto-granted for localhost; cold-start retry handles the case where launchd hasn't bootstrapped the service yet. 4. Settings grew a Claude Code section (detect → install via NSTask → Sign-in via Terminal) and a 4-step first-launch stepper at the top (① API key → ② Claude Code → ③ Permissions → ④ Background services) that hides once .firstrun-complete is written. Built artifacts: Sutando.app 350MB / Sutando.dmg 143MB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six fixes for things that broke on a clean macOS account install. All caught by
walking the install flow with state stashed aside (`Sutando.bak`):
1. Mic permission entitlement key — hardened-runtime apps must declare
`com.apple.security.device.audio-input`; the sandbox-era
`com.apple.security.device.microphone` alone makes tccd silently refuse to
prompt ("Policy disallows prompt for ... access to kTCCServiceMicrophone
denied"). Both keys now declared for forward/back compat.
2. Mic prompt suppression on LSUIElement apps — TCC prompts from .accessory
activation-policy apps can be silently swallowed. `grantPermission` now
bumps to `.regular` for the duration of the AVCaptureDevice request,
reverts after. Reorders: fire the prompt FIRST, fall through to System
Settings only if the user denies (opening Settings before the prompt
steals focus and macOS suppresses prompts from non-frontmost apps).
3. Skills missing from `~/.claude/skills/` — the core-agent invokes
`/proactive-loop` as a slash command, which claude resolves by looking
in `~/.claude/skills/`. The .app installer never seeded that directory,
so claude printed "Unknown skill: proactive-loop" and exited, sending
the wrapper into a 30-second crash-loop. `LaunchAgentInstaller.install()`
now runs the bundled `skills/install.sh` to symlink every shipped skill
into the user's global skills dir.
4. Claude Code 2.1.x bypass-permissions warning — Claude now shows an
interactive `1. No, exit / 2. Yes, I accept` prompt on every launch,
even with `--dangerously-skip-permissions`. The default is "exit", so
our wrapper would idle until claude timed out. `run-core-agent.sh` now
sends `Down + Enter` 2 seconds after session start. Also kills any
pre-existing tmux session on every wrapper start (a zombie session
stuck on the warning would otherwise be treated as healthy by
has-session and the send-keys would never fire). Pane output is now
piped to `core-agent.pane.log` so the next bug like this is one tail
away.
5. screen-capture-server "No such file or directory: 'screencapture'" —
the launchd plist's PATH had `/usr/local/bin:/usr/bin:/bin` but the
system `screencapture` lives at `/usr/sbin/screencapture`. Added
`/usr/sbin` to the screen-capture plist template.
6. Screen Recording permission on launchd-spawned python3 — TCC tracks
the launchd-spawned python as a separate identity from Sutando.app,
so granting Screen Recording to the .app doesn't help the helper.
`screenRecording.request()` now also pokes the running screen-capture
service AND fork-spawns python3 with a one-shot `screencapture` so
python3 lands in System Settings → Privacy → Screen Recording
alongside Sutando.app — user toggles both in one trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…0/4.11 plan
Wire desktop-side emitters for the three new admin event streams the
cloud started accepting in Phase 4.11. Without this the funnel and
reliability views are empty no matter how many users sign in.
src/cloud-client.ts (Node)
- Stamp every recordEvent with appVersion (read from bundled Info.plist
or package.json once at startup). Powers release-vs-release regression
analysis on /admin/reliability.
- New: recordOnboarding(step), recordError({...}), startSession({kind}),
endSession(id, {turnCount, toolsUsed, endedReason}). All fire-and-forget,
silent no-op when signed out.
src/cloud_metrics.py — Python equivalents (record_error,
record_onboarding) for image-gen / Twilio / dashboard services.
src/Sutando/CloudClient.swift (new) + app/build-app.sh — Swift caller
for /api/onboarding and /api/errors. Reads cloud-auth.json the same way
CloudAuth does.
SettingsWindow.swift — emits 5 onboarding milestones at their natural
checkpoints (gemini_key_set, firstrun_complete on Save; claude_installed
when claudeCodePath() resolves; perms_granted when all 3 TCC checks pass;
services_installed on clean LaunchAgentInstaller pass). Per-launch
in-memory dedup keeps the 1.5s polling loop quiet; server's
(user_id, step) unique index is the source of truth. Per-failure
launchd.bootstrap_fail error events; fatal launchd.install_failed if
install() throws.
voice-agent.ts — onSessionStart opens cloud session row, onSessionEnd
PATCHes with turnCount + toolsUsed + mapped endedReason. onError +
voice-failure transport classifier emit error events.
conversation-server.ts — twilioCall opens phone.outbound session keyed
by callSid (promise stored in map to handle hangup-before-start race);
twilioHangup awaits the promise and PATCHes. Twilio REST 4xx/5xx →
phone.twilio.call_failed.
DISTRIBUTION.md — adds Phase 4.10 (distribution test, 4 tracks: cloud
deploy + signed DMG + fresh-Mac validation + telemetry emission) and
Phase 4.11 (admin panel summary). Track 4 marked DONE.
Verified: stando typecheck clean, 150/150 tests pass, swiftc parse
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cloud control plane (auth, dashboard, /api/*) now lives at sutando.ag2.ai. Downloads + Sparkle appcast served from ag2.ai/sutando. Replaced everywhere except env-var override paths (SUTANDO_CLOUD_URL, SutandoCloudURL Info.plist key, NEXT_PUBLIC_APP_URL) which still take precedence — the new defaults just match production. - src/Sutando/CloudAuth.swift — apiBase fallback → sutando.ag2.ai - src/Sutando/SparkleUpdater.swift — feedURL → ag2.ai/sutando/updates/<channel>/appcast.xml - app/Info.plist — SUFeedURL → ag2.ai/sutando/updates/internal/appcast.xml - app/sparkle/generate-appcast.sh — log message - src/dashboard.py — Website link in HTML footer - README.md — Website badge → ag2.ai/sutando - DISTRIBUTION.md — every architectural reference Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Partnership with Tigris. S3-compatible so the implementation when transcript storage actually ships will be identical (@aws-sdk/client-s3 with AWS_ENDPOINT_URL_S3 pointed at Tigris). Both have zero egress; Tigris adds versioning + object lock + dynamic data placement that R2 lacks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated via app/sparkle/generate-keys.sh. Private key is in macOS keychain (item: "Private key for signing Sparkle updates"); also exported + base64-encoded for the SPARKLE_PRIVATE_KEY_BASE64 GitHub Actions secret so release.yml can sign appcast entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… signed releases ready
Sparkle 2's SPUUpdater doesn't expose `delegate` as a settable property the way SUUpdater did in Sparkle 1; the delegate must be passed at controller construction. Defer controller creation to after super.init so `self` is available as updaterDelegate. Surfaced by the v0.2.0 CI release build: src/Sutando/SparkleUpdater.swift:37:33: error: value of type 'SPUUpdater' has no member 'delegate' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.0 notarization came back Invalid because: 1. Bundled node + tmux + dylibs in Contents/Resources/runtime/ were ad-hoc signed by bundle-runtime.sh and never re-signed with the Developer ID. Apple's notary walks every Mach-O in the bundle and rejects anything not signed by your team. 2. Sparkle.framework helpers were re-signed with --timestamp=none. Notarization REQUIRES a secure Apple TSA timestamp on every signature. 3. notarize.sh failed silently on Invalid status — the CI log showed only "status: Invalid" with no diagnostic detail. Fixes: - build-app.sh re-signs every Mach-O under Resources/runtime/ with the Developer ID + hardened runtime + secure timestamp + a runtime entitlements file (allow-jit + allow-unsigned-executable-memory + disable-library-validation + allow-dyld-environment-variables) since V8 JIT and child processes don't inherit the parent's entitlements. - Drops --timestamp=none from Sparkle helper signing; switches to default Apple TSA timestamp when SIGNING_IDENTITY is real. - New app/Sutando-runtime.entitlements for the bundled binaries. - notarize.sh now captures the submission ID, detects Invalid status, and runs `xcrun notarytool log` to print the JSON diagnostic — so next-time failures self-explain in the workflow log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…untime/ v0.2.0 round 2 still rejected — npm packages ship pre-built native binaries (ripgrep, fsevents, sharp, esbuild, libvips, .node addons) and skills/ ships a custom rec-indicator helper. Apple's notary walks every Mach-O in the bundle, not just our /runtime/ subdir. Widened the recursive sign pass from Resources/runtime/ to all of Contents/Resources/, and added .node and dylib name patterns to the find. Same runtime entitlements (allow-jit + library-validation off) applied to all — needed because node addons load alongside non-Apple-signed dylibs. Apple's complaint shape (from the v0.2.0 round-2 notarytool log, visible now thanks to the previous commit's log-on-failure): "The binary is not signed with a valid Developer ID certificate" "The signature does not include a secure timestamp" "The executable does not have the hardened runtime enabled" These hit ~10 npm-shipped binaries. The recursive sign pass solves all three for every Mach-O found. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eleases
Two changes in one commit since both block end-user delivery:
1. Notarize round 3 fix — extend recursive Mach-O sign to Frameworks/
The previous round caught all node_modules + runtime binaries but
missed Sparkle.framework's nested helpers. Apple specifically
complained about:
Sparkle.framework/Versions/B/Updater.app/Contents/MacOS/Updater
The find walk now covers both Resources/ AND Frameworks/, so every
Mach-O — including Updater.app's inner binary, the XPC service
binaries, Autoupdate, and the framework's own dylib — gets re-signed
with our Developer ID + hardened runtime + secure timestamp + runtime
entitlements before the outer Sparkle helpers + .app are sealed.
2. Auto-update wiring — Sparkle feed → GitHub Releases
Previously SUFeedURL pointed at ag2.ai/sutando/updates/<channel>/appcast.xml
which doesn't exist yet. Repointed to:
github.com/AG2Platform/stando/releases/latest/download/appcast.xml
GitHub redirects /releases/latest to the most recent non-prerelease.
release.yml previously marked all non-stable channels as prerelease
(so internal releases never appeared as "latest" → Sparkle 404). Now
only `beta` is marked prerelease; `internal` is treated as latest
until stable channel exists. Multi-channel filtering still works via
<sparkle:channel> tags inside the appcast (Sparkle's built-in
mechanism, controlled by SUSelectedChannel UserDefault).
When ag2.ai's CDN is set up, swap the URL back in one commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.2.0 round 4 hit the 60-min job cap with submission still "In Progress" at minute 58. Build + sign takes ~2 min; the rest was Apple queue, which can stretch past an hour during peak/post-resubmit-storm windows. 120 gives enough headroom that we don't waste a queue slot on retries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple's notary scan time scales with the number of Mach-O binaries in the bundle. After 4 hours of "In Progress" status on the v0.2.0 notarization with no response, traced the slowness to 21 native binaries in node_modules — 5 of which come from packages with zero callers. Removed @anthropic-ai/claude-agent-sdk (68M, includes ripgrep + sharp + libvips natives) — DISTRIBUTION.md flagged it as unused months ago, but it lingered in the bundle because bodhi-realtime-agent declares it in optionalDependencies and npm pulls those by default. No code imports it. Also stripping @img/* sharp natives (15M, transitive of claude-agent-sdk but standalone-installed too). Switched the bundle staging from rsync-the-dev-tree to `npm ci --omit=dev` directly into Resources/repo/. That cuts another ~30M (typescript + @types/* + their transitives) which only matter at typecheck time. Bundle reduction: node_modules size: 192M → 108M (44% smaller) Mach-O bins to sign: 21 → 16 (24% fewer) Side cleanups in package.json: - @types/ws moved to devDependencies (where it belongs — types only) - tsx moved from devDependencies to dependencies (the launchd plists invoke `npx tsx` at runtime) - Removed redundant top-level @anthropic-ai/claude-agent-sdk pin Also fixes a TS compile break that surfaced when npm install pulled TypeScript 6.0.2 → 6.0.3 (stricter `private` enforcement on VoiceSession.clientConnected). Added a typed-bypass helper instead of spreading `as any` casts at the 5 callsites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple's notary routes apps with weaker memory protections (especially allow-unsigned-executable-memory on the main executable) into an "in-depth analysis" path that stretches from minutes to days (forum thread 707377). That's exactly what stuck v0.2.0 in queue for 12+ hours. The Swift launcher doesn't JIT — it never needed the entitlement. Bundled node + tmux still get it via Sutando-runtime.entitlements where they actually need it. Also: - Split Mach-O re-sign into two passes: Resources/ uses Sutando-runtime.entitlements (allow-jit etc); Frameworks/ uses --preserve-metadata=entitlements on Sparkle's Downloader.xpc so we don't clobber its sandbox entitlements (would break Sparkle's privilege-separated download at runtime). - Sparkle helpers signed deepest-first per Sparkle's codesigning guide, with the framework wrapper sealing them last. - notarize.sh no longer blocks on `xcrun notarytool submit --wait`. Polls every 30s for up to NOTARY_MAX_WAIT_SEC (default 1800s = 30m), then exits non-zero with submission ID preserved so a follow-up can resume via `notarytool info <id>` + `stapler staple` if Apple eventually decides hours later. - release.yml: timeout 120 → 75 min (notarize.sh self-bounds now). Verified: 4 prior submissions of this build (with the old entitlement) took 12+ hours each in Apple's queue. With the entitlement removed, notarization should follow the fast path (~5–15 min normal SLA). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ut field
Pin bodhi-realtime-agent to sonichi/bodhi_realtime_agent#ffec0cd which
sends `audio:` instead of `media:` in Gemini's sendRealtimeInput call.
The npm-published 0.1.5 we were on shipped a build that pre-dates
this fix and still uses `media:`, which the @google/genai SDK maps to
the deprecated `realtime_input.media_chunks` wire format. Gemini 3.1
recently flipped the deprecation from soft warning to hard reject
(WebSocket close 1007), breaking voice on every install.
The fork SHA is the same as the original Phase 0 pin — unchanged
upstream, but the SOURCE there has the fix even though no new tarball
was published. Installing via npm's git source forces a build from
that source, which produces the corrected dist/.
Verified locally: `dist/chunk-*.js` now contains
this.session.sendRealtimeInput({ audio: { data: base64Data, ... } })
not `media: { ... }`. Typecheck + 150 tests pass.
Phase 0's "won't survive notarization" concern doesn't apply to git
sources of npm packages — the bundle ends up with regular built JS in
node_modules, identical structure to npm-installed packages. Apple's
notary doesn't care about resolution origin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ppcast
Two bugs prevented v0.2.1 from being offered as an upgrade to a v0.2.0
client running "Check for Updates…":
1. Workflow bug — generate-appcast ran BEFORE the version-rename step,
so Sparkle embedded the bare filename `Sutando.dmg` into the
enclosure URL. The actual GitHub asset is `Sutando-0.2.1.dmg`,
meaning even if the appcast surfaced, the DMG download would 404.
Reordered: rename → generate-appcast → rewrite URLs to point at
the canonical `releases/download/v<ver>/Sutando-<ver>.dmg`. Stable
per-tag URL — no `releases/latest` redirect magic that breaks once
we ship a stable channel.
2. Sparkle bug — every appcast item we ship is tagged
<sparkle:channel>internal</sparkle:channel>. Sparkle filters tagged
items unless the user's SUSelectedChannel matches. Fresh installs
have no SUSelectedChannel default, so Sparkle silently skipped every
item ("you're up to date" lie). Register the default at app launch.
Users can still override via `defaults write`.
Test the existing v0.2.1 release with the bug-fixed expectation: client
must have `defaults write com.sutando.app SUSelectedChannel internal`
manually applied since the v0.2.0 binary didn't register it. The next
release built off this commit will register on launch automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ly uninstall Two related changes that surfaced from a real teardown of the v0.2.0 install on a clean account: 1. tmux leak — `LaunchAgentInstaller.uninstall()` unloaded the launchd jobs but left the `sutando-core` tmux session running on `/tmp/sutando-tmux.sock`. tmux daemonizes out of the wrapper's process tree, so killing the launchd job doesn't reach the server. Added `killCoreAgentTmux()` that resolves tmux from the bundled runtime → Homebrew → /usr/bin and runs `kill-server` + removes the stale socket. Fixes both the menu-bar "Uninstall Background Services" and the new in-app uninstall. 2. New "Danger zone" → Uninstall Sutando… in Settings. NSAlert with a "Keep my settings and API keys" checkbox (default on, so reinstalls retain the user's `.env` + results). New `Uninstaller.swift` runs the teardown on a background queue: services + tmux, cloud sign-out (keychain + cloud-auth.json), skill symlinks pointing into our bundle (leaves user's own skills alone), Library caches/HTTPStorages/ WebKit/Saved Application State, optionally Application Support, `tccutil reset All com.sutando.app`. Spawns a detached /tmp/sutando-uninstall-<pid>-<rand>.sh that waits for our PID to exit, then removes the .app bundle and the Preferences plist (which the running app rewrites on quit), then deletes itself. Bundle removal is skipped when not launched from a .app, so dev users running the raw binary don't lose their checkout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4.10 Track 1 step 7 — auth-gated DMG download endpoint that streams the latest GitHub release through Vercel. Records the GITHUB_TOKEN + GITHUB_RELEASE_REPO env requirements and the new download_events table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es SUSelectedChannel
The v0.2.1 SUSelectedChannel-registration fix was a dud: Sparkle 2.9.x's
binary has zero references to that user default. Channel filtering goes
through `SPUUpdaterDelegate.allowedChannelsForUpdater:` exclusively,
which we never implemented. Sparkle was treating allowed-channels as
empty, so every item tagged <sparkle:channel>internal</sparkle:channel>
was silently filtered out and "Check for Updates" reported "you're up
to date" no matter how new the release was.
Confirmed via:
$ strings .../Sparkle.framework/Sparkle | grep -i SUSelectedChannel
(no matches)
$ strings .../Sparkle.framework/Sparkle | grep allowedChannelsFor
allowedChannelsForUpdater:
Error: -allowedChannelsForUpdater: cannot return nil. Treating this
as an empty set.
Implementation reads SUSelectedChannel from user defaults if set
(comma-separated allowed for QA: `defaults write com.sutando.app
SUSelectedChannel internal,beta`), otherwise defaults to {"internal"}
which is the channel every release we ship today is tagged with.
Note for the v0.2.1 → v0.2.3 cliff: v0.2.1 clients still have the
broken Sparkle build and cannot pull v0.2.3 in via auto-update. A
manual DMG swap is needed once; subsequent v0.2.3 → v0.2.4+ updates
will work normally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cancelTaskTool resolved tasks/ + results/ via process.cwd(), which inside the .app bundle landed at /Applications/Sutando.app/Contents/ Resources/repo/tasks/ — read-only and non-existent — so listing or cancelling tasks errored with ENOENT on a fresh install. Switch to stateDir() so it lands in $SUTANDO_HOME (the user's writable state dir), and gets auto-created on first use. Surfaced via the voice agent on a clean second-Mac install. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…unchd The Settings → Install Claude Code button runs `curl ... claude.ai/install.sh | bash`, which lands the binary at ~/.local/bin/claude. The Settings UI detects it (claudeCodePath checks ~/.local/bin) but the wrapper's PATH extension didn't include it, so `command -v claude` failed under launchd and the wrapper crash-looped before ever creating the tmux socket — the sutando-core session never came up for users without Homebrew claude. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…olve The plist sets WorkingDirectory=SUTANDO_HOME, so claude's cwd is ~/Library/Application Support/Sutando — but proactive-loop SKILL.md uses relative paths like `bash src/watch-tasks-stream.sh` and `python3 src/health-check.py`, which only work when cwd contains a `src/` tree. On a developer's box this had been masked by the LLM learning to prepend `cd ~/stando &&` to script invocations from accumulated session memory; on a fresh non-dev install (no ~/stando clone), the cd fails silently and every relative `src/`- or `skills/`-rooted command in the loop dies. Diagnosed by attaching to the .app's launchd-spawned tmux: claude was running but `pgrep -f watch-tasks` returned empty. Wrapper now symlinks src, skills, node_modules, package.json, package-lock.json, tsconfig.json, CLAUDE.md from the bundled repo into SUTANDO_HOME on every start. Idempotent — skips when a real file/dir already exists at the path so we never clobber user content. Replaces dangling symlinks so .app moves (Sparkle relocation, drag to a non-/Applications path) self-heal on next launchd restart. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dashboard.py and health-check.py had a hardcoded pgrep pattern of `src/Sutando/Sutando` left from the dev workflow. The .app's binary lives at `/Applications/Sutando.app/Contents/MacOS/Sutando`, so under the distributed install the menu bar app is reported as down even when it's running — the user just sees "✗ Sutando app not running" on the dashboard while the app is alive. Pattern broadened to `(Sutando|MacOS)/Sutando` (already used in health-check.py for the pgrep call itself; this just removes the dev- only existence gate and the stale literal in mark_stale_if_outdated). Dev binary staleness check kept gated on dev_bin existence — bundled .app and its source share a build mtime so the comparison there is always equal. Other launchd-spawned services (agent-api, voice-agent, web-client, credential-proxy, screen-capture) verified clean: their pgrep patterns are service-name-based or already-broadened, and state IO routes through util_paths state_dir/state_path so cwd-as-REPO_DIR is fine. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude Code 2.1.x shows two interactive dialogs on first launch:
1. Workspace trust ("Is this a project you trust?", default cursor on
"Yes, I trust this folder" — bare Enter accepts).
2. Bypass-permissions warning ("Yes, I accept", default cursor on
"No, exit" — Down+Enter accepts).
On an already-trusted Mac only #2 appears, which is what the previous
blind `sleep 2; send-keys Down Enter` was sized for. On a fresh Mac the
trust dialog renders first; Down on that dialog selects "No, exit",
Enter then quits claude, and launchd crash-loops the session
indefinitely. Watcher never comes up, tasks pile up unread.
Replace the single shot with a capture-pane-driven loop: identify which
dialog is visible at each tick and send the right keystroke. Exit once
two consecutive checks are dialog-free. Covers fresh-Mac (trust then
bypass), relaunch (bypass only), and the "TUI slow to render" case
the prior fixed-2s-sleep raced.
Diagnosed on a teammate's second Mac that had v0.2.5 installed but
core-agent stuck on "1. No, exit" of the bypass dialog because the
keystrokes had been spent on the trust dialog earlier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude Code 2.1.133's TUI reads stdin fast enough that `send-keys Down Enter` in a single tmux call confirms the default-highlighted "1. No, exit" before the cursor visually advances. Wrapper exits, launchd crash-loops every 30s, voice tasks never reach core. Surfaced on a fresh DMG install on a new Mac. Split into two `send-keys` calls with a 0.3s gap. Verified empirically: the cursor lands on "Yes, I accept" and claude proceeds to /proactive-loop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The macOS app's Settings tab is native Swift (SettingsWindow.swift), not the React web page — so the earlier web Settings card never showed there. This adds Slack to the native Channels section. Slack needs two tokens (xoxb- bot + xapp- app-level), but ChannelConfig modelled one envKey per channel. Generalized it: - ChannelConfig.envKey → fields: [ChannelField], each with its own env key, label, and placeholder. Discord/Telegram keep one field; Slack has two. - channelRow renders one secure field per ChannelField; editors are keyed "<channelId>/<envKey>". save/load iterate the fields. - New com.sutando.slack-bridge.plist.template — PathState-gated on ~/.claude/channels/slack/.env, same lifecycle as discord/telegram. - slack_bolt added to LaunchAgentInstaller's auto-installed pip deps. Tokens still land in ~/.claude/channels/slack/.env, which slack-bridge.py reads (b181147). Web Settings card and native card write the same file.
…he row Slack's help text is much longer than Discord's/Telegram's. The hint NSTextField had no width constraint, so its intrinsic width was the full single-line string — widening the Slack row past the 504px editors and pushing the whole block out of left-alignment with the other channels. Constrain the hint to 504 (preferredMaxLayoutWidth + width anchor) so it wraps within the editor width and every channel row is the same width.
Adopt upstream's vision capability backend: frame capture/streaming tools, the vision controller, and a localhost control server; plus vision_push.py for out-of-process frame pushes (used by bridges). Port collision fix: upstream defaults the vision control server to 7847, which the fork's terminal-server.ts already owns. Moved the default to 7848 (VISION_CONTROL_PORT) in both files. Wiring into inline-tools.ts / voice-agent.ts is deferred to CP-6 (voice-agent reconcile). Upstream PRs: sonichi#697, sonichi#715, sonichi#735.
recent_context, pidfile lock, gws-gmail-voice Reconciles upstream's voice/agent-capability features into the fork's voice-agent.ts + inline-tools.ts (both heavily fork-divergent). voice-agent.ts: - pidfile single-instance lock (sonichi#738) — duplicate processes exit before any side effect instead of stranding the vision control port - result-watcher inject re-check + 1 retry + Discord stuck-voice fallback (sonichi#924) - `[System: ...]` speech guard added to the agent prompt (sonichi#847) - vision wiring (sonichi#735) — setVisionSession + startVisionControlServer on session start, teardown on shutdown - clearActiveArtifact() on onSessionEnd (sonichi#828) inline-tools.ts: - register vision tools + artifact-cache tools - recent_context tool (sonichi#665) — reads state/voice-session-context.json via the workspace resolver; writer convention documented in CLAUDE.md - tier-aware skill-manifest loader (sonichi#702) — manifests split owner vs any_caller; integrated into the fork's cloud-skills bySkill loader New: src/artifact-cache-tools.ts, skills/gws-gmail-voice/ (sonichi#702/sonichi#704/ sonichi#705/sonichi#743), tests/active-artifact.test.ts. Vision port collision already fixed in CP-5. Deferred: sonichi#669 deictic-bias prompt line — depends on a `read_selection` tool the fork doesn't have; would instruct Gemini to call a missing tool. sonichi#791 conversation-store is its own feature (touches task-bridge / CP-7 scope), not bundled here.
… chat tracking Reconciles upstream's task-bridge.ts hardening into the fork (which had diverged with telemetry + stateDir changes). - work() dedup (sonichi#820): identical normalized task text within a 2-minute window returns the existing taskId instead of spawning a duplicate run — kills the voice over-delegation cascade. - voice-only push channel (sonichi#923): results/voice-*.txt files are spoken on the next voice turn or held in queue until reconnect; no Discord fallback. - result-watcher containment (sonichi#761): try/catch around the _pendingTasks timeout-check loop + a logging catch on the result-scan, so one throw can't silently kill the 2s watcher. - chat-path task tracking (sonichi#695): writeChatTask() helper, task-chat-* archival in the result watcher, createChatTaskTool defined (unregistered future hook), CLAUDE.md shell-snippet for core-agent self-tracking. - result-body marker docs (sonichi#689): CLAUDE.md + proactive-loop SKILL.md. sonichi#791 conversation-store stays a separate checkpoint.
…chi#791) Adopts upstream's sonichi#603 conversation-store — a time-indexed, searchable SQLite mirror written best-effort alongside the text conversation.log (text log stays primary truth; sqlite errors are swallowed). - src/conversation-store.ts (new) — node:sqlite DB helper: conversation, sessions, tool_calls, session_events tables; recordConversation / recordSessionBoundary / recordSession. - task-bridge.ts — logConversation/logSessionBoundary parallel-write the mirror; logConversation gains an optional sessionId. - voice-agent.ts — recordSession replaces the data/voice-metrics.jsonl writer; per-turn logConversation passes SESSION_ID. - conversation-server.ts (phone) — recordSession replaces the call-metrics.jsonl writer; per-call turns mirror via recordConversation. - diagnose.py — reads the sessions table (call-metrics.jsonl no longer fresh); adopted verbatim (fork was identical pre-sonichi#791). - import-conversation-log.py / import-session-metrics.py — one-shot backfill importers. query-conversation.sh — sqlite3 CLI. - engines.node >=22.5.0 (node:sqlite floor). Retired two stale voice-metrics POC scripts. Deferred: the sonichi#791 cross-node-sync doc reframes (land with CP-9) and the init-script.test.ts path fix (belongs with sonichi#913 / CP-11).
node:sqlite is a flagged-experimental builtin until Node 22.13. The
bundled .app runtime is 22.11 — a static `import` of node:sqlite threw
ERR_UNKNOWN_BUILTIN_MODULE at module load, crash-looping the voice-agent
(and taking down the conversation screen) because task-bridge.ts and
voice-agent.ts statically import conversation-store.
Switch to a type-only import + a guarded createRequire('node:sqlite')
inside init(). On a runtime without node:sqlite the mirror disables
itself (initFailed latch, no-op) — the text conversation.log and the
rest of the agent are unaffected. This is what the module's best-effort
contract always intended; a hard static import of an experimental
builtin violated it.
…ne" false alarm /tasks/active reported claude_ok via `pgrep -f "claude.*sutando-core"`, but the core agent is launched as `claude … -- /proactive-loop` inside a tmux session *named* sutando-core (src/run-core-agent.sh). The session name never appears in the claude process args, so the pattern matched nothing and the dashboard always showed "brain offline" even with a healthy core agent. Match `claude.*proactive-loop` — the command the core agent actually runs with.
screen-companion, zoom, discord-voice Adopts six upstream skills. Four are pure new directories; two touch shared core files. Clean adoptions: - deal-finder (sonichi#657), whatsapp (sonichi#771), context-drop (sonichi#902/sonichi#907) - screen-companion (sonichi#794/sonichi#801/sonichi#816) — manifest skill, auto-discovered by loadSkillManifestTools(); imports CP-5's vision-tools. Zoom move (sonichi#827): - new skills/zoom/tools.ts — summon / dismiss / join_zoom relocated out of meeting-tools.ts per the CLAUDE.md inline-tool-location rule - meeting-tools.ts stripped to joinGmeet / lookupMeetingId / callContact - inline-tools.ts re-exports the Zoom trio from the skill Discord voice (sonichi#783): - new skills/discord-voice/ — standalone TS voice-channel server (discord.js + @discordjs/voice + bodhi VoiceSession) + share-screen - package.json: +discord.js, @discordjs/voice, @discordjs/opus, libsodium-wrappers; package-lock regenerated, all deps load (incl. native @discordjs/opus) - browser-tools.ts: switch_tab description disambiguated vs share_screen Typecheck clean, 176 tests pass. README integrations-table tweak from sonichi#783 skipped (fork README diverged).
Adopts upstream's diagnostics + memory-sync work. The fork never independently modified any of these skills (only an old upstream merge touched them), so upstream's versions are adopted wholesale — clean. - self-diagnose (sonichi#768, sonichi#727): cross-node gather over SSH (gather-remote.sh), gh-resolution fix when miniconda shadows it. - cross-node-sync (sonichi#758): MEMORY.md regenerator (regenerate-memory-index.py) auto-called after sync. Also lands the sonichi#791-deferred doc reframes (SKILL.md, merge-voice-metrics.sh, setup-rsync-sync.sh). - sync-memory (sonichi#659, sonichi#764, sonichi#755): scripts/sync-memory.sh — memory+notes rsync; default ~/.sutando/memory-sync/ with legacy auto-migration; .env sourced from workspace root. - docs/memory-sync.md, scripts/stage-readiness.sh. - .gitignore: !tests/*.test.sh so the new shell tests are tracked. npm test still 176 pass. New shell tests (sync-memory-migration, gather-remote) aren't wired into `npm test` (it runs .test.ts + .py only) — sync-memory-migration verified manually, 5/5 pass. README integrations tweak skipped (fork README diverged).
Small isolated fixes across inline/browser tools + skills. inline-tools.ts: - type-text (sonichi#660/sonichi#661/sonichi#664): clipboard-paste for non-ASCII; UTF-8 locale + stdin pbcopy (launchd locale fix); `append` flag. - press_key (sonichi#673): uparrow/downarrow/arrowup/... aliases — bare names fell through to literal keystroke. - cancel-task (sonichi#663): writes a CANCEL_INSTRUCTION task into the pipeline instead of just unlinking; CLAUDE.md gets the matching handler note. browser-tools.ts: - scroll (sonichi#672): amount param (small/medium/large → 150/400/800px). - open_url (sonichi#721/sonichi#919): reuse active tab on same-origin; reject whitespace / zero-width chars early; query-redacted failure logging. skills: - screen-record (sonichi#662/sonichi#667/sonichi#668): mic preference, silent-audio warning, ffmpeg stderr capture — adopted, fork's bundled-ffmpeg (bare `ffmpeg`) re-applied. - tts (sonichi#691): quote-strip in synthesize.sh (openai-tts + gemini-tts). - morning-briefing (sonichi#690): gws calendar instead of removed script. - schedule-crons (sonichi#732): example cron points at scripts/sync-memory.sh. Typecheck clean, 176 tests pass.
…-api First slice of CP-11 (app/startup/init). Heavy CX checkpoint — the self-contained pieces: - watch-tasks (sonichi#693/sonichi#736): retire one-shot watch-tasks.sh; CLAUDE.md documents the Monitor + watch-tasks-stream.sh pattern. sonichi#736 (SUTANDO_WORKSPACE TASKS_DIR) already satisfied — fork's watch-tasks-stream.mjs honors it. - startup.sh (sonichi#698): macOS 15+ Screen Recording denial detection (silent black-PNG → size gate); gate screen-capture launch on PERM_OK. - startup.sh (sonichi#780): git committer attribution from stand-identity.json. sonichi#911 (LOGS_DIR redirect) already done in the fork. - agent-api.py + dashboard.py (sonichi#728): absolute /usr/bin/{pgrep,git,pmset} for launchd-spawnable code paths. sonichi#729 (/logs/voice path) already satisfied — fork uses state_path(). - launchd (sonichi#914): health-check fallback log → $WORKSPACE/logs. - start-cli (sonichi#688): tmux mouse-scroll. schedule-crons SKILL.md, tmux-status test: adopted. Deferred to CP-11 part 2: init.sh sonichi#913 (workspace runtime dirs) + the matching tests/init-script.test.ts fix, main.swift sonichi#694/sonichi#699 (pause-loop submenu, Timer cadence), Info.plist sonichi#807. 176 tests pass.
…tcher cadence Finishes CP-11 of the sonichi/sutando → AG2Platform/stando upstream sync. Applied surgically — the fork diverged from upstream on every touched file. - sonichi#913 init.sh: workspace-rooting was already done in the fork; ported only the missing one-time migrate_legacy_runtime_state() sweep, adapted to the fork's STATE_ROOT + repo/workspace split (notes/, build_log.md, pending-questions.md stay repo-rooted — cross-fleet-shared). - sonichi#694 main.swift: Pause Loop submenu (30m / 1h / indefinite) via shared writePauseSentinel(); new pauseSentinelActive() guard added to the bodies of refreshContextualChips() and runHealthCheck() so paused windows also skip startup one-shot fires. - sonichi#699 main.swift: checkWatcher Timer 30s → 300s; dropped the now-dead 120s lastWatcherAlert throttle. - sonichi#807: no change needed — app/Info.plist already declares NSAppleEventsUsageDescription and build-app.sh seals it at codesign. - tests/init-script.test.ts: adopt a separate .workspace dir (closes the deferred sonichi#791 test fix) + 3 new legacy-migration tests. Verified: bash -n + swiftc -parse clean; npm test 179/179 pass; init.sh migration smoke-tested (moves stale state, idempotent on second run). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-homes the four web-client.ts features the fork lost when src/web-client.ts was deleted for the React client. Surgical port — upstream's inline-HTML implementations rebuilt as React components/hooks against the fork's client/ structure. - sonichi#650 markdown: marked + dompurify added; lib/chat-markdown.ts renders sanitized markdown in ConversationStream bubbles (finalized user/assistant turns only — system pills + interim streams stay plain text). Separate from lib/markdown.ts, which is notes-trust-only and must not render untrusted chat content. - sonichi#826 capabilities: static "What I can do" panel on the idle conversation screen (CapabilitiesPanel atom + const-values/capabilities.ts). - sonichi#651 subscriptions: subscription-scanner skill ported into skills/ (it was missing from the repo); web-server.ts gains GET /paidsubscriptions/data + localhost-gated POST /paidsubscriptions/scan; new /subscriptions React route with summary cards + sortable table. - sonichi#735 vision: web-server.ts /vision/* proxy to the voice-agent control server (127.0.0.1:7848); useVision hook drives getDisplayMedia capture + JPEG frame loop; Watch pill in the top bar + bottom-right VisionPreview. - sonichi#804 /presenter: excluded per the user — not ported. Verified: pnpm client build clean; tsc --noEmit clean; npm test 179/179. Live integration testing (vision streaming, scan task pickup) needs the full voice-agent stack — see .claude/5-19-2026-sync/09-manual-test-guide.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…11-12-app-webclient # Conflicts: # .gitignore # CLAUDE.md # README.md # package-lock.json # package.json # skills/cross-node-sync/scripts/regenerate-memory-index.py # skills/phone-conversation/scripts/conversation-server.ts # skills/screen-record/scripts/record.py # skills/subscription-scanner/SKILL.md # src/Sutando/main.swift # src/agent-api.py # src/archive-stale-results.py # src/browser-tools.ts # src/cartesia-tts.ts # src/dashboard.py # src/discord-bridge.py # src/event_log.py # src/github-webhook.py # src/health-check.py # src/init.sh # src/inline-tools.ts # src/screen-capture-server.py # src/session-handoff.sh # src/slack-bridge.py # src/startup.sh # src/task-bridge.ts # src/telegram-bridge.py # src/vision-tools.ts # src/vision_push.py # src/voice-agent.ts # src/voice-context.ts # src/watch-tasks-stream.sh # src/web-client.ts # tests/agent-state-endpoint.test.ts # tests/slack-bridge-tier-map.test.py
Upstream sync phase 2 — CP-4 through CP-7 + Slack settings UI
Upstream sync phase 3 — CP-8–CP-12, conversation-store, and the upstream merge
- docs/features.md — full catalog of Sutando capabilities after the CP-0–CP-12 upstream sync (surfaces, built-in tools, 42 skills, vision, memory, observability, architecture). - docs/skill-test-plan.md — concrete trigger/expected checklist for live-testing the synced features.
docs: feature reference + skill test plan
check_core_proactive_loop() read core-status.json from the workspace root, but PR sonichi#940 relocated it to state/core-status.json (see CLAUDE.md "Work Status" and the proactive-loop step-0 instruction). The check therefore read a stale pre-relocation orphan at the root and emitted a "core-proactive-loop ... last heartbeat > 600s ago" warning on every pass — a permanent false positive that masked the real signal. Point status_path at WORKSPACE_DIR / "state" / "core-status.json". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /status and /answer handlers resolved pending-questions.md via
personal_path("pending-questions.md", REPO_DIR). personal_path tries
$SUTANDO_MEMORY_DIR/machine-<host>/<file> then <workspace>/<file> — but
the workspace arg was REPO_DIR (the repo checkout root), not the real
workspace. The file lives at <workspace>/pending-questions.md, so it
resolved to a non-existent path and /status always returned an empty
`questions` array. The web UI Questions panel was therefore always
empty, hiding every pending question from the user.
Resolve the workspace via resolve_workspace() (same as
check-pending-questions.py) and pass it to both call sites. Repo-bundled
public assets (stand-avatar.png, stand-identity.json) keep REPO_DIR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The core-status.json relocation fix (this branch) made check_core_proactive_loop read WORKSPACE_DIR/state/core-status.json, but health-check-stuck-loop.test.py still wrote its fixtures to the workspace root — so the check saw no file and returned "ok / not yet written" instead of the expected "warn" (case e failed). Point the write_status() helper and the malformed-JSON case at state/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…path fix(workspace): correct core-status.json and pending-questions.md paths
The upstream-sync merge (#38) added @anthropic-ai/claude-agent-sdk to package.json but regenerated package-lock.json (npm) instead of pnpm-lock.yaml — the repo's actual lockfile. CI's `pnpm install --frozen-lockfile` then failed with ERR_PNPM_OUTDATED_LOCKFILE. Regenerated pnpm-lock.yaml via `pnpm install`; frozen-lockfile install now passes.
fix(ci): update pnpm-lock.yaml for @anthropic-ai/claude-agent-sdk
`regenerate-memory-index.py:default_memory_dir()` derived the Claude Code project slug from `Path(__file__).resolve().parent` walked up four levels. When `skills/` is an app-bundle symlink (the normal install layout), `.resolve()` follows it into `/Applications/Sutando.app/.../repo` — so the memory dir pointed at a location with no frontmatter files, `collect_entries` returned 0 entries, and the clobber guard turned the regen into a silent no-op on every cross-node-sync cron pass (every 7 min). Fix: - Derive the slug from `resolve_workspace()` (the canonical workspace contract: $SUTANDO_WORKSPACE / ~/.sutando/workspace), never from __file__. - `_slugify_path()` now replaces every non-alphanumeric char (the old code replaced only `/`, leaving the dot in `.sutando` and producing a slug that did not match Claude Code's actual project directory). Adds tests/regenerate-memory-index-workspace-resolution.test.py (3 cases: slugify, env-var precedence, workspace-not-__file__ resolution). All pass; `py_compile` clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Collaborator
Author
|
Retargeting to AG2Platform/stando fork (proactive-loop PRs land on the fork). Reopening there. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
skills/cross-node-sync/scripts/regenerate-memory-index.py→default_memory_dir()derived the Claude Code project slug fromPath(__file__).resolve().parentwalked up four levels.When
skills/is an app-bundle symlink (the normal install layout),.resolve()follows it into/Applications/Sutando.app/Contents/Resources/repo. The slug then pointed at an empty memory dir →collect_entries()found 0 frontmatter files → the clobber guard made the regen a silent no-op on every cross-node-sync cron pass (every 7 min).This is the exact
Path(__file__).resolve()anti-pattern CLAUDE.md andworkspace_default.pyalready document.Found during a proactive-loop pass when the cron-driven regen refused to run:
The 4 memory files all had valid frontmatter — the script was just looking in the wrong directory.
The fix
resolve_workspace()(the canonical workspace contract:$SUTANDO_WORKSPACE/~/.sutando/workspace), never from__file__._slugify_path()now replaces every non-alphanumeric char. The old code replaced only/, leaving the dot in.sutando— producing-Users-john-.sutando-workspaceinstead of the real-Users-john--sutando-workspace.$SUTANDO_MEMORY_DIRenv override is unchanged (still priority 1).resolve_workspace(migrate=False)— read-only regen, no migration side effects.Tests
tests/regenerate-memory-index-workspace-resolution.test.py— 3 cases: slugify (dot+slash), env-var precedence, workspace-not-__file__resolution incl. an explicit assert that the result never lands inSutando.app.python3 -m py_compileclean--memory-dir) now writes 4 entries instead of hitting the clobber guard.🤖 Generated with Claude Code