* 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>