Skip to content

feat: AgentField Desktop (Mac-first) + Windows enablement, bundled CLI, tray, deep links#752

Merged
AbirAbbas merged 66 commits into
mainfrom
feat/desktop-app
Jul 15, 2026
Merged

feat: AgentField Desktop (Mac-first) + Windows enablement, bundled CLI, tray, deep links#752
AbirAbbas merged 66 commits into
mainfrom
feat/desktop-app

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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 //subdir installer selector), Secrets (the whole store — every key with scope + which agents declare it, revoke with an all-agents warning), Settings.

Installer: //subdir source 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@ref bug), and af uninstall also removes the node's node-scoped secrets. SWE-AF's missing go/agentfield-package.yaml is contributed in Agent-Field/SWE-AF#96 (includes un-breaking its go.mod local 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 icons regenerates).

Autopilot (the point: agents are already answering when Claude/Codex asks) —

  • Open at login (hidden, tray-only) · start the control plane when nothing is listening · per-agent auto-start; stale post-reboot registry entries get restarted, not skipped.
  • Windows/Linux tray with status glyph (brand dot goes gold when the control plane runs), close-to-tray; macOS keeps af-tray as its menu-bar companion.
  • agentfield://dashboard|agents|activity|install|settings deep links + single instance; the macOS af-tray now opens the desktop app via deep link when installed and falls back to the web UI (the failed open is the detection).

Agent keys (secrets) — agents declaring user_environment in 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_of groups / optionals with defaults) with per-var resolution status, password inputs for type: secret, and Revoke. Everything rides the af CLI against its encrypted store (af secrets ls/set/rm — the store af run decrypts 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-cli stages it), version-stamped like goreleaser (AF_CLI_VERSION from 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/latest after 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. agentfield teaches building multi-agent systems; agentfield-use teaches using the agents installed on this machine: health check → capability discovery (with the colon-vs-dot invocation_target gotcha) → sync/async execution → polling/SSE, session headers, failure triage (af secrets set for missing keys, af run for 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.sh installs both.

Also fixes a shared runner bug: a manifest declaring only require_one_of groups skipped env resolution entirely (nothing injected, unsatisfied groups never errored). Both paths now include RequireOneOf in 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-jq discovery fallback for fresh Windows boxes, corrected discovery semantics (it lists dead agents too — filter health_status == "active"), concurrency guidance (fire async calls together; ~3–4 heavy runs per node), af ps / executions/active as the in-flight answer, a batch-status warning (terminal entries embed full result payloads — write to file, never argv), and a wedge protocol (stale latest_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/executions was 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. New GET /api/v1/executions/active returns every run with ≥1 non-terminal execution — run_id, root execution, agent.reasoner target, 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 by agent_id/session_id; af ps is its CLI face. Registered in the apicatalog + knowledge base so agents can discover it.

Storage-side: ExecutionFilter.ActiveOnly filters 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 a status=running pre-filter gets wrong. "Active" means every canonical non-terminal status — including paused (a pause-wedged run must not vanish from af ps) and unknown. The agent_id filter 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; AgentNodeID previously didn't apply to run summaries at all (the agentic query surface accepted it but silently ignored it). QueryRunSummaries also clamps its limit before pre-allocating results (CodeQL go/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_key endpoint — zero results for every query in local mode. No-auth callers now get api_key level, 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)

  • Checked-in symlink control-plane/agentfield.yaml removed (materializes as a broken text file on Windows and kills viper).
  • Local-mode storage path defaulting fixed (cmd/af/main.go).
  • Python interpreter probing that survives Microsoft Store aliases (python3 stub answering exit 9009) — run-probes candidates instead of LookPath, with uv provisioning verified in production during a real SWE-AF install.
  • PYTHONUTF8=1 for spawned agents (cp1252 + emoji log spam), on both spawn paths.
  • Services-package test compile fix under GOOS=windows.

af-tray (macOS)

Deep-links into the desktop app with browser fallback; assets/appicon.icns was 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.yml gains tray-darwin (macos-14 go build + go vet of cmd/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 server on :8090; :8080 was owned by an unrelated service), ending in a real LLM-backed swe-planner.plan run that succeeded: keys stored via the desktop editor (real OpenRouter key) → af run swe-planner (registered, 30 reasoners in discovery) → async smoke-agent.demo_echo through the CP (succeeded, 6ms) → swe-planner.plan on 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:

  • Port probe blind on Windows (control plane): bind-only availability checks miss live listeners (no SO_EXCLUSIVEADDRUSE — net.Listen(":8001") succeeds while uvicorn is LISTENING there); af run assigned smoke-agent the port swe-planner was already on. Availability now also dial-probes.
  • Readiness check trusted squatters (control plane): waitForAgentNode accepted any 200 — smoke-agent was declared "started successfully" on the strength of swe-planner's health response. It now verifies the health payload's node_idbest-effort: a responder whose health payload carries no node_id (custom health endpoints, and currently the Go SDK's) still passes permissively; tightening the Go SDK payload is a follow-up.
  • Harness CLIs unfindable (python SDK): create_subprocess_exec does no PATHEXT resolution, so npm-installed provider CLIs (opencode/codex/gemini are .cmd shims on Windows) always raised FileNotFoundError. Now resolved via shutil.which.
  • Prompts too long for cmd.exe (python SDK): the prompt-as-argv convention blows cmd.exe's ~8k cap through a .cmd shim; on Windows opencode now gets the prompt over stdin (run_cli grew input_text, fed concurrently with the output drains).
  • os.killpg on Windows (python SDK): the watchdog kill path crashed with AttributeError; now guarded, falls through to proc.kill().

Not exercised: SWE-AF's build/execute phases 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:

  • Pending transitions never completed: the StatusManager held starting→active open as a "pending" transition, so State stayed starting (persisted as health unknown) even as the same update set lifecycle ready. The Go SDK renews its status lease every 2 minutes — exactly MaxTransitionTime — 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.
  • Lease-renewing nodes were never health-polled: only POST /heartbeat enrolled a node in HTTP health monitoring; the Go SDK's keep-alive is PATCH /nodes/:id/status. The lease handler now enrolls nodes too (serverless excluded).
  • Monitor state reset on every heartbeat: HealthMonitor.RegisterAgent is called per DB-updating heartbeat and reset tracking to unknown each time; it's now idempotent for an unchanged BaseURL.
  • Reconciler sweep for already-wedged rows: needsReconciliation gained a rule for health unknown + 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-go cycle reaches active in under 5 seconds; before the fix the identical flow sat at unknown for 100+ minutes. All four parts regression-tested.

Known gaps / notes

  • Not yet live-exercised on Windows (blocked by a foreign service owning :8080 during testing): autostart actually spawning af server on :8080 (a real af server was 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.
  • The NSIS/DMG installers build but haven't been installed end-to-end (testing used unpacked builds).
  • Unsigned packaging; signing/notarization before distribution. Release pipeline doesn't publish desktop artifacts yet. Shipped: release.yml now 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; an agentfield-use skill (using installed agents) is a queued follow-up. Shipped in this PR.
  • One intermittent Python-SDK wedge observed under load (a pause_cascade registration 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 ps latest_activity + cancel-tree); the SDK root-cause fix is tracked separately.
  • The app assumes the default control plane: 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 own af server spawn 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.
  • Release-pipeline gaps to fix in a follow-up: the desktop installers build after the GitHub release is published (an installer failure post-dates the release); the release workflow's path trigger excludes 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).
  • An independent model review (Codex) of the full diff drove a hardening round before merge: stale-registry af stop reconciliation, the non-terminal/paused active set + run-level agent filter on /executions/active, and the bundled-CLI version stamping above.

macOS smoke checklist

cd desktop && npm ci
npm run bundle-cli        # needs Go; add `-- full` for embedded web UI (build web/client first)
npm run dist              # DMG + zip in release/ (unsigned: right-click → Open)
  1. Chrome: traffic lights inset over the sidebar, vibrancy, no File/Edit menus beyond the minimal app menu, light/dark.
  2. Deep links (after first launch): open agentfield://agents, open agentfield://settings — window focuses and switches view.
  3. Settings → AgentField CLI card: with a curl-installed af it should say "installed in ~/.agentfield"; on a clean account it should auto-provision the bundled copy.
  4. Autostart: quit everything, toggle "Start the control plane automatically", relaunch → af server comes up (port 8080 free), tray dot on the af-tray side goes active.
  5. af-tray → desktop handoff: build 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.
  6. Install + lifecycle: install an agent from the catalog, Start/Restart/Stop it from Agents.
  7. Keys: on the swe-planner row, click Start with nothing configured — the Keys editor should open ("Needs keys") instead of failing; set GH_TOKEN + one LLM key, watch the chip clear, af secrets ls should show them; Revoke puts it back.
  8. Skills: after first launch, ls ~/.claude/skills should show both agentfield and agentfield-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):

  • Finder-launch PATH: GUI apps inherit launchd's minimal PATH; af's subprocesses (go, uv, claude…) were unfindable. New main/env.ts resolves the login-shell PATH once and feeds every spawn.
  • Cold-start deep links: open-url before whenReady() crashed on BrowserWindow construction — now stashed until ready.
  • Login items: --hidden arg is Windows-only; darwin uses openAsHidden (macOS 13+ caveat documented), and no-op setLoginItemSettings calls are skipped.
  • Update flow: releases without this platform's desktop installer (CLI-only releases — all of them, today) are no longer offered as app updates; the "View release" browser fallback is gone; macOS DMG selection is arch-aware; shell.openPath errors surface.
  • Settings scroll: .view-body's flex children were squeezed to fit instead of overflowing into the scrollbar (all platforms).
  • Valid ad-hoc signatures: identity-less builds carried only a linker signature (codesign/spctl-invalid); an afterPack hook now full ad-hoc signs (no-op once real signing lands).
  • Control-plane single ownership: the app kickstarts the launchd server agent when present instead of double-spawning; server-side, agentfield server exits 0 when a healthy control plane already answers — killing the launchd relaunch loop observed on net-new installs.
  • /executions/active session filter: row-level session_id filtering collapsed runs to their root (children carry no session_id) — latest_activity sat frozen through a 27-min run. Now run-membership based; verified live.

Features:

  • af-tray provisioning (macOS): the app bundles and provisions af-tray into ~/.agentfield/bin and runs its installer — a desktop-only install now gets the menu-bar icon. Settings toggle to remove it.
  • Install from repository: paste a 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.
  • Readable install errors (zerolog JSON unwrapped), swe-planner-go catalog entry restored (feat(go): af install via the //go subdirectory selector SWE-AF#96 merged).
  • agentfield-use skill corrections found by driving it blind as a sub-harness (af lsaf 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

AbirAbbas and others added 5 commits July 10, 2026 15:20
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>
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 86.80% 87.40% ↓ -0.60 pp 🟡
sdk-go 92.50% 92.00% ↑ +0.50 pp 🟢
sdk-python 93.82% 93.73% ↑ +0.09 pp 🟢
sdk-typescript 90.44% 90.42% ↑ +0.02 pp 🟢
web-ui 84.75% 84.79% ↓ -0.04 pp 🟡
aggregate 85.51% 85.75% ↓ -0.24 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 568 80.00%
sdk-go 0 ➖ no changes
sdk-python 0 ➖ no changes
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

AbirAbbas and others added 22 commits July 10, 2026 16:21
…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>
AbirAbbas and others added 4 commits July 14, 2026 17:07
…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
AbirAbbas marked this pull request as ready for review July 15, 2026 01:02
@AbirAbbas
AbirAbbas requested a review from a team as a code owner July 15, 2026 01:02
AbirAbbas and others added 5 commits July 14, 2026 22:30
…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>
Comment thread desktop/src/main/updates.test.ts Fixed
Comment thread desktop/src/main/updates.test.ts Fixed
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>
AbirAbbas and others added 11 commits July 15, 2026 15:46
…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
AbirAbbas enabled auto-merge July 15, 2026 22:30
@AbirAbbas
AbirAbbas disabled auto-merge July 15, 2026 22:30
@AbirAbbas
AbirAbbas merged commit 9b03715 into main Jul 15, 2026
38 checks passed
@AbirAbbas
AbirAbbas deleted the feat/desktop-app branch 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants