* docs(spec): feed-publishing + macOS signing design (#2220)
Design for the electron-updater feed-publishing workstream plus the macOS
code-signing half of Track B, shipped as one PR so a single review unblocks
auto-update. Sidecar-only post-matrix join job emits latest*.yml / nightly*.yml
+ .blockmap sidecars on all three platforms (no maker changes, no artifact
mutation). macOS signing reproduces the proven local runbook in CI: keychain
provisioning, hardened-runtime entitlements, and notarization via the App Store
Connect API key (.p8) path, with the osxNotarize cast fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(plan): feed-publishing + macOS signing implementation plan (#2220)
Six TDD tasks: blockmap wrapper, feed module (selection + yml), osxNotarize
API-key rewire, macOS signing-setup composite action, and the two workflow
wirings (latest + nightly feed jobs). Two evidence-based simplifications vs the
spec: no custom entitlements plist (default osxSign entitlements are proven by
the local runbook) and no Node bump (CI already below the crash ceiling).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(feed): blockmap sidecar wrapper over app-builder-lib
Adds frontend/scripts/blockmap.mjs isolating the single fragile internal
import (app-builder-lib/out/targets/blockmap/blockmap.js) behind a thin
writeBlockmap(filePath) wrapper. Returns { sha512, size } only, omitting
blockMapSize to force the sidecar differential path in electron-updater.
Test added with // @vitest-environment node directive (required: project
vitest config defaults to jsdom; same pattern as nightly-version.test.mjs).
TDD: red on missing module, green after implementation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(feed): installer selection + electron-updater yml assembly
Implement selectInstallers, feedFilename, and buildYml pure functions
for packaging versioned installers and generating platform-specific
feed metadata. Excludes ao-start aliases and deb/rpm packages.
Generates blockmap sidecars and yml with deprecated top-level
path/sha512 for compatibility, no blockMapSize (forces sidecar diff).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sign): notarize via App Store Connect API key; drop osxNotarize cast
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ci): macOS signing-setup composite action
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ci): sign macOS + publish latest feed in the release workflow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ci): sign macOS + publish nightly feed in the nightly workflow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* docs(spec): auto-update with stable + nightly channels
Design spec for channel-aware auto-update via electron-updater: stable
(manual semver releases) and nightly (daily CI cron, version
X.Y.(Z+1)-nightly.<UTC-ts>+<sha>). Covers runtime wiring, the feed,
the version scheme and its ordering/channel rationale, the nightly
pipeline, in-app UX (opt-in + channel + nightly disclaimer), error
handling, testing, and the deferred/prereq items (CI signing for macOS
updates, stable version stamping, polished UI in #2207).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(plan): auto-update channels implementation plan
Bite-sized TDD plan for the in-app channel-aware updater + nightly pipeline:
nightly version compute module, daily cron workflow, macOS feed metadata,
~/.ao update-settings module, electron-updater shell + main.ts wiring, and the
first-run opt-in/channel/nightly-disclaimer prompt. Deferred items and Track-B
prereqs (CI signing, stable stamping) are called out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(release): nightly version compute module
computeNightlyVersion -> X.Y.(Z+1)-nightly.<UTC-ts>+<sha>: next-patch base,
fixed-width UTC timestamp for monotonic prerelease ordering, sha as build
metadata. Pure ESM so CI runs it and vitest tests it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(release): daily nightly build + publish workflow
Cron computes the nightly version from the latest stable tag, stamps it,
builds all platforms, and publishes a prerelease (nightly channel). Skips
when HEAD is already covered by the latest nightly. forge publisher prerelease
flag is now env-driven (AO_RELEASE_PRERELEASE) so stable stays non-prerelease.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(release): pass nightly version via env, not inline interpolation
Avoids interpolating ${{ }} into the run script (GitHub Actions injection
hardening); the version now arrives via the NIGHTLY_VERSION env var.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(update): persist auto-update settings under ~/.ao
readUpdateSettings/writeUpdateSettings store {enabled, channel, nightlyAck}
with safe defaults and channel coercion, atomic temp+rename like app-state.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(update): channel-aware electron-updater wiring
Replace update-electron-app (stable-only, no channels) with electron-updater
driven by the user's ~/.ao settings: channel from settings, allowDowngrade for
channel switches, auto-download gated on opt-in, errors swallowed. Feed
configured via setFeedURL since forge does not emit app-update.yml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(update): tabs in auto-updater.ts to match repo prettier config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(update): first-run opt-in + channel + nightly disclaimer prompt
Minimal main-process dialog flow: opt into auto-updates, pick stable/nightly,
and acknowledge a nightly instability/data-loss disclaimer. Persists the choice
to ~/.ao. Polished Settings-page selector is tracked in #2207.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(spec): correct feed-yml premise (no platform emits it as built)
Final review verified that publish:null in both custom makers makes
electron-builder skip update-info yml generation on Windows and Linux too
(app-builder-lib PublishManager returns early before createUpdateInfoTasks),
not just signing-blocked macOS. The runtime updater is therefore inert on all
platforms until a separate feed-publishing workstream (yml gen+upload across
all 3 platforms, coupled to Track-B signing) lands. Recorded in the Feed
section and the prerequisites table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(release): nightly skip-guard must match the v<version> tag namespace
The forge publisher tags nightly releases v<version> (e.g.
v0.10.4-nightly.<ts>), not desktop-v..., so the no-new-commits guard queried a
namespace that never exists and never fired. Query v*-nightly.* instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* chore(release): run nightly at 19:00 IST (13:30 UTC)
Was 03:00 UTC (08:30 AM IST); move to 13:30 UTC = 7:00 PM IST.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The desktop release only built on macos-latest (Apple Silicon, arm64),
so the darwin-x64 stable alias was never produced and Intel-Mac
ao start 404s fetching agent-orchestrator-darwin-x64.zip.
Add the macos-13 (Intel x64) runner to the job matrix and broaden the
macOS alias-upload step's condition from matrix.os == 'macos-latest' to
startsWith(matrix.os, 'macos') so it runs on both macOS runners. The
step already derives arch via uname -m, so the Intel runner now emits
and uploads the x64 alias. Updated the now-stale gap comment.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(start): implement ao start fetch/open for Windows and Linux
Fill in the non-darwin branches of the bootstrapper (T6/T7):
- assetName() selects the per-GOOS stable release asset: windows ->
agent-orchestrator-win32-x64.exe (NSIS installer), linux ->
agent-orchestrator-linux-x64.AppImage. amd64-only for now via
requireAMD64(), which returns a clear unsupported-arch error.
- fetchApp() dispatches per GOOS. Windows downloads the NSIS installer and
runs it silently (/S) to the default per-user dir, then resolves the
installed exe under %LOCALAPPDATA%\Programs. Linux downloads the AppImage
to a stable path under ~/.ao (atomic temp+rename), chmods it executable,
and skips any install step so re-runs resolve without re-fetching.
- knownAppLocations() scans the per-user and per-machine Windows install
dirs and the stable Linux AppImage path.
- isUsableBundle() treats a win exe / linux AppImage as a regular file
(darwin stays a directory).
- openApp() launches win/linux detached via the existing StartProcess seam,
forwarding --installed-via=npm-bootstrap, and falls back to manual-open on
spawn failure.
The Windows silent-install path is marked ponytail (untestable on the macOS
build host); a wrong install dir surfaces as a clear not-found error. Tests
cover asset naming, arch gating, scan locations, regular-file vs dir, and
the detached-spawn + fallback paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(release): build + publish a Linux AppImage for ao start (T7)
AppImage is the Linux fetch-and-run artifact for the bootstrapper (spec
§11.3): a single self-contained executable ao start downloads and runs
directly, no system package manager.
- makers/maker-appimage.ts: a MakerBase subclass bridging to
electron-builder's buildForge (appImage target), mirroring maker-nsis.ts,
since Forge has no first-party AppImage maker. publish:null so Forge owns
release uploads.
- forge.config.ts: register MakerAppImage for linux; keep deb/rpm for users
who want a system package.
- frontend-release.yml: on ubuntu-latest, copy the built AppImage to the
stable, space-free name agent-orchestrator-linux-x64.AppImage and upload
it to the v<version> release with --clobber, mirroring the Windows step.
Build-untested on this macOS host: the first ubuntu CI run must confirm the
electron-builder AppImage target token and the out/make/*.AppImage output
glob.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ci): green up ao start Win/Linux PR (lint, cross-OS test, container smoke)
Five golangci-lint findings in start.go, two cross-OS test failures, and the
fresh-install container smoke were broken by the Win/Linux bootstrapper diff.
Go lint (start.go):
- gocritic filepathJoin: build "/Applications/<bundle>" as a literal instead of
filepath.Join with a separator-bearing arg.
- gocritic httpNoBody: pass http.NoBody, not nil, to NewRequestWithContext.
- gosec G302: annotate the AppImage chmod 0755 with a nolint; an AppImage is a
self-contained executable and must be executable.
- nilerr: annotate openApp's intentional (false, nil) on launch failure; the
failure is reported via the bool, not as an error.
- unparam: resolveApp's error result was always nil; drop it and update callers.
Cross-OS tests (start_test.go):
- makeBundle created a directory, which only stats as usable on macOS. Make it a
regular file on Windows/Linux so the marker/scan resolve tests pass there,
matching isUsableBundle's per-OS rule.
Container fresh-install smoke (test/cli/install-check.sh, Dockerfile):
- ao start is now the desktop-app launcher and no longer runs a daemon, so the
old daemon status/shutdown/stop assertions could never pass. Assert instead
that on a fresh box start reaches the fetch path and exits non-zero with a
clear error (404 download on amd64, unsupported-arch on arm64). Refresh the
stale daemon-reaping comments in the Dockerfile.
Verified locally: go build/vet ok, golangci-lint v2.12.2 reports 0 issues,
go test -tags e2e ./internal/cli/... passes (the only remaining failure,
TestE2E_Lifecycle, is a pre-existing daemon-shutdown flake that fails the same
way on upstream/main on this host), and the container smoke passes on both
linux/arm64 and linux/amd64.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: grounded spec for ao start bootstrapper + npm deprecation
Real-codebase-verified implementation spec (Track A: launcher + app-state
marker + release asset wiring). Replaces the somthing.md draft's aspirational
assumptions with file:line ground truth: correct bundle name (Agent
Orchestrator.app), publish repo (aoagents/agent-orchestrator), draft-release
+ asset-rename gaps, unsigned-build reality, and the already-wired
update-electron-app updater. Includes a dependency-ordered task breakdown
for AO execution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: reframe spec around Go ao start subcommand (not a JS launcher)
The npm package ships the existing Go cobra CLI (backend/cmd/ao); this effort
rewrites the `ao start` subcommand to fetch+open the desktop app. Corrections:
- releases land on AgentWrapper/agent-orchestrator (aoagents was the temporary
rewrite home; forge publisher must be repointed)
- ao start stops starting the daemon; the frontend owns the daemon
- adds the real Go CLI command surface (1.7), the npm-delivery gap for the Go
binary (1.6), and the legacy first-boot import decision (6.4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: separate prod vs test release targets in ao start spec
Dev/test loop must never cut a production release. Download repo + forge
publisher + npm scope are now build-time overridable:
- prod: AgentWrapper/agent-orchestrator + real package name
- test: harshitsinghbhandari/agent-orchestrator (fork) + @theharshitsingh/ao
T3/T5 now release+test against the fork and the test scope, with prod cut as a
separate gated step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): rewrite ao start to fetch + open the desktop app (T1)
ao start no longer starts the daemon (the desktop app owns it). It now
resolves the installed app (~/.ao/app-state.json marker -> stat -> known-
location scan), fetches the latest release zip and ditto-unpacks it on macOS
when absent, opens it with --installed-via=npm-bootstrap, and prints an honest
deprecation notice. releaseRepo is build-time overridable (-ldflags) so test
builds fetch from the fork. Windows/Linux fetch/open are stubbed for T6/T7.
Review fixes folded in: download() copies deps.HTTPClient and drops its 2s
loopback-probe timeout (a real release asset is hundreds of MB), and fetchApp
clears ~/.ao/staging before unpacking. Regression test covers the timeout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(desktop): write ~/.ao/app-state.json marker on launch (T4)
The app is the sole writer of the marker ao start reads to locate the bundle.
New frontend/src/main/app-state.ts does an atomic temp+rename write mirroring
the daemon's runfile.Write, preserving installedAt/installSource across launches
and refreshing appPath/version/lastReconciledAt. main.ts hooks it into
app.whenReady ordered relocate (macOS) -> write marker -> createWindow, both
non-fatal. Bundle path is derived from process.execPath (not app.getAppPath,
which is the asar path); JSON keys match start.go's appState reader exactly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(desktop-release): overridable repo, published release, stable asset aliases (T3)
Make the bootstrapper's constant releases/latest/download URL resolve:
- forge publisher repo reads AO_RELEASE_REPO (default AgentWrapper/agent-
orchestrator); the workflow sets it to github.repository so a fork run
publishes to the fork and never to prod
- draft:false so the release is immediately live (constant URL needs it)
- add ubuntu-latest to the matrix (issue #2191)
- post-publish steps upload stable space-free aliases
(agent-orchestrator-darwin-arm64.zip, agent-orchestrator-win32-x64.exe)
matching exactly what start.go fetches
Review fix: the alias upload targeted GITHUB_REF_NAME (the git tag), but
publisher-github creates the release as v<package.json version>; retargeted to
that and relaxed the guard so workflow_dispatch also produces aliases.
Known gaps (documented inline): macOS x64 needs an Intel runner (macos-latest is
arm64-only); the Linux stable asset name awaits the deb/rpm-vs-AppImage decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* feat(npm): deliver Go ao binary as @aoagents/ao via per-platform packages (T2)
Ship the ao CLI to npm with ZERO install scripts using the esbuild model:
- packages/ao: pure-JS @aoagents/ao with a bin/ao.js shim that resolves and
execs the matching @aoagents/ao-<platform>-<arch> optionalDependency
- four platform packages (darwin-arm64/x64, win32-x64, linux-x64), each os/cpu
gated so npm installs only the host's; binary cross-compiled CGO-free
(modernc.org/sqlite), gitignored, shipped via files
- build-binaries.sh cross-compiles all four; releaseRepo keeps its prod default
Not added as root workspaces on purpose: os/cpu-restricted members make root
`npm ci` fail EBADPLATFORM (CI's api-drift job runs it). Packages publish
standalone; the shim was verified in a published-like layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(npm): set @aoagents/ao packages to 0.10.0 (above existing 0.9.5)
The existing @aoagents/ao on npm is 0.9.5; the launcher must publish a higher
version so npm latest advances and existing users get the new fetch-and-open
binary on update. All five packages plus the four optionalDeps pins set to
0.10.0.
* chore: format with prettier [skip ci]
* fix(start): capture install provenance before macOS relocation
moveToApplicationsFolder() relaunches the app from /Applications without
forwarding the --installed-via arg, and code past a successful move never
runs in the staging instance. The post-move instance therefore wrote
installSource="unknown", and writeAppStateMarker's sticky logic then locked
it there, losing the npm-bootstrap provenance in the exact path it exists for.
Write the marker before relocation when --installed-via is present so the
source is persisted while the arg is still available; the post-move launch
preserves it (sticky installSource) while refreshing appPath to /Applications.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(desktop): package Windows via NSIS instead of Squirrel (#401)
Squirrel.Windows is a poor fit: per-user install only, no custom install
directory, no proper add/remove-programs uninstaller, and fragile updates.
Replace it with a real NSIS installer (per-user or per-machine, custom
install dir, uninstaller), matching recordly's working Windows setup.
Electron Forge ships no first-party NSIS maker, so add a thin MakerBase
subclass (makers/maker-nsis.ts) that bridges to electron-builder's
buildForge, the same engine electron-builder uses, scoped to win32. The
maker exposes the NSIS knobs the issue calls for (oneClick:false,
allowToChangeInstallationDirectory, per-machine) and defaults to an
assisted installer.
- forge.config.ts: drop maker-squirrel, add the NSIS maker instance.
- testing-build.yml: target "nsis"; smoke-install via /S under out/make;
drop the Squirrel-specific log capture.
- Rename the package "agent-orchestrator-frontend" -> "agent-orchestrator":
this repo is the full app, not just a frontend. User-facing naming was
already "Agent Orchestrator" (productName) / agent-orchestrator.exe.
Deferred (per issue, separate follow-ups): bundling zellij.exe so a fresh
Windows install needs no manual zellij, an actionable "zellij not found"
error at session-create, and Windows code-signing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(desktop): disable electron-builder publish + drop electron-squirrel-startup
CI fix: the NSIS maker's electron-builder run inferred a GitHub publish
target from package.json `repository` and tried to upload (to emit
auto-update info), failing with "GitHub Personal Access Token is not set".
Forge owns publishing (the workflow uploads via `gh release`), so set
`config.publish = null` to disable electron-builder's upload entirely.
Also remove `electron-squirrel-startup`: it only handled Squirrel.Windows
install/update hooks (--squirrel-* flags) and is dead weight under NSIS.
Drop the dependency, its import, the startup quit-block, the whenReady
guard, and the type shim. The EPIPE std-stream guard stays (it covers any
windowless Windows GUI launch, e.g. from a shortcut).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: gitignore electron-builder's builder-debug.yml dump
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(desktop): handle Squirrel startup events + CI smoke-install the Windows build
Two Windows-install fixes.
1. Add electron-squirrel-startup handling. main.ts had no handler for the
--squirrel-{install,updated,uninstall,obsolete} flags Squirrel runs the exe
with during install/update/uninstall. Without it the install hook booted the
full app (window + daemon spawn) and never exited, so the installer hung
waiting on it. Now we create/remove shortcuts and quit immediately; it is a
no-op on macOS/Linux.
2. Add a Windows CI smoke-install step. After the build, run Setup.exe --silent
on the clean native x64 windows-latest runner, assert the install dir is
created, and upload SquirrelSetup.log as an artifact. This captures the
install log we otherwise cannot get and gives a build-vs-host verdict: a clean
install proves the artifact is good, so a failing user machine is host-side
(AV/disk/signing). The runner has no real-time AV, so it does NOT prove
SmartScreen/Defender will accept the unsigned binaries on end-user machines;
code-signing stays the durable fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(windows): capture SquirrelSetup.log from the app dir, fix misleading warning
Update.exe writes SquirrelSetup.log into %LocalAppData%\AgentOrchestrator, not
SquirrelTemp; the smoke step looked only in SquirrelTemp and so printed
"failed before Update.exe ran" even on a successful install (exit 0, dir
created). Look in the app root dir first, fall back to SquirrelTemp, and drop
the false-failure wording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Replaces linux/windows/macos-testing-build.yml with a single testing-build.yml
(matrix over ubuntu/windows/macos) that publishes all artifacts to one
0.0.0-testing-<sha> prerelease. Manual dispatch + 0.0.0-testing-* tag push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a tag-triggered workflow that builds the Electron desktop app (with the
bundled Go daemon) on macOS, Windows, and Linux runners and attaches the
unsigned artifacts to a GitHub prerelease for end-to-end pipeline validation.
Signing/notarization is intentionally off until the certs and secrets exist,
so these builds are for validating packaging, not distribution.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(daemon): self-heal a stale run-file instead of refusing to start
On Windows the desktop supervisor can only TerminateProcess the daemon
(no POSIX signal reaches a detached child), so the daemon's graceful
shutdown never runs and ~/.ao/running.json is never removed. The leaked
file survives into the next launch, and because Windows reuses PIDs
aggressively the recorded PID usually belongs to an unrelated process.
The startup pre-flight trusted PID liveness alone (runfile.CheckStale ->
processalive.Alive), so it concluded a daemon was "already running" and
exited with "refusing to start" on every restart. A dead daemon then
makes the renderer's loopback REST calls (e.g. Spawn Orchestrator) fail
silently.
Verify the recorded port is actually served by an AO daemon with the
recorded PID (a /healthz probe matching service + pid, the same ground
truth inspectDaemon already uses) before refusing. A run-file left by a
crashed, hard-killed, or reused-PID predecessor is treated as stale and
overwritten, so startup is robust to a leaked run-file from any cause.
Fixes#256
* fix(release): build the desktop daemon natively on each target OS
build-daemon.mjs compiles the bundled `ao` daemon with the build host's
GOOS and names it off the host platform (ao.exe only when the builder is
Windows). The release workflow ran only on macos-latest, so a Windows
package would ship a macOS binary named `ao` with no `ao.exe`, and the
app could not launch a valid Windows daemon ("This program cannot be run
in DOS mode" / binary not found).
Run the release as a per-OS matrix (macOS + Windows) so host == target
and each installer bundles a daemon compiled for its own platform, and
pin the Go toolchain with setup-go since build-daemon needs it on every
runner.
Fixes#235
* feat(terminal): Windows ConPTY support for /mux attach
Replaces the Windows stub in internal/terminal/pty_windows.go with a real ConPTY implementation backed by github.com/aymanbagabas/go-pty, so the daemon's /mux attach can stream a live terminal to the renderer on Windows.
PTYSource.AttachCommand now returns (argv, env, err). On Windows the zellij attach is spawned directly (no powershell.exe wrapper) — wrapping ConPTY startup around a shell surfaces as modal application-error dialogs — and the per-session ZELLIJ_SOCKET_DIR is delivered via the spawn's CreateProcess env block instead of an 'env -u NO_COLOR' shim. Unix continues to use the env-shim wrapper and returns nil env.
Adds go-pty v0.2.3 (+ bumps golang.org/x/sys to v0.44.0 transitively). Updates the in-process test fakes (terminal/fakes_test.go, httpd/terminal_mux_test.go) for the new signature.
* feat(zellij): discover zellij binary on Windows and raise command timeout
Defaults the zellij binary to whatever exec.LookPath finds first (preferring zellij.exe on Windows), falling back to LOCALAPPDATA\Programs\zellij\zellij.exe and ProgramFiles{,(x86)}\{zellij,Zellij}\zellij.exe so a fresh-installed Windows user gets a working runtime without setting Options.Binary.
Raises the per-command timeout from 5s to 30s on Windows: the first zellij invocation after install routinely takes longer than 5s on Windows due to filesystem/AV warmup, which was causing benign DeadlineExceeded failures during session create.
* feat(zellij,cli): Windows agent launcher trampoline for codex argv
On Windows, zellij's KDL `args` quoting cannot round-trip codex's --config key=value flags (or any argv with embedded quotes), and shell-wrapping the agent in powershell/cmd quoting is equally unsound. This adds a small launch trampoline so zellij runs a known-fixed argv and the real argv is delivered out-of-band.
How it works on Windows:
1. zellij.Runtime.writeLayout persists cfg.Argv to a temp JSON spec via the new agentlaunch package (AO_LAUNCH_SPEC env var points at the file).
2. The KDL layout runs the trampoline as `<ao.exe> launch` (windowsLaunchArgv); PATH is augmented so the trampoline resolves.
3. The new hidden `ao launch` subcommand reads the spec, deletes the temp file, and execs the real agent with cfg.Argv inside cfg.WorkspacePath.
Also adds:
- runner.Start fire-and-forget path (process_windows.go uses powershell.exe -EncodedCommand + Start-Process -WindowStyle Hidden with CREATE_NEW_CONSOLE so the daemon is not blocked on zellij's --create-background settling).
- powerShellEncodedCommand helper and switch from -Command to -EncodedCommand for the existing powershell shellLaunchSpec (avoids brittle KDL→PowerShell quoting round-trips).
Unix is unchanged: writeLayout passes cfg.Env straight through, createSession stays synchronous via runner.Run, and process_other.go is a stub that returns an error if anyone calls into the background path.
* feat(codex): Windows binary resolution, terminal compat flags, TOML literal strings
Three Windows-targeted refinements to the codex agent plugin so a default Windows install lands in a working state:
1. ResolveCodexBinary now follows .cmd/.ps1 shims to the underlying codex.exe (resolveNativeWindowsCodex + windowsNativeCodexCandidatesForShim). The npm-distributed codex shim cannot be exec'd directly under ConPTY without a shell wrapper; jumping straight to the .exe avoids that wrapper.
2. appendTerminalCompatibilityFlags adds Windows-specific args (e.g. --no-alt-screen) so codex's TUI renders correctly inside zellij's pane without the alternate-screen buffer churn that breaks ConPTY redraws.
3. hooks.go gains codexTOMLLiteralString / codexTOMLConfigString / containsTOMLControl so paths and other values with backslashes and quotes round-trip through codex's --config TOML parser using literal strings ('...') when basic strings would require unsafe escaping.
* fix(lint): paramTypeCombine in pty_unix.go, revive doc comments in agentlaunch, codex test quotes
* fix: stabilize windows zellij sessions
---------
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
Co-authored-by: Madhav <madhavkumar@microsoft.com>
Co-authored-by: Vaibhaav <user@example.com>
* ci: run the frontend vitest suite on pull requests
The renderer suite was dead for months with nothing noticing — no workflow
executed it (and until #171 it could not even run: vitest auto-loads only
vite.config.ts / vitest.config.ts, which the repo lacked). This job keeps
the revived suite alive.
Typecheck is intentionally left out until the pre-existing forge.config /
update-electron-app type errors are fixed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): regenerate package-lock.json in sync with package.json
The committed lock was missing dozens of entries (ansi-regex, error-ex,
minimatch, ...), so `npm ci` under CI's npm 10 hard-fails with "Missing:
<pkg> from lock file" — the new Frontend workflow caught it on its first
run. Newer local npm versions tolerated the drift, which is why it went
unnoticed. Regenerated with npm install (npm 10.9.8, lockfileVersion 3) and
validated with a clean `npm ci` + full vitest run (9/9 files, 99/99 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore: add prettier config and CI auto-formatter
Adds .prettierrc and .prettierignore (config only, no local enforcement).
Formatting runs in CI via the prettier.yml workflow: on every push to a
non-main branch, Prettier rewrites changed files and commits the result back
using GITHUB_TOKEN. Developers never need to run Prettier locally.
Intentionally excludes husky/lint-staged — local pre-commit hooks are the
wrong layer for a formatter that the whole team doesn't need installed.
Also adds .envrc.local to .gitignore for personal local shell overrides.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 27336650d2ee
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* ci(frontend): add react-doctor check for landing site
Adds a CI job that runs react-doctor (https://github.com/millionco/react-doctor)
against the Next.js 15 + React 19 landing site at frontend/src/landing/.
The landing has 29 .tsx components and no existing lint coverage; the only
prior frontend CI was a schema.ts drift check in go.yml.
- New workflow .github/workflows/react-doctor.yml, path-filtered to
frontend/src/landing/** so backend-only PRs don't pay for it.
- doctor script + react-doctor devDep added to the landing package.
- Default --blocking=error: surfaces security, bugs, perf, a11y, and
maintainability findings without failing CI on existing warnings
(current baseline: 0 errors, 58 warnings, ~5s locally).
- Runs with --no-telemetry so CI runners don't ping react.doctor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci(frontend): also scope react-doctor push trigger to landing paths
Mirrors the PR path filter on push to main so backend-only merges don't
re-run the landing check. Unlike go.yml/cli-e2e.yml (which trigger on
broad path sets), this workflow only cares about frontend/src/landing/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* ci(frontend): use react-doctor GitHub Action, drop devDep + lockfile churn
Replaces the npm install + npm run doctor approach with the official
millionco/react-doctor@v2 composite action. The action manages its own
Node setup and react-doctor install on the runner, so the landing
package.json and lockfile stay untouched (no transitive-dep bloat from
react-doctor's 479-package tree).
The action also wires up sticky PR summary comments, inline review
comments, and commit statuses out of the box — requires pull-requests
and statuses write perms.
* feat: add npm run api scripts and document API contract change flow
Adds three root package.json scripts mirroring the sqlc convention:
- api:spec — runs go generate to regenerate openapi.yaml
- api:ts — runs openapi-typescript@7.4.4 to generate frontend/src/api/schema.ts
- api — runs both in sequence (the single contributor command)
Documents the full flow in a new AGENTS.md "API contract changes" section
so contributors know which files to edit, how to regenerate, and what to
verify. Implements slice 1 of issue #102.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: correct paths in AGENTS.md API contract changes section
- api:ts equivalent command: use full relative path
backend/internal/httpd/apispec/openapi.yaml, not bare openapi.yaml
- verify command: prefix with `cd backend &&` so it works from repo root
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add CI drift guard for OpenAPI spec and frontend TS types
- Adds api-drift job to go.yml: regenerates openapi.yaml + schema.ts
via `npm run api`, then fails if git diff detects any change
- Expands go.yml path filter to trigger on package.json and schema.ts
- Commits initial schema.ts generated from the current spec
- Notes the CI guard in AGENTS.md API contract changes section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address review feedback on api-drift CI job
- Scope git diff to schema.ts only; openapi.yaml drift is already
caught by TestBuild_MatchesEmbedded in the build-test Go job
- Add npm ci step so openapi-typescript is installed from the lockfile
rather than re-fetched by npx on every run
- Move openapi-typescript from npx -y to a pinned devDependency
(exact 7.4.4, no caret) with a committed package-lock.json
- Note in AGENTS.md Verify block that go test does not cover schema.ts drift
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces backend/.golangci.yml (27 linters across correctness, dead-code/
boilerplate, style, and security), wires it into CI as a blocking job, and
fixes every finding so the tree starts at zero.
Config:
- 27 linters: errcheck, govet, staticcheck, errorlint, bodyclose,
sqlclosecheck, rowserrcheck, nilerr, makezero, unused, unparam, unconvert,
wastedassign, copyloopvar, prealloc, dupl, revive (incl. exported-symbol doc
comments), gocritic, misspell, usestdlibvars, predeclared, nakedret, gosec, …
- Tuned for signal over noise: govet/shadow and gocritic hugeParam/rangeValCopy/
unnamedResult disabled (idiomatic-Go false positives); sqlc-generated code and
tests get scoped exclusions; gosec G304 excluded (paths are config/run-file/
worktree-derived, not user input); nilerr excluded in cli/status.go (probe
failures are the reported status, not a command error).
CI:
- New blocking lint job (golangci-lint-action, latest binary for Go-version
compatibility).
- go-version now read from go.mod (was pinned 1.22 while go.mod declares 1.25).
Cleanup to reach zero (no behavior change):
- errcheck: wrap deferred/inline Close()/Remove()/Rollback() with `_ =`.
- gosec: tighten dir/file perms (0755->0750, 0644->0600).
- unparam: drop always-nil error return from startLifecycle; drop unused
shellPath param (zellij PowerShell) and always-500 fallbackStatus param
(writeProjectError).
- gocritic: regexp \d, s != "", switch->if, combined appends.
- revive: doc comments on all exported symbols; rename project.ProjectRow ->
project.Row (stutter); rename `max` locals shadowing the builtin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the growing bash smoke test with a Go os/exec suite behind the `e2e`
build tag (backend/internal/cli/e2e_test.go). It builds the real binary and
drives start/status/doctor/stop + the daemon-control HTTP surface against
isolated state (temp dir + OS-assigned free port), and now runs natively on
ubuntu + macOS + WINDOWS in CI — finally covering the Windows
CREATE_NEW_PROCESS_GROUP detach path and per-OS os.UserConfigDir resolution
that a Linux container can't observe. `go test -tags e2e -v` logs every command
and its output, replacing the bash -v flag.
- backend/internal/cli/e2e_test.go: 8 table-style TestE2E_* cases; strips any
inherited AO_* env so a real daemon's AO_PORT can't leak in.
- test/cli/install-check.sh: small, linear fresh-install proof the Dockerfile
runs (binary on PATH, no toolchain) — kept as the hardening tier.
- test/cli/Dockerfile: run install-check.sh instead of the full bash suite.
- .github/workflows/cli-e2e.yml: `native` is now a go test matrix over
ubuntu+macos+windows; `container` builds the image and runs it with --init.
- Removes test/cli/smoke.sh and test/cli/run-local.sh (superseded by `go test`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stale-daemon assertion does not depend on container PID-1 reaping — it
writes a fabricated dead PID rather than killing a real process. --init is
still run so the real daemon spawned by the `start` test (detached via setsid)
is reaped after `stop` instead of lingering as a zombie. Reword the README,
Dockerfile, and workflow comments to say that accurately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a fresh-machine, install→use→verify E2E test for the `ao` CLI and wires
it into CI. The suite drives the real binary (start/status/doctor/stop + the
daemon-control HTTP surface) against fully isolated state — its own temp
run-file, data dir, and an auto-picked free loopback port — so it never
collides with a developer's real AO install or daemon.
- test/cli/smoke.sh: 40 assertions covering install resolution, version/help
(daemon hidden), doctor text+json (and that it does NOT migrate SQLite),
status stopped/stale/ready, start fresh+idempotent, daemon-created store,
/healthz identity, the /shutdown CSRF + DNS-rebinding guard (403 + survives),
graceful/stale/idempotent stop, run-file ownership cleanup, exit codes
(2 usage / 1 runtime), and completion for all four shells. It deliberately
ignores an inherited AO_PORT and self-allocates a free port for isolation.
- test/cli/Dockerfile: models installing ao on a fresh machine — builds the
binary, drops it on PATH in a clean Debian image with only runtime deps
(git/tmux/curl), runs the suite as a non-root user.
- test/cli/run-local.sh: build-from-source + native run convenience wrapper.
- .github/workflows/cli-e2e.yml: two tiers — `native` runs the suite on a
ubuntu+macos runner matrix (the real VMs, to cover the unix setsid detach and
macOS os.UserConfigDir paths a Linux container can't), and `container` runs
the fresh-machine Docker image with --init (real PID-1 reaper so the
stale-daemon assertion is reliable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- go.yml: gofmt check, build, vet, and race-enabled tests for backend/,
triggered on backend changes and pushes to main.
- gitleaks.yml: secret scanning on PRs and main using gitleaks-action v1
(license-free; v2 requires GITLEAKS_LICENSE for org repos).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>