* docs: design for dashboard legacy-migration popup + app-state marker
Spec for the app-side migration trigger (Approach A): projects-only import
daemon API + a migration marker in ~/.ao/app-state.json, with a launch-time
popup (Proceed / Skip / Don't Migrate). Settings redo path deferred to #2205.
Includes the projects-only import-offer backend plan it consumes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: implementation plan for legacy-migration popup + marker
Part A reuses the projects-only import API (import-offer plan) with an
availability-only Status; Part B adds the app-state migration marker (schema v2),
IPC, the useMigrationOffer gate, and the MigrationPopup (Proceed/Skip/Don't
Migrate). Settings redo path deferred to #2205.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(legacyimport): scope import to projects + settings only
Remove orchestrator/transcript import code (orchestrator.go, claude.go,
session_import_store.go and their tests). Trim Store, Options, Report to
projects-only fields. Drop defaultClaudeProjectsDir and projectSessionsDir
from paths.go. Add yaml.TypeError robustness in config.go. Update cli/import.go
confirm prompt and summary. Update importer_test.go to projects-only assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(importer): availability probe + projects-only run
Create service/importer.Manager with Status (physical availability check only,
no DB heuristic) and Run (delegates to legacyimport.Run). The app-state.json
marker governs whether to prompt; this service only answers whether legacy data
is physically present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(httpd): import controller + DTOs (GET/POST /api/v1/import)
Add ImportStatusResponse/ImportRunResponse DTOs to dto.go. Create
ImportController with GET (status probe) and POST (run) handlers, both
returning 501 when Svc is nil. Wire APIDeps.Import + API.imports in api.go.
Add controller tests (status, status error, run, run error).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(apispec): describe /api/v1/import; regenerate openapi + schema.ts
Add import tag, importOperations() (GET + POST /api/v1/import), and schema
name mappings (ImportStatusResponse, ImportRunResponse, ImportReport) to
build.go. Regenerate openapi.yaml and frontend/src/api/schema.ts. Route-spec
parity test passes. Restore the nil-svc-501 import controller test now that
the spec includes the operation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(daemon): mount import service on the API
Wire importsvc.New(importsvc.Deps{Store: store}) into APIDeps.Import in
daemon.go so the daemon serves GET/POST /api/v1/import backed by the live
sqlite store. Projects-only; no DataDir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(cli): drop resolved §6.4 first-boot-import TODO
The legacy import is now a daemon API (GET/POST /api/v1/import) served by
the importer service and the desktop app handles the popup prompt via the
app-state.json migration marker. The TODO comment is resolved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(legacyimport): remove dead isDir helper
isDir was only used by the deleted projectSessionsDir function.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: update package-lock.json after npm install for api:ts
openapi-typescript was missing from root node_modules; npm install
populated it so npm run api:ts could regenerate schema.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(httpd): drop em-dash comment + unused ImportStatusResult alias
Review findings M1/M2 from G1 task review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(app-state): migration marker (schema v2) + updateMigration
Bump SCHEMA_VERSION to 2, add MigrationStatus/MigrationState types and
migration? field to AppStateMarker. Extract atomicWriteMarker helper and
reuse it. writeAppStateMarker now preserves an existing migration block
across launch writes. Add updateMigration (IPC setter) and readMigrationState
(IPC getter) exports. TDD: tests added first (red), then implementation (green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ipc): expose app-state migration getter/setter to the renderer
Add appState:getMigration / appState:setMigration IPC handlers in main.ts.
Add ao.appState.getMigration / setMigration to preload.ts (typed via AoBridge).
Add appState preview fallback in bridge.ts and test setup so AoBridge stays
satisfied in both browser preview and test environments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(app-state): cover corrupt-marker case + clean up temp dirs
G2 review findings I2 (corrupt-JSON branch untested) and m1 (temp dirs not cleaned).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(renderer): useMigrationOffer gate (marker + availability)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(renderer): MigrationPopup (Proceed / Skip / Don't Migrate)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(renderer): surface MigrationPopup on the dashboard
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>
* 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>
* feat(import): rewrite-side legacy → rewrite first-boot import
Port the legacy-side TS reader (AgentWrapper #2144/#2129) to Go and run the
migration inside the rewrite as an opt-in import, per the FINAL v2 plan. Reads
the legacy flat-file store (~/.agent-orchestrator) read-only and writes the
rewrite's own SQLite DB via the native storage layer; legacy files are never
touched, and a re-run skips existing rows, so a declined or failed import loses
nothing.
What's included:
- internal/legacyimport: Go reader + field mappers (issue #247). Lifecycle
double-decode (lifecycle key or statePayload+stateVersion:"2"),
role/orchestrator detection, sessionPrefix fallback (first 12 chars of id),
8→4 activity-state map, per-harness resume-id selection, permission/harness
remap, and the claude transcript slug + relocation to the rewrite's
orchestrator worktree path ({DataDir}/worktrees/{id}/orchestrator/{prefix}-orchestrator).
- store.ImportSession: verbatim session insert (explicit id/num, ON CONFLICT
DO NOTHING) so the orchestrator lands at id "{prefix}-orchestrator", num 0.
- `ao import`: explicit, idempotent import with --from/--dry-run/--yes/--json.
Refuses while a live daemon owns the run-file (the daemon is sole writer; the
import runs offline, matching the #2129 reference).
- First-boot opt-in: `ao start` offers the import before launching the daemon
when legacy data is present and the rewrite DB has no projects yet. Declining
or any failure is non-fatal; a non-interactive boot prints a hint instead of
auto-importing.
Scope (gist §6): all projects + per-project settings, and the single
non-terminated orchestrator session per project (claude-code/codex/opencode;
aider skipped with a note). Workers are not imported (they respawn fresh).
Resume-id mapping (#247 §2.2): agent_session_id carries claudeSessionUuid /
codexThreadId / opencodeSessionId by harness. codexModel and
restoreFallbackReason have no rewrite column, so they are dropped and surfaced
as import notes — codex resumes from the thread id alone, the rest is forensic.
Gate: `go build ./... && go test -race ./...` green (1423 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(import): resolve golangci-lint errcheck/gocritic/nilerr findings
- start.go: check fmt.Fprint* returns in the first-boot import path
- project.go: combine same-typed return params (gocritic paramTypeCombine)
- claude.go: use a pathExists helper so a missing transcript source is a normal
skip, not an err-then-return-nil (nilerr)
- importer.go: fold best-effort transcript relocation into a switch so the
non-fatal path no longer returns nil from an error branch (nilerr)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(import): resolve transcript dest path like the daemon; harden lifecycle parse
Code-review follow-ups on the legacy importer:
- claude.go: compute the Claude transcript DESTINATION slug from the
symlink-resolved orchestrator worktree path (new resolvePhysical, mirroring
gitworktree.physicalAbs), not the literal path. The daemon resolves that cwd
through physicalAbs before `claude --resume` runs, so a literal-path slug
missed the resume bucket whenever any component of AO_DATA_DIR was a symlink
(custom data dir, macOS /tmp→/private/tmp, symlinked $HOME) — the orchestrator
would have resumed without its prior context. Source slug now uses the same
resolver for symmetry.
- orchestrator.go: accept a numeric stateVersion (JSON 2 → float64) as well as
the string "2" when falling back to statePayload, so a V2 record carried only
in statePayload is not misparsed as stateless.
- orchestrator.go: build the dropped-resume-metadata note as a joined list
instead of string concatenation.
Tests: added a symlinked-data-dir dest-slug test and a numeric-stateVersion
fallback test. Gate green: `go build ./... && go test -race ./...` (1425).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <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>