feat: AgentField Desktop (Mac-first) + Windows enablement, bundled CLI, tray, deep links#752
Merged
Conversation
Docker-Desktop-style dashboard for non-technical users: shows control plane health (GET /health) and the locally installed agent nodes from ~/.agentfield/installed.yaml, cross-checked against GET /api/v1/nodes for a running/stopped/unknown badge per agent. Polls every 5s with a manual Refresh button and graceful empty states. Electron + electron-vite + React + TypeScript, plain CSS. Secure defaults: contextIsolation on, nodeIntegration off, sandboxed renderer, single contextBridge API. All data access is isolated in src/main/agentfield.ts with a marked seam to later swap the registry read to `af list -o json`. 29 vitest unit tests cover registry parsing, health mapping, and badge derivation. Self-contained under desktop/ (own package.json); no packaging (electron-builder) yet, and the GUI is untested in this headless environment — typecheck, production build, and unit tests all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the stale commented-out windows block with a real agentfield-windows-amd64 build mirroring the linux/darwin ones (goreleaser appends .exe on its own). Groundwork only: the release workflow's build matrix filters by --id and does not build this id yet; shipping the artifact needs a follow-up matrix entry (windows runner, or mingw-w64 on the linux runner for the CGO sqlite dependency). Also modernizes archives.builds/format to ids/formats so `goreleaser check` passes clean again (both were deprecated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`af stop` used process.Signal(os.Interrupt) and a signal-0 probe, both unsupported on Windows (and os.FindProcess always succeeds there, so the liveness check was meaningless). Extract the two process operations into build-tagged helpers: proc_unix.go keeps the existing SIGINT + signal-0 behaviour; proc_windows.go uses taskkill for the graceful request and a tasklist PID query for liveness. stop.go changes are limited to swapping the two call sites and the now-unused syscall import. Windows paths are compile-verified only (GOOS=windows cross-build), not yet tested on a real Windows machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`af logs` shelled out to tail(1), which does not exist on Windows. The tail/follow commands now go through one tailCommand helper: unchanged tail(1) invocations on Unix, PowerShell Get-Content -Tail (-Wait for follow) on Windows, with proper single-quote escaping of the log path. Windows path is compile-verified only, not yet run on a real machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Go agent nodes declare unix-style binary paths in their manifests (entrypoint.start: bin/foo). On Windows the install-time `go build -o` output now carries the conventional .exe extension, and the runner's GoBinaryProgram resolves an extensionless start path to the built .exe when present. No behaviour change on other platforms; windows path is compile-verified only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
…ed helpers The patch-coverage gate flagged the runtime.GOOS-gated windows branches (PowerShell tail construction in logs.go, .exe naming/resolution in gointerp.go) as untestable on linux CI. Extract each into a pure helper taking an explicit goos string — tailCommandArgs, withExeSuffixFor, goBinaryProgramFor — with the exported wrappers passing runtime.GOOS, so behavior is unchanged while both platform paths are unit-testable anywhere. Table-driven tests cover the windows tail command (incl. single-quote escaping), .exe suffixing, and the built-.exe fallback resolution; refactored regions now profile with zero uncovered blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p on Windows control-plane/agentfield.yaml was a checked-in symlink to config/agentfield.yaml. On Windows checkouts (core.symlinks=false, the default) git materializes it as a plain text file containing the literal target path, which Viper then finds via its . search path and fails to parse: `cannot unmarshal !!str config/...` — a fatal error on every `af server` run from the control-plane directory. The symlink is redundant on every platform: both cmd/af and cmd/agentfield-server already search ./config and <execDir>/config, which resolve to the same file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The af binary only filled in DatabasePath/KVStorePath when storage.mode was absent entirely. A config file that sets mode: "local" while leaving the paths empty (which the repo's own sample config/agentfield.yaml does, and which af picks up automatically when running next to it) skipped the defaulting block and failed startup with "database path is empty". Default the paths whenever the effective mode is local, mirroring cmd/agentfield-server which already had this shape. Found on Windows but platform-independent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
exec.LookPath alone cannot pick a Python on Windows: stock machines ship Microsoft Store "app execution alias" stubs named python3.exe (and often python.exe) that resolve like real binaries but exit 9009 without running anything. resolveVenvInterpreter took the first PATH hit and only version-checked that one, so a node declaring requires-python failed with "no compatible interpreter" even when a perfectly good python was next in line. Verified live on Windows 11: python3 -> dead Store stub (exit 9009), python -> real 3.11.9, never consulted. ambientPythonInterpreter now probes each candidate by actually running it (-c version query) and takes the first that answers. The candidate list gains "py", the Windows launcher: python.org installers register it even when "add python to PATH" is left unchecked (the default), where it is the only working entry. It does not exist on Unix, so probing it there is a no-op. The legacy no-requires-python path in InstallPythonDependencies now uses the same probe instead of blindly running python3 -m venv and falling back, and fails with an actionable error listing the probed candidates when nothing on PATH runs. Test stubs that previously only handled -m venv now answer the -c version probe, matching how real interpreters behave; the venv-creation-failure contracts now expect the earlier, more actionable error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spawned agents log through stdout/stderr redirected to a log file. On Windows, Python then encodes with the legacy ANSI code page (cp1252), which cannot represent the SDK's emoji log prefixes - every heartbeat flooded the log with UnicodeEncodeError tracebacks from the logging module. Verified live: with PYTHONUTF8=1 the same agent logs cleanly. Applied in both spawn paths (services.buildProcessConfig, which backs af run, and the legacy packages runner). An explicit PYTHONUTF8 in the caller's environment wins. UTF-8 mode is a no-op where UTF-8 is already the default, so this is safe on every platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dows dev_service.go is build-tagged !windows (Windows gets a stub service), but two pieces of its test surface were not: - mockFileSystemAdapter lived in dev_service_test.go (!windows) while untagged test files (package_service_test.go, coverage_additional_ test.go) use it. Moved to a new untagged mocks_fs_test.go. - Three tests in the untagged coverage_additional_test.go exercised Unix-only DefaultDevService methods (loadDevEnvFile, startDevProcess, port helpers) that the Windows stub does not define. Moved into the !windows-tagged dev_service_test.go. Before this, go vet / go test of internal/core/services failed to compile on Windows. Also carries the one-line stub update from the interpreter-probe change (the fake python in coverage_additional_test.go now answers the -c version query); no other test logic changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…control plane
checkControlPlane treated any HTTP 200 from {baseUrl}/health as
"healthy", so anything squatting on the popular default port lit the
dashboard green. Found live on Windows: an unrelated dev server answering
{"status":"alive"} on /health showed as a running control plane.
The probe now recognizes an AgentField control plane by its health
payload shape (status: healthy|unhealthy, per routes_core.go). Anything
else reachable on the port reports recognized: false with an explanatory
error, renders as a yellow "Another service is on this port" state, and
is excluded from the nodes cross-check so a foreign /api/v1/nodes
response cannot corrupt agent badges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Data layer growth behind the same single-snapshot IPC: - fetchExecutions parses GET /api/ui/v2/workflow-runs into in-flight runs plus a short tail of finished ones. - fetchDashboardMetrics parses GET /api/ui/v1/dashboard/summary (agents running/total, runs today/yesterday, success rate). - Both are only consulted on a recognized control plane, so a foreign service on 8080 can't inject activity or metrics. Install flow, with the af CLI as the single contract: - src/shared/catalog.ts is a curated hard-coded list of installable nodes (the pre-marketplace seam; swap for a remote catalog fetch later). Entry names must equal the node's manifest name — that is the registry key the app uses to detect installed state (SWE-AF installs as "swe-planner"). - src/main/installer.ts spawns `af install <source>`, sanitizes ANSI/spinner output into displayable lines, and streams them to the renderer over agentfield:install-progress. The renderer only ever sends catalog names over IPC; unknown names are refused, raw sources never reach a shell. A missing af CLI degrades to an actionable message. Verified live on Windows: installed SWE-AF from the app end-to-end — including uv provisioning Python 3.12.13 because the node requires >=3.12 and the ambient interpreter is 3.11 (the interpreter-probing fix working in production). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native-feeling chrome: - No default File/Edit/View menu bar anywhere. macOS keeps the minimal app menu it needs for Cmd+Q/copy-paste; Windows/Linux drop the bar entirely (Menu.setApplicationMenu(null)). - Seamless titlebar: hiddenInset + sidebar vibrancy + traffic-light inset on macOS, hidden titlebar with the native control overlay on Windows. The sidebar rail and view header are draggable regions. - System font stack, light/dark from the OS, hairline borders. Layout: left sidebar (Dashboard / Agents / Activity / Install + a control-plane status pill pinned at the bottom), right content view. The Dashboard leads with stat tiles (agents running, executing now, runs today vs yesterday, success rate) over a recent-activity list; Agents and Activity render as clean row panels; Install streams per-row progress. macOS-specific chrome is behind platform guards and still needs one smoke run on a real Mac; everything else verified live on Windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`npm run dist` produces DMG+zip on macOS and a one-click NSIS installer on Windows into release/ (git-ignored); `npm run dist:dir` for a quick unpacked smoke. electron-builder is pinned to v25 — v26 requires require(ESM) support (Node 20.19+/22.12+) that older Node 22 lacks. Unsigned for now, default Electron icon; signing/notarization and a real icon come before distribution. Verified on Windows: the packaged AgentField.exe boots and opens its window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the default Electron icon everywhere. scripts/make-icons.mjs renders the brand mark (the exact outlined "af" + dot paths from the web UI logo, so no font dependency) via an offscreen Electron window into: - build/icon.icns — a real ICNS container (Apple-grid margins, baked shadow) that electron-builder ships on macOS - build/icon.png — the 1024px source electron-builder converts to the Windows/Linux app icon (verified: the packaged exe carries the mark) - resources/icon.png — runtime window/taskbar icon for win/linux - resources/tray/* — tray glyphs at 1x/1.5x/2x, active/inactive crossed with light/dark-taskbar variants; the gold dot doubles as the status light Outputs are committed (`npm run icons` regenerates), resources/** now ships inside the app package, and desktop/build is un-ignored for exactly the two icon files the root .gitignore would otherwise drop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tray (Windows/Linux only — macOS has af-tray, installed with AgentField itself): a status glyph whose brand dot goes gold while the control plane runs, tooltip + disabled menu row naming the state (running / unhealthy / port-in-use / stopped), Open AgentField / Open web UI / Quit. Closing the window now hides to the tray, Docker-Desktop style; presentation logic is pure in tray-model.ts and unit-tested. If tray creation fails (some Linux desktops) the app logs why and keeps classic quit-on-close. Deep links: the app registers the agentfield:// scheme (declared for macOS via electron-builder `protocols`, HKCU-registered at runtime on Windows) and holds the single-instance lock — a relaunch or an agentfield://dashboard|agents|activity|install URL focuses the running app and switches it to that view. Parsing is pure in shared/deeplink.ts (unit-tested); the view union now lives there as the one canonical list. Verified live on Windows (packaged build): deep link cold-open, deep link into a tray-resident app, view switching, single-instance focus, close-to-tray, and the port-in-use state against a foreign service squatting on 8080. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The macOS menu-bar tray now prefers the AgentField desktop app when it is installed: every "open" action first tries the agentfield:// deep link for the equivalent view (dashboard/agents/activity) and falls back to the web UI in the browser. Detection is the deep link itself — `open agentfield://…` exits non-zero fast when nothing registered the scheme, so there is no separate probe to drift. Page→view mapping and the browser fallback are pure helpers in shared.go with contract tests; the darwin file only gains the two-line try/fallback. assets/appicon.icns was a renamed 512px PNG, not an ICNS container — Finder/dock could show a generic icon. It is now a real icns (PNG members at 32…1024 on Apple's grid), generated by the same desktop/scripts/make-icons.mjs that produces the desktop app's icons, so both apps wear the same mark. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replacing the icon and context menu on every 5s poll churns native tray APIs for nothing and can dismiss a menu the user has open on Windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swept the org: a repo is installable iff it has agentfield-package.yaml at its root (the manifest `af install` requires) — that held for four of fifteen repos, and the catalog now carries all of them: swe-planner (SWE-AF), pr-af, sec-af, and cloudsecurity-af, each keyed by its manifest `name:` so installed-state detection keeps working. The sweep rule is documented at the top of catalog.ts for the next addition. Required secrets (e.g. OPENROUTER_API_KEY) are resolved at `af run` time, not install time, so installs from the app stream cleanly and setup prompts happen where a terminal exists. Verified in the packaged app: all four render, swe-planner shows Installed ✓. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agents view: Start / Stop / Restart per row, shelling out to the af CLI (`af run` / `af stop`; restart is stop-then-run — the CLI has no restart verb). Names are validated against installed.yaml before anything is spawned; the renderer only ever sends names. Settings view: open at login (OS login item, packaged builds only — launches hidden with --hidden, tray-only), start the control plane automatically, and per-agent auto-start switches. Persisted to settings.json in userData, normalized on load so hand-edits and old shapes can't break the app. Autostart on every launch (src/main/autostart.ts, planning pure and unit-tested): spawn `af server` detached (logs to ~/.agentfield/logs/control-plane.log, same file macOS launchd uses) only when nothing answers on the port — never over a live control plane or a foreign service — then start the selected agents. Agents whose registry entry went stale (running with no control-plane presence, e.g. after a reboot; Windows never reconciles that live) are restarted, not skipped. The point: agents are already answering when Claude/Codex/anything queries them — nobody has to start a server first. Also fixes a deep-link race this surfaced: a link that cold-starts the app pushed the view at did-finish-load, before React subscribed, and got dropped. The renderer now announces readiness and collects the pending view (agentfield:renderer-ready), so agentfield://settings into a hidden app lands on Settings. Verified live on Windows (packaged build): start/restart/stop from the UI (badge, port, registry all agree), settings round-trip, login-item registration with --hidden, hidden tray-only launch, autostart bringing a stopped agent up on relaunch, and the port-in-use guard skipping the control-plane start while a foreign service owns 8080. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Non-technical users install ONLY the desktop app and get all of AgentField: the package now carries the af CLI (extraResources from vendor/, staged by `npm run bundle-cli` or the release pipeline). Every launch resolves which af to drive (src/main/cli.ts): managed (~/.agentfield/bin — the same location the curl installer uses, so the two installers converge instead of double-installing) → PATH → bundled. A copy older than MIN_AF_VERSION is skipped — the app runs on its bundled CLI meanwhile, and Settings grows an "AgentField CLI" card showing version + source with an Update button that installs the bundled copy into the managed location (never over a newer one; dev builds are trusted). With no CLI anywhere, first launch auto-provisions ~/.agentfield/bin (agentfield + af alias, curl-installer naming), copes with a running binary via rename-aside, and registers the Windows user PATH so terminals get `af` too. Launches also run `af skill install --non-interactive` (Settings toggle, default on) so detected coding agents — Claude Code, Codex, Gemini … — always know how to use AgentField; skillkit's state file makes this idempotent and shared with the curl installer. Verified live on Windows from a simulated fresh machine (no managed copy, no PATH af): first launch provisioned both binaries, registered the user PATH, installed the skill into ~/.claude/skills, resolved to the managed copy, and drove agent start/stop through it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two blind spots closed: - The desktop app had no CI at all — its tests only ever ran on a dev machine. New desktop.yml runs typecheck, vitest, and an unsigned electron-builder package on macos-14 and windows-latest (a stub in vendor/ validates the bundled-CLI extraResources wiring), so the platform-guarded chrome and packaging config prove out on both ship targets per PR. - cmd/af-tray's darwin implementation is CGO code that Linux CI never compiles (it builds the !darwin stub, and the CGO_ENABLED=0 matrix can't touch it) — a type error in tray_darwin.go would only surface at release time. control-plane.yml grows a macos-14 job that builds and vets the tray, wired into required-checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…af ps Callers had no way to ask "what is running right now": GET /api/v1/executions and /executions/active were 404, batch-status needs execution IDs the caller must have tracked, and the data lived only on the UI-internal /api/ui/v2/workflow-runs. Live-tested tonight with three concurrent agent runs, the answer required scraping the UI API. GET /api/v1/executions/active returns every run with at least one non-terminal execution: run_id, root execution, target agent.reasoner, active/total execution counts, full status_counts, started_at and latest_activity (the wedge tell — active>0 with stale latest_activity means a run is likely dead, not working). Filters: agent_id, session_id, limit. `af ps` is the CLI face of the same endpoint. Storage: ExecutionFilter.ActiveOnly filters runs AFTER aggregation (HAVING on the active-count) so a surviving run's terminal children still show in status_counts — a Status="running" pre-filter both loses those rows and misses queued-only runs. AgentNodeID now also applies to QueryRunSummaries; the agentic query surface always accepted the filter but silently ignored it. Also fixes the auth-level hole that made agentic discover useless on default installs: with no API key configured, the middleware never set auth_level, so getAuthLevel fell back to "public" and FilterByAuth hid every api_key endpoint — discover returned zero results for every query in local mode. No-auth callers now get api_key level, matching their actual access. agentfield-use skill 0.1.0 → 0.2.0, rewritten from tonight's fresh-agent test findings: no-jq discovery fallback (fresh Windows boxes), corrected discovery-lists-inactive-agents claim (filter health_status=="active"), concurrency guidance (fire async calls together, ~3-4 heavy runs per node), af ps / executions/active, batch-status result-size warning (terminal entries embed full payloads — never pass through argv), and a wedge protocol: stale latest_activity + quiet logs → cancel-tree (NOT plain cancel, which orphans children) → restart agent → re-submit. Test flake fix: workflow_execution_events_test asserted CompletedAt.After(StartedAt), which fails when both events land within one coarse timer tick (Windows ~15ms); now asserts !Before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeQL (go/uncontrolled-allocation-size, 3 high) flagged the result slice/map pre-allocations in QueryRunSummaries: their capacity is filter.Limit, which flows straight from request input, so a single call with limit=1<<30 would try to allocate gigabytes before reading a row. The allocations predate this PR (#46) — the scanner surfaced them here because this branch touched the function. Every live caller already clamps its page size (UI ≤200, agentic ≤100, af ps ≤200), but the storage layer now enforces its own ceiling (maxRunSummaryLimit=1000) instead of trusting callers. Regression test issues the query with Limit=math.MaxInt32. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the request limit CodeQL still flagged the three allocations after the previous clamp: it does not treat reassignment-style clamps (x = max when x > max) as sanitizers — the identical clamps in every HTTP caller were being ignored the same way. Instead of arguing with the guard recognizer, sever the flow: capacity now derives from totalRuns (the query's own COUNT, not caller input) clamped to maxRunSummaryLimit. Semantically better too — never reserve more than the query can return. The limit clamp stays; it bounds the SQL LIMIT itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas
marked this pull request as ready for review
July 15, 2026 01:02
…nalling blind A "running" registry entry outlives reboots and crashes, and stop treated it as fact: os.FindProcess/kill on the recorded PID, /shutdown to the recorded port, no verification that either still belongs to the agent. After a reboot that meant (a) a dead PID errored out BEFORE the registry was reset, so stop-then-start flows — the desktop restart button and login autostart both abort when stop fails — wedged permanently, and (b) a reused PID or port could get an unrelated process signalled or shut down. Stop now verifies before it acts: a dead PID reconciles the record to "stopped" and succeeds; a recorded port whose /health answers as a DIFFERENT node marks the whole record stale and signals nothing (the live PID is almost certainly reused); a process that exits between the aliveness check and the kill (os.ErrProcessDone) counts as stopped. The identity probe reuses the packages.HealthNodeID/NodeIDsEquivalent helpers introduced for start readiness. Tests: dead-PID reconciliation (cross-platform via test-binary re-exec), foreign-node port never receives /shutdown and its process survives, and the two ops assertions that had encoded the old error behavior now assert reconciliation. Existing shutdown-path tests grew /health handlers for the new probe. Found by an independent pre-merge review (Codex). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two gaps between the endpoint's documented contract ("every run with a
non-terminal execution, with complete per-run status counts") and the
SQL:
- The active set was running/pending/queued/waiting — but paused and
unknown are canonical non-terminal statuses too. A pause-wedged run
(the exact failure this surface exists to expose) would vanish from
af ps. The ActiveOnly HAVING clause now matches
types.IsTerminalExecutionStatus; the handler's active_executions is
computed from status_counts the same way, deliberately leaving the
aggregation's narrower pre-existing ActiveExecutions column alone —
the UI's status derivation still depends on it.
- The agent_id filter was a row-level WHERE applied before GROUP BY, so
a cross-agent run lost other agents' rows from its counts, lost its
root fields when the root ran elsewhere, and disappeared entirely when
its only execution on the filtered agent was already terminal. It is
now run-level membership (run_id IN (...)): every run that touched the
agent, aggregated over ALL of its rows.
Tests cover a paused-only run and a cross-agent run whose agent-filtered
result keeps complete counts and the foreign root.
Found by an independent pre-merge review (Codex).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es; pin spawn port The release pipeline bundled an UNSTAMPED af: bundle-cli.mjs ran plain `go build`, so every shipped binary answered `Version: dev`. The app trusts unparseable versions and prefers the managed copy, so the first launch provisioned a dev binary into ~/.agentfield/bin that would win over every future release forever — no min-version gate, and Settings could never offer an update (both sides must be parseable). Upgrades were structurally broken. - bundle-cli.mjs stamps main.version/commit/date exactly like goreleaser: AF_CLI_VERSION (release.yml now passes the tag) → `git describe --tags` → dev fallback. - The selection floor is the bundle's own stamped version when it has one — the app needs at least the features it shipped with — falling back to MIN_AF_VERSION for unstamped dev bundles, so the constant no longer has to predict release numbers. - Against a stamped bundle, dev-versioned managed/PATH copies are superseded (chosen becomes the bundle, which then re-provisions the managed location). Unstamped bundles keep trusting them, so source-build dev workflows are unchanged. - The spawned `af server` is pinned to port 8080 — the port the app polls. Without it a config-file custom port made the server bind elsewhere while the app spun on 8080 forever. Custom-port/authed control planes remain unsupported by the app (documented follow-up). Found by an independent pre-merge review (Codex). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # sdk/python/agentfield/harness/_cli.py
Users should never have to re-download the app from the releases page by hand. The public repo's releases feed is the update channel: packaged builds poll /releases/latest shortly after launch and every 4 hours (stable releases only — RC prereleases never surface; an RC install IS offered the stable build of its own version once it lands, via a prerelease-aware version compare), and a newer release shows an "Update available" banner across the top of the window plus a Settings → App updates card. - Install downloads this platform's installer asset (matched precisely: AgentField-Setup-*.exe / *.dmg — releases also carry goreleaser CLI archives) with streamed progress, then hands off: Windows quits into the NSIS one-click installer (replaces the app in place, relaunches); macOS opens the DMG for a drag-install (silent replacement needs signed builds — known follow-up). No installer asset for the platform falls back to the release page. - Dismissing the banner persists for that version only (settings.dismissedUpdateVersion): the next release brings it back, and Settings keeps offering the update regardless. - 404 from /releases/latest (no stable release yet) reads as up to date, not an error. Dev builds never auto-check (static package.json version); the manual check in Settings works everywhere. - main/updates.ts is electron-free (side effects injected by index.ts) and covered by 21 unit tests; the check path was also smoke-tested against the live GitHub API (found v0.1.108, correctly reported its lack of installer assets and the release-page fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeQL (js/incomplete-url-substring-sanitization) flagged the updater
tests' fake fetch for routing on includes('api.github.com'). It is a
stub, not sanitization, but hostname comparison is the canonical safe
pattern and costs nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas
added a commit
to Agent-Field/SWE-AF
that referenced
this pull request
Jul 15, 2026
…ory selector (#96) The installer now supports subdirectory installs (Agent-Field/agentfield#752): `af install <repo>//go`. This ships the manifest that section "Deployment: no af install (yet)" was waiting for. - go/agentfield-package.yaml: node swe-planner-go, language go, entrypoint build ./cmd/swe-planner -> bin/swe-planner, same user_environment contract as the Python root manifest. NODE_ID and PORT are deliberately not declared: manifest defaults are appended after the runner's env, so a PORT default would override the installer's port assignment (found the hard way). - go/go.mod: the local `replace` for the SDK pointed outside the package and made any installed copy unbuildable. The SDK is now required by pseudo-version at the exact commit go/Dockerfile already pins (AGENTFIELD_SDK_REF 795bdd57), so the module resolves anywhere. Dev workspaces can still layer a local checkout via go.work. - go/README.md: deployment section rewritten around the new install path. Verified end to end on Windows: install -> go build at install time -> af run (registers 30 reasoners, honors the assigned port) -> af uninstall (clean removal). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…processes A macOS app launched from Finder/Dock inherits launchd's minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin), not the user's shell PATH. The af CLI itself still resolved (absolute paths), but everything af shells out to — go, uv, python3, claude, codex — was unfindable, killing installs, runs, and skill sync on a normal double-click launch. Windows never surfaced this: GUI apps inherit the full user PATH there. New main/env.ts resolves the real PATH once per launch (login shell probed with sentinel markers and a 3s cap, merged with process.env.PATH and the well-known bin dirs, deduped in that priority order; win32 passes through untouched) and every child spawn now uses childEnv(). The version probe in cli.ts and the secrets CLI runner are wired here; the remaining spawn sites ride with their own commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ck, surfaced DMG errors The updater polled the monorepo's /releases/latest and offered any newer tag as an app update. But that release train also ships CLI-only releases (wheels, goreleaser binaries, af-tray) — v0.1.108 has no desktop installer at all — so every fresh install saw a phantom 'Update available' whose only action was a 'View release' browser link, the manual flow in-app updates exist to replace. Same on Windows and macOS. A release is now only an app update when it carries this platform's installer asset; the browser fallback and its 'View release' label are gone. macOS asset selection is arch-aware (exact -arm64/-x64 match → suffix-less universal → any .dmg; .blockmap never matches), and shell.openPath's error-string result — it never rejects — is checked and routed to status.error instead of being dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ct spawn
Once af-tray installs its launchd agents, two owners could start the
control plane: the app's detached 'af server' spawn and launchd's
ai.agentfield.server (RunAtLoad + KeepAlive={SuccessfulExit:false}). On a
net-new first launch the app spawns first, then the tray installs, and
launchd's copy fails against the held resources and relaunch-loops every
~10s for the whole session (observed live: runs climbing, last exit 1).
startControlPlane now probes launchctl for the server agent on darwin and
kickstarts it when loaded — one owner, launchd — falling back to the
original direct spawn when the agent is absent (pre-tray first launch) or
kickstart fails. Spawns inherit the resolved user PATH (main/env.ts).
The other half of the fix — the server exiting clean when a healthy
control plane already answers — lands control-plane-side.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A desktop-app-only install had no menu-bar icon: af-tray only shipped via
the curl installer. The app now carries af-tray (bundle-cli builds it into
vendor/ on darwin, stamped like the CLI) and provisions it the way it
provisions af: staged into ~/.agentfield/bin/af-tray — the managed location
both installers converge on — then 'af-tray install' builds the
~/Applications bundle and launchd agents.
Re-staging is version-gated via 'af-tray version' (dev-stamped copies are
trusted, mirroring cli.ts), and 'af-tray install' only runs when the binary
changed or its launchd agent isn't loaded — install reloads launchd via
bootout+bootstrap and would blink the tray on every launch otherwise. A new
macOS-only Settings toggle ('Show the menu bar icon', default on) drives
it; off runs 'af-tray uninstall'. Pure logic in tray-companion.ts, DI'd and
unit-tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or lines The Install view gains an 'Install from repository' box: paste https://github.com/<owner>/<repo> (or the //subdir selector form) and the app runs 'af install <source>' with the same streamed progress, mutex, and registry convergence as catalog installs. Both platforms. This deliberately relaxes the renderer-sends-catalog-names-only rule for exactly one channel; the compensating control is strict main-process shape validation (parseRepoSource): https://github.com/ prefix required, owner/ repo/subdir character classes with no leading dashes, no whitespace, query strings, fragments, or .. traversal — an accepted value can never read as a CLI flag. 23 accept/reject cases in tests. Also unwraps the af CLI's zerolog JSON error lines into their human message before display — an install failure now reads 'invalid package structure: …' instead of a wall of raw JSON. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gin item, scroll, valid ad-hoc signature Wiring and the remaining macOS fixes, verified on a real Mac (the README's 'needs one smoke run' debt is paid): - open-url can fire before app.whenReady() on a cold-start deep link; building a BrowserWindow then throws. navigate() now stashes the view until ready. Verified: 'open agentfield://dashboard' cold-starts clean. - Login items: args:['--hidden'] is Windows-only. darwin uses openAsHidden/ wasOpenedAsHidden (best-effort; SMAppService may ignore them on macOS 13+, documented), and setLoginItemSettings is skipped when state is unchanged — unsigned apps outside /Applications log 'Operation not permitted' on every launch otherwise. - Settings (any tall view) never scrolled: .view-body is a flex column, so flex-shrink squeezed children to fit instead of overflowing into the scrollbar. Children now keep natural height. - Unsigned packages carried only the linker's ad-hoc signature — no sealed resources — which codesign/spctl reject as INVALID and which blocks normal launches. New afterPack hook gives identity-less builds a full ad-hoc signature (no-op once real signing lands). mac.icon pinned; index.ts wires env/tray-companion/install-source/updater arch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lready answers
Under a supervisor, a second 'agentfield server' racing an incumbent is not
an error — the desired state (control plane up) is already true. But
startup died non-zero on whichever shared resource it hit first (in local
mode that's the BoltDB file lock during storage init, well before the port
bind), and af-tray's launchd agent uses KeepAlive={SuccessfulExit:false}:
non-zero exit meant a relaunch loop, throttled ~10s, for as long as the
incumbent lived. Observed live on a net-new install where the desktop app
started the server before the tray's launchd agent was bootstrapped.
New startguard: on any server create/start failure, probe /health (2s cap)
and accept only AgentField's payload shape — status healthy plus non-empty
version and checks, so a foreign 200 on the port is rejected. A healthy
incumbent logs 'control plane already running on port N — nothing to do'
and exits 0, which SuccessfulExit:false correctly reads as a clean stop.
Everything else keeps the non-zero exit. Both entry points (af server and
agentfield-server) guard both their create and start paths.
Verified live: with the launchd server healthy on :8080, a second
'af server --open=false' fails storage init, logs the honest line, exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…row filtering GET /api/v1/executions/active?session_id=X drove the documented wedge heuristic, but for any run doing its work in local child calls it reported active=1, total=1, latest_activity frozen at dispatch for the whole run — observed across a real 27-minute pr-af-go.review whose executions table held 21 progressively-updated rows the entire time. Root cause: workflow execution events carry no session_id, so child rows persist with it empty; only the dispatch-path root has one. The SessionID filter in QueryRunSummaries applied at the ROW level, dropping every session-less child before the GROUP BY and collapsing each run to its root — whose updated_at only moves on completion. The filter now selects runs by membership (run_id IN (SELECT run_id … WHERE session_id = ?)), the same shape the AgentNodeID filter above it already uses for exactly this reason. Verified live: a session-scoped poll over a fresh review run now shows total_executions growing 2→7, active 2→4→2, latest_activity advancing with the agent's log — where the identical query previously sat frozen at 1/1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd empty discovery
Found by running the skill as written, as a coding agent, with no outside
docs. Two places it lied or broke:
- It said 'af ls' lists installed agents. 'af ls' lists REASONERS — against
a control plane with nothing running it prints {"reasoners":[],…} and a
fresh agent concludes nothing is installed. The registry command is
'af list'; all three references corrected, and the cheat sheet now names
both commands for what they are.
- The jq-less discovery snippet crashed on a machine with zero registered
agents ('capabilities' is null, not []). Null-guarded.
Both synced copies updated (skills/ and skillkit's embedded skill_data).
The rest held up under a real end-to-end run — discovery, async execute,
active-poll, and the wedge protocol's log-check correctly cleared a
legitimately silent 27-minute run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # sdk/python/tests/test_harness_provider_opencode.py
env.ts is a darwin/linux-only concern by design (win32 early-returns the process PATH everywhere), but its pure helpers leaned on the HOST path module: mergePaths defaulted to path.delimiter and wellKnownBinDirs used host join. On the windows-latest Desktop CI leg that merged POSIX PATHs with ';' and minted '\home\me\...' dirs, failing 6 env tests that (correctly) assert POSIX output. Pin the module to posix.delimiter / posix.join and strip host join from the test assertions. Suite green on a real Windows host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas
enabled auto-merge
July 15, 2026 22:30
AbirAbbas
disabled auto-merge
July 15, 2026 22:30
AbirAbbas
added a commit
that referenced
this pull request
Jul 16, 2026
…licts) Conflict resolutions: - agent_commands.go: union requires_auth — keep #784's reasoners endpoint alongside the executions steering endpoints - commands/install.go: keep both --json (this PR) and --path (#750); thread path through executeJSON so the flags compose - logs.go: keep runtime import from main - stop.go: keep JSON-aware printf/Outcome from this PR plus main's ErrProcessDone guard and markStopped refactor; route main's new stale-registry warnings through the quiet-aware printer so --json stdout stays machine-readable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
AgentField Desktop + Windows enablement
A Mac-first desktop companion (Docker-Desktop analog) plus the Windows fixes that make the whole stack run natively there. Everything below is runtime-verified on real Windows 11 (packaged builds, not dev mode); macOS is compile/package/test-verified in CI on macos-14 and needs one manual smoke (checklist at the bottom).
The app
Views — Dashboard (stat tiles + recent activity; success rate rounded and tone-colored), Agents (status badges + Start / Stop / Restart per row), Activity (in-flight runs with live pulse + recent tail), Install (curated catalog with one-click
af install+ streamed progress, an Update button for installed agents that reinstalls latest and restores run state, installed-state carried in the row title, and a confirm-guarded Uninstall: swe-planner, pr-af, sec-af, cloudsecurity-af plus the Go ports pr-af-go and swe-planner-go via the new//subdirinstaller selector), Secrets (the whole store — every key with scope + which agents declare it, revoke with an all-agents warning), Settings.Installer:
//subdirsource selector (control plane) —af install <repo>//<subdir>[@ref]installs a package whose manifest lives in a subdirectory, so repos shipping a Python root and a Go port side by side (pr-af, SWE-AF) are fully installable; registry provenance now records the source string verbatim (fixes the doubled@ref@refbug), andaf uninstallalso removes the node's node-scoped secrets. SWE-AF's missinggo/agentfield-package.yamlis contributed in Agent-Field/SWE-AF#96 (includes un-breaking itsgo.modlocal replace via an SDK pseudo-version pin).Mac-first chrome — no menu-bar clutter (minimal app menu on macOS only), hiddenInset titlebar + traffic-light inset + sidebar vibrancy (macOS) / native control overlay (Windows), system font, light/dark from the OS, brand "•af" icon set rendered from the web-UI logo paths (real ICNS, exe icon, window icon, tray glyphs —
npm run iconsregenerates).Autopilot (the point: agents are already answering when Claude/Codex asks) —
af-trayas its menu-bar companion.agentfield://dashboard|agents|activity|install|settingsdeep links + single instance; the macOSaf-traynow opens the desktop app via deep link when installed and falls back to the web UI (the failedopenis the detection).Agent keys (secrets) — agents declaring
user_environmentin their manifest get a Keys button + a "Needs keys" chip while something required is unresolved; Start in that state opens an inline editor instead of failing with "missing required environment variables". The editor mirrors the manifest (required /require_one_ofgroups / optionals with defaults) with per-var resolution status, password inputs fortype: secret, and Revoke. Everything rides the af CLI against its encrypted store (af secrets ls/set/rm— the storeaf rundecrypts into the agent process): values piped over stdin, validated against the manifest regex, written to the manifest-declared scope, never read back into the UI.Bundled CLI — the package carries
af(extraResources;npm run bundle-clistages it), version-stamped like goreleaser (AF_CLI_VERSIONfrom the release tag →git describe→ dev). Per-launch resolution: managed~/.agentfield/bin(same location/names as the curl installer → no double install) → PATH → bundled. The version floor is the bundle's own stamped version (the app needs at least the features it shipped with; MIN_AF_VERSION is the fallback for unstamped dev bundles), with an "Update AgentField" button in Settings. Against a stamped bundle, stale dev-versioned copies are superseded and re-provisioned — otherwise a dev binary the app once installed would win over every future release forever. Fresh machines get auto-provisioned (both binary names, Windows user PATH registered). Launch also installs both bundled skills (af skill install <name> --non-interactive, toggleable) so detected coding agents always know AgentField.App updates (in-app) — the app updates itself from the public GitHub releases: packaged builds poll
/releases/latestafter launch and every 4 hours (stable releases only — RC prereleases are never offered, and an RC install is offered the stable build of its own version once it lands). A newer release shows an "Update available" banner across the top plus a Settings → App updates card. Install downloads this platform's installer asset with streamed progress (matched precisely against the release's mixed assets:AgentField-Setup-*.exe/*.dmg, never the goreleaser CLI archives) and hands off — Windows quits into the NSIS one-click installer (replaces in place, relaunches); macOS opens the DMG for a drag-install (silent replacement needs signed builds). No installer asset → the release page opens instead. Dismissing the banner is per-version and persisted; Settings keeps offering the update. 21 unit tests + a live-API smoke run.New skill: agentfield-use (skillkit)
The embedded skill catalog gains a second skill.
agentfieldteaches building multi-agent systems;agentfield-useteaches using the agents installed on this machine: health check → capability discovery (with the colon-vs-dotinvocation_targetgotcha) → sync/async execution → polling/SSE, session headers, failure triage (af secrets setfor missing keys,af runfor stopped agents), and the VC-chain audit trail. Grounded in the verified HTTP surface — it deliberately omits endpoints that don't exist. Skills now carry per-skill trigger sentences in marker-block targets;install.shinstalls both.Also fixes a shared runner bug: a manifest declaring only
require_one_ofgroups skipped env resolution entirely (nothing injected, unsatisfied groups never errored). Both paths now includeRequireOneOfin the guard, with a regression test.The skill was then battle-tested end-to-end (a fresh coding agent driving installed agents through it, including two concurrent pr-af-go PR reviews that both succeeded on per-PR workspaces) and rewritten to 0.2.0 from what the test hit: a no-
jqdiscovery fallback for fresh Windows boxes, corrected discovery semantics (it lists dead agents too — filterhealth_status == "active"), concurrency guidance (fire async calls together; ~3–4 heavy runs per node),af ps/executions/activeas the in-flight answer, a batch-status warning (terminal entries embed full result payloads — write to file, never argv), and a wedge protocol (stalelatest_activity+ quiet logs →cancel-tree, not plain/cancel, which orphans children → restart → re-submit).In-flight visibility: GET /api/v1/executions/active + af ps (control plane)
That same test found there was no way to ask "what is running right now":
GET /api/v1/executionswas a 404, batch-status needs execution IDs the caller already tracked, and the data lived only on the UI-internal/api/ui/v2/workflow-runs. NewGET /api/v1/executions/activereturns every run with ≥1 non-terminal execution — run_id, root execution,agent.reasonertarget, active/total counts, full status_counts, started_at, and latest_activity (the wedge tell: active > 0 with stale latest_activity means dead, not working) — filterable byagent_id/session_id;af psis its CLI face. Registered in the apicatalog + knowledge base so agents can discover it.Storage-side:
ExecutionFilter.ActiveOnlyfilters runs post-aggregation (HAVING on the active count), so a surviving run's terminal children still show in status_counts and queued-only runs aren't missed — both of which astatus=runningpre-filter gets wrong. "Active" means every canonical non-terminal status — includingpaused(a pause-wedged run must not vanish fromaf ps) andunknown. Theagent_idfilter is run-level membership (every run that touched the agent, aggregated over all its rows), so cross-agent runs keep complete counts and their root fields;AgentNodeIDpreviously didn't apply to run summaries at all (the agentic query surface accepted it but silently ignored it).QueryRunSummariesalso clamps its limit before pre-allocating results (CodeQLgo/uncontrolled-allocation-size, 3 high — pre-existing since #46, surfaced by this diff).Also fixes agentic discover being dead on every default install: with no API key configured, the auth middleware never set an auth level, so discover's filtering treated local callers as "public" and hid every
api_keyendpoint — zero results for every query in local mode. No-auth callers now getapi_keylevel, matching their actual access.Live-verified on Windows: three concurrent async runs visible in flight (count 3 → 0 on completion), all pre-existing routes intact, unauthenticated discover returns results.
Windows enablement (control plane)
control-plane/agentfield.yamlremoved (materializes as a broken text file on Windows and kills viper).cmd/af/main.go).python3stub answering exit 9009) — run-probes candidates instead of LookPath, with uv provisioning verified in production during a real SWE-AF install.PYTHONUTF8=1for spawned agents (cp1252 + emoji log spam), on both spawn paths.af-tray (macOS)
Deep-links into the desktop app with browser fallback;
assets/appicon.icnswas a renamed PNG, now a real ICNS wearing the same mark.CI added
desktop.yml: typecheck + 103 vitest tests + unsigned electron-builder package on macos-14 and windows-latest.control-plane.ymlgains tray-darwin (macos-14go build+go vetofcmd/af-tray) — previously the darwin CGO tray was only compiled at release time.End-to-end run on Windows (and the five bugs it caught)
A full loop was executed against a live control plane (
af serveron :8090; :8080 was owned by an unrelated service), ending in a real LLM-backedswe-planner.planrun that succeeded: keys stored via the desktop editor (real OpenRouter key) →af run swe-planner(registered, 30 reasoners in discovery) → asyncsmoke-agent.demo_echothrough the CP (succeeded, 6ms) →swe-planner.planon a small FastAPI repo → 10 minutes of multi-role LLM work (PM → architect → tech lead → sprint planner) through the control plane, PRD + architecture + sprint issues written to disk. Peeling that onion caught five real Windows bugs, all fixed + regression-tested in this PR:net.Listen(":8001")succeeds while uvicorn is LISTENING there);af runassigned smoke-agent the port swe-planner was already on. Availability now also dial-probes.waitForAgentNodeaccepted any 200 — smoke-agent was declared "started successfully" on the strength of swe-planner's health response. It now verifies the health payload'snode_id— best-effort: a responder whose health payload carries nonode_id(custom health endpoints, and currently the Go SDK's) still passes permissively; tightening the Go SDK payload is a follow-up.create_subprocess_execdoes no PATHEXT resolution, so npm-installed provider CLIs (opencode/codex/gemini are.cmdshims on Windows) always raised FileNotFoundError. Now resolved viashutil.which..cmdshim; on Windows opencode now gets the prompt over stdin (run_cligrewinput_text, fed concurrently with the output drains).os.killpgon Windows (python SDK): the watchdog kill path crashed with AttributeError; now guarded, falls through toproc.kill().Not exercised: SWE-AF's
build/executephases and PR-opening (needs a real GH token + target repo — GH_TOKEN is stored as a placeholder).Agent health no longer wedges at "unknown" (control plane)
Running, traffic-serving agents could report
health_status: "unknown"forever — deterministically for Go SDK nodes, intermittently for Python ones (visible as a permanent "Unknown" badge in the desktop app and missing nodes in/api/v1/nodes' default filter). Root cause was structural, in the status state machine, and is fixed for every SDK at once:starting→activeopen as a "pending" transition, soStatestayedstarting(persisted as healthunknown) even as the same update set lifecycleready. The Go SDK renews its status lease every 2 minutes — exactlyMaxTransitionTime— so every renewal re-created the pending transition right before the timeout sweeper could rescue it. A state claim always arrives with its evidence (ready lease renewal, heartbeat, health-check result), so transitions now complete immediately.POST /heartbeatenrolled a node in HTTP health monitoring; the Go SDK's keep-alive isPATCH /nodes/:id/status. The lease handler now enrolls nodes too (serverless excluded).HealthMonitor.RegisterAgentis called per DB-updating heartbeat and reset tracking tounknowneach time; it's now idempotent for an unchanged BaseURL.needsReconciliationgained a rule for healthunknown+ fresh heartbeat past the startup grace, so rows wedged by pre-fix control planes promote without a server restart.Live-verified on Windows: a fresh
af stop && af run swe-planner-gocycle reachesactivein under 5 seconds; before the fix the identical flow sat atunknownfor 100+ minutes. All four parts regression-tested.Known gaps / notes
af serveron :8080 (a realaf serverwas e2e'd on :8090; the don't-fight-the-port guard covers :8080), the "unhealthy control plane" visual state, and the gold/active tray glyph on a live control plane. Logic is unit-tested; these fall out of any run against a free :8080.Release pipeline doesn't publish desktop artifacts yet.Shipped:release.ymlnow builds and attaches desktop installers to every staging/production release — macOS DMG + zip (Apple Silicon) and Windows NSIS.exe(x64), each carrying an embedded-UI af CLI built from the release commit. Intel-mac installers and signing remain follow-ups.The embedded skill teaches building on AgentField; anShipped in this PR.agentfield-useskill (using installed agents) is a queued follow-up.pause_cascaderegistration followed by silence — child "running" with no harness process; the same payload succeeded before and after): detection and recovery tooling ship in this PR (af pslatest_activity +cancel-tree); the SDK root-cause fix is tracked separately.http://localhost:8080, no auth. A config-file custom port or an API-key-protected control plane is not supported by the desktop UI yet (the app's ownaf serverspawn is pinned to 8080 so it can never bind where the app isn't looking; a control-plane URL/key setting is the follow-up). The README documents this.desktop/**, so desktop-only merges produce no installers; desktop CI packages with a stub CLI (the real CGO binary is only built at release time). An installed-app (DMG/NSIS) end-to-end smoke — Gatekeeper/SmartScreen, bundled CLI execution, N→N+1 upgrade, reboot autostart — hasn't been run. The post-publish installer build also means a just-published release briefly has no installer assets — the in-app updater degrades to its "View release" fallback during that window (another reason to reorder the pipeline).af stopreconciliation, the non-terminal/pausedactive set + run-level agent filter on/executions/active, and the bundled-CLI version stamping above.macOS smoke checklist
open agentfield://agents,open agentfield://settings— window focuses and switches view.af servercomes up (port 8080 free), tray dot on the af-tray side goes active.cmd/af-tray, run it, click "Open Dashboard" — should open the desktop app (not the browser); delete the app and it should fall back to the web UI.af secrets lsshould show them; Revoke puts it back.ls ~/.claude/skillsshould show bothagentfieldandagentfield-use.macOS hardening & feature round (July 15)
The macOS side is now exercised end-to-end on a real Mac — full teardown → DMG install → first-launch provisioning → live agent runs. What landed:
Fixes (verified live on macOS):
af's subprocesses (go,uv,claude…) were unfindable. Newmain/env.tsresolves the login-shell PATH once and feeds every spawn.open-urlbeforewhenReady()crashed on BrowserWindow construction — now stashed until ready.--hiddenarg is Windows-only; darwin usesopenAsHidden(macOS 13+ caveat documented), and no-opsetLoginItemSettingscalls are skipped.shell.openPatherrors surface..view-body's flex children were squeezed to fit instead of overflowing into the scrollbar (all platforms).agentfield serverexits 0 when a healthy control plane already answers — killing the launchd relaunch loop observed on net-new installs./executions/activesession filter: row-levelsession_idfiltering collapsed runs to their root (children carry no session_id) —latest_activitysat frozen through a 27-min run. Now run-membership based; verified live.Features:
af-trayinto~/.agentfield/binand runs its installer — a desktop-only install now gets the menu-bar icon. Settings toggle to remove it.https://github.com/org/repo[//subdir]URL in the Install view; strict main-process shape validation compensates for the one raw-source IPC channel. Both platforms.swe-planner-gocatalog entry restored (feat(go): af install via the //go subdirectory selector SWE-AF#96 merged).agentfield-useskill corrections found by driving it blind as a sub-harness (af ls→af list, null-capabilities guard).Test count: desktop 221 (from 141); new Go table tests for startguard + run-summary session filtering. Known pre-existing failure
TestStartAndStopCoverAdditionalBranches(internal/server) fails at branch base too — not from this work.Follow-ups filed elsewhere: Agent-Field/pr-af#55 (silent empty-diff), #56 (diff-only verifier greps agent cwd), #57 ($0 cost tracking).
🤖 Generated with Claude Code