diff --git a/cmd/axis/main.go b/cmd/axis/main.go index 0430fea1..8eab710b 100644 --- a/cmd/axis/main.go +++ b/cmd/axis/main.go @@ -223,11 +223,11 @@ func versionCmd() *cobra.Command { ui.PrintLogo(out, Version) fmt.Fprintln(out) fmt.Fprintf(out, "axis %s\n", Version) - if buildinfo.Commit != "" { - fmt.Fprintf(out, " commit: %s\n", buildinfo.Commit) + if commit := buildinfo.ResolvedCommit(); commit != "" { + fmt.Fprintf(out, " commit: %s\n", commit) } - if buildinfo.Date != "" { - fmt.Fprintf(out, " built: %s\n", buildinfo.Date) + if built := buildinfo.ResolvedDate(); built != "" { + fmt.Fprintf(out, " built: %s\n", built) } goVer := buildinfo.GoVersion if goVer == "" { diff --git a/docs/README.md b/docs/README.md index 812ec240..6c64e2d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ ## Start Here - [Current State](current-state.md) — current orientation doc for the live repo, command surface, coverage snapshot, and known weak spots -- [Agent Worklog](agent-worklog.md) — active coordination surface, file ownership, and current task tracking +- [Agent Worklog](agent-worklog.md) — historical coordination log and file-ownership notes; not the current queue - [RAM Balancing Research](ram-balancing-research.md) — research-backed design note on how AXIS should model cluster RAM sharing and balancing ## Design Docs diff --git a/docs/agent-worklog.md b/docs/agent-worklog.md index 918438e1..af7cbbc7 100644 --- a/docs/agent-worklog.md +++ b/docs/agent-worklog.md @@ -1,6 +1,6 @@ # AXIS Agent Worklog -This file is the canonical coordination surface for active AXIS work. +Historical coordination log. The reviewed HEAD below is 2026-06-04. Live orientation is `docs/current-state.md`. The task tables are not the current queue. ## Coordination Contract diff --git a/docs/phase-tracking.md b/docs/phase-tracking.md index 2dc21880..201118c6 100644 --- a/docs/phase-tracking.md +++ b/docs/phase-tracking.md @@ -10,4 +10,4 @@ | Phase 6 | **Completed** | Trust-and-foundations work: GPU/storage/network enrichment, stable identity, and failure-memory placement signals | | Phase 7 | **Completed** | Runtime hardening: streamed `/run`, reservation durability, forwarded provenance, and discovery freshness | -Current release: **v0.12.2** +Phase completion is not the release record. The repo version constant is `internal/buildinfo/version.go`, repeated in the generated fact in `docs/current-state.md`. Published releases are on GitHub Releases. diff --git a/docs/runbooks/a2a-task-delegation.md b/docs/runbooks/a2a-task-delegation.md index 4be95cc9..ce59b13b 100644 --- a/docs/runbooks/a2a-task-delegation.md +++ b/docs/runbooks/a2a-task-delegation.md @@ -106,7 +106,11 @@ curl -s -H "Authorization: Bearer $T" --unix-socket "$S" \ http://localhost/a2a/v1/tasks/pending # -> {"tasks":[]} ``` -`axis version` prints `buildinfo.Commit` baked into the binary you just executed -(`cmd/axis/main.go`, `internal/buildinfo`). It does not ask the running daemon who -it is. **`axis update` replaces the binary but does not restart a supervised -daemon** — check the process start time, or the API response, not `axis version`. +`axis version` prints `commit:` from the ldflag `buildinfo.Commit` when that +value is set (`cmd/axis/main.go`, `internal/buildinfo.ResolvedCommit`). A plain +build whose commit ldflag is empty prints `vcs.revision` and appends `-dirty` +when `vcs.modified` is true. `built:` is only the ldflag build time. +`vcs.time` is the revision time and is not a build time. The command does not +ask the running daemon who it is. **`axis update` replaces the binary but does +not restart a supervised daemon** — check the process start time, or the API +response, not `axis version`. diff --git a/internal/buildinfo/build.go b/internal/buildinfo/build.go index 3a012194..ccaae87d 100644 --- a/internal/buildinfo/build.go +++ b/internal/buildinfo/build.go @@ -1,13 +1,61 @@ package buildinfo +import "runtime/debug" + // Commit, Date, and GoVersion are set at build time via -ldflags. // Example: // // go build -ldflags "-X github.com/toasterbook88/axis/internal/buildinfo.Commit=abc1234 // -X github.com/toasterbook88/axis/internal/buildinfo.Date=2026-04-01T12:00:00Z // -X github.com/toasterbook88/axis/internal/buildinfo.GoVersion=go1.26.2" +// +// When the commit ldflag is empty, ResolvedCommit uses vcs.revision and +// appends -dirty when vcs.modified is true. ResolvedDate is only the ldflag +// build time. vcs.time is the revision time and is not a build time. var ( Commit string Date string GoVersion string ) + +// ResolvedCommit returns the ldflags commit, or the toolchain revision. +// A dirty toolchain revision is marked so it is not printed as a clean commit. +func ResolvedCommit() string { + if Commit != "" { + return Commit + } + info, ok := debug.ReadBuildInfo() + if !ok || info == nil { + return "" + } + return commitFromSettings(info.Settings) +} + +func commitFromSettings(settings []debug.BuildSetting) string { + rev := settingFrom(settings, "vcs.revision") + if rev == "" { + return "" + } + if settingFrom(settings, "vcs.modified") == "true" { + return rev + "-dirty" + } + return rev +} + +// ResolvedDate returns the ldflag build time. It does not use vcs.time. +func ResolvedDate() string { + return dateFromSettings(Date, nil) +} + +func dateFromSettings(date string, _ []debug.BuildSetting) string { + return date +} + +func settingFrom(settings []debug.BuildSetting, key string) string { + for _, s := range settings { + if s.Key == key { + return s.Value + } + } + return "" +} diff --git a/internal/buildinfo/build_test.go b/internal/buildinfo/build_test.go new file mode 100644 index 00000000..d04306f3 --- /dev/null +++ b/internal/buildinfo/build_test.go @@ -0,0 +1,87 @@ +package buildinfo + +import ( + "runtime/debug" + "testing" +) + +func TestResolvedCommitPrefersLdflags(t *testing.T) { + old := Commit + t.Cleanup(func() { Commit = old }) + Commit = "abc1234" + if got := ResolvedCommit(); got != "abc1234" { + t.Fatalf("ResolvedCommit() = %q, want ldflags value", got) + } +} + +func TestResolvedDatePrefersLdflags(t *testing.T) { + old := Date + t.Cleanup(func() { Date = old }) + Date = "2026-04-01T12:00:00Z" + if got := ResolvedDate(); got != "2026-04-01T12:00:00Z" { + t.Fatalf("ResolvedDate() = %q, want ldflags value", got) + } +} + +func TestResolvedCommitEmptyLdflagMatchesToolchainSettings(t *testing.T) { + old := Commit + t.Cleanup(func() { Commit = old }) + Commit = "" + info, ok := debug.ReadBuildInfo() + if !ok || info == nil { + t.Fatal("ReadBuildInfo unavailable") + } + want := commitFromSettings(info.Settings) + if got := ResolvedCommit(); got != want { + t.Fatalf("ResolvedCommit() = %q, want toolchain settings %q", got, want) + } +} + +func TestResolvedDateEmptyLdflagStaysEmpty(t *testing.T) { + old := Date + t.Cleanup(func() { Date = old }) + Date = "" + if got := ResolvedDate(); got != "" { + t.Fatalf("ResolvedDate() = %q, want empty ldflag", got) + } +} + +func TestCommitFromSettingsMarksADirtyTree(t *testing.T) { + settings := []debug.BuildSetting{ + {Key: "vcs.revision", Value: "abc1234"}, + {Key: "vcs.modified", Value: "true"}, + } + if got := commitFromSettings(settings); got != "abc1234-dirty" { + t.Fatalf("commitFromSettings() = %q, want dirty revision", got) + } + settings[1].Value = "false" + if got := commitFromSettings(settings); got != "abc1234" { + t.Fatalf("commitFromSettings() = %q, want clean revision", got) + } + if got := commitFromSettings([]debug.BuildSetting{{Key: "vcs.modified", Value: "true"}}); got != "" { + t.Fatalf("commitFromSettings() = %q, want empty without a revision", got) + } +} + +func TestDateFromSettingsIgnoresRevisionTime(t *testing.T) { + settings := []debug.BuildSetting{{Key: "vcs.time", Value: "2020-01-01T00:00:00Z"}} + if got := dateFromSettings("", settings); got != "" { + t.Fatalf("dateFromSettings() = %q, want empty build time", got) + } + if got := dateFromSettings("2026-04-01T12:00:00Z", settings); got != "2026-04-01T12:00:00Z" { + t.Fatalf("dateFromSettings() = %q, want ldflag build time", got) + } +} + +func TestSettingFrom(t *testing.T) { + settings := []debug.BuildSetting{{Key: "vcs.revision", Value: "deadbeef"}} + if got := settingFrom(settings, "vcs.revision"); got != "deadbeef" { + t.Fatalf("settingFrom() = %q", got) + } + if got := settingFrom(settings, "vcs.time"); got != "" { + t.Fatalf("missing key = %q, want empty", got) + } + if got := settingFrom(nil, "vcs.revision"); got != "" { + t.Fatalf("nil settings = %q, want empty", got) + } +}