feat(build): add flywheel+legacy archive build tags; default ao omits them (age-focus-membrane-bookkeeper-m1wg.12)

Mechanism first, no behavior change (ADR-0012). Default `go build ./...` and the
shipped `ao` are the spine. `make build-flywheel` (-tags "flywheel legacy") and
`AGENTOPS_LEGACY=1 make build` (-tags legacy) restore the archived satellites.

- cmd/ao/buildtags.go + buildtags_{flywheel,legacy}.go: the tag-gated anchor +
  a hidden `ao buildtags` introspection surface (spine vs restored). Hidden, so
  no user-facing command surface change.
- Makefile/cli/Makefile: build-flywheel + verify-buildtags targets, GO_TAGS plumbing.
- scripts/verify-buildtags.sh: compiles all 4 variants, asserts default omits +
  tags restore (make verify-buildtags).
- docs/architecture/build-tags.md: the convention .13/.14 follow to archive
  real commands. Archive-not-delete: code stays buildable per ADR-0004/0009/0011.
This commit is contained in:
boshu
2026-06-30 16:45:34 -04:00
parent 57a5f9c18b
commit d1e5d28722
10 changed files with 281 additions and 6 deletions
+8 -2
View File
@@ -1,6 +1,6 @@
SHELL := /usr/bin/env bash
.PHONY: local-ci local-ci-fast ci build test docs-check regen-all regen-check clean help
.PHONY: local-ci local-ci-fast ci build build-flywheel verify-buildtags test docs-check regen-all regen-check clean help
# Default: run the release-grade local CI gate.
# Note: scripts/ci-local-release.sh already includes build + test + release-binary validation.
@@ -14,9 +14,15 @@ local-ci-fast: ## Run local CI without e2e install test, using quick security mo
ci: local-ci ## Alias for local-ci
build: ## Build ao CLI binary
build: ## Build ao CLI binary (default = spine; archived satellites omitted)
$(MAKE) -C cli build
build-flywheel: ## Build ao with the ADR-0012 archived satellites restored (flywheel + legacy)
$(MAKE) -C cli build-flywheel
verify-buildtags: ## Verify the ADR-0012 build-tag mechanism (default omits; flywheel/legacy restore)
$(MAKE) -C cli verify-buildtags
test: ## Run CLI tests
$(MAKE) -C cli test
+26 -3
View File
@@ -1,4 +1,4 @@
.PHONY: build test lint clean install sync-hooks inner membrane-parity
.PHONY: build build-flywheel test lint clean install sync-hooks inner membrane-parity verify-buildtags
# Build variables
BINARY_NAME=ao
@@ -8,6 +8,17 @@ LDFLAGS=-ldflags "-X main.version=$(VERSION)"
IDENTIFIER=com.12factoragentops.ao
GOLANGCI_LINT ?= ../scripts/golangci-lint-v2.sh
# ADR-0012 archive build tags. The default build OMITS both the corpus/flywheel
# (`flywheel`) and RPI/factory (`legacy`) command sets — they are archived, not
# deleted, so the code stays buildable for the revival conditions. Restore them:
# make build-flywheel # both tags (flywheel + legacy)
# AGENTOPS_LEGACY=1 make build # legacy only
GO_TAGS ?=
ifdef AGENTOPS_LEGACY
GO_TAGS += legacy
endif
GO_TAGS_FLAG = $(if $(strip $(GO_TAGS)),-tags "$(strip $(GO_TAGS))",)
# Default target
all: build
@@ -21,14 +32,26 @@ sync-hooks:
@cp ../scripts/pawl-review.sh ../scripts/pawl-verdict.sh ../scripts/pawl.sh embedded/pawl/scripts/
@cp ../schemas/pawl-verdict.v1.schema.json embedded/pawl/schemas/
# Build the binary
# Build the binary (default = spine; archived satellites omitted)
build: sync-hooks
@mkdir -p $(BUILD_DIR)
go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/ao
go build $(LDFLAGS) $(GO_TAGS_FLAG) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/ao
ifeq ($(shell uname -s),Darwin)
codesign -f -s - -i $(IDENTIFIER) $(BUILD_DIR)/$(BINARY_NAME)
endif
# Build with the archived satellites restored (ADR-0012): corpus/flywheel
# (`flywheel`) + RPI/factory (`legacy`). Used to run or maintain the archived
# command set under the revival conditions.
build-flywheel: GO_TAGS += flywheel legacy
build-flywheel: build
# Verify the build-tag mechanism: default build omits the tagged set; the
# flywheel/legacy tags restore it. Compiles all three variants and checks the
# hidden `ao buildtags` introspection surface.
verify-buildtags:
bash ../scripts/verify-buildtags.sh
# Run tests
test: build
go test -v -shuffle=on ./...
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
)
// archiveBuildTags lists the ADR-0012 archive build tags this binary was
// compiled with. It is EMPTY in the default build (the spine). Tag-gated files
// append to it from their init():
//
// cmd/ao/buildtags_flywheel.go (//go:build flywheel) -> "flywheel"
// cmd/ao/buildtags_legacy.go (//go:build legacy) -> "legacy"
//
// The mechanism: a command archived behind one of these tags lives in a file
// carrying the matching `//go:build <tag>` constraint, so its init() (which
// registers the cobra command) is only compiled when that tag is passed. The
// default `go build ./...` omits the file entirely — the command is neither
// compiled nor registered. `make build-flywheel` (tags: flywheel legacy) or
// `AGENTOPS_LEGACY=1 make build` (tag: legacy) restore them. ADR-0012 requires
// archive-not-delete, so the code stays buildable for the revival conditions.
var archiveBuildTags []string
// buildtagsCmd is a hidden introspection surface: it reports which archive build
// tags the running binary was compiled with, so an operator (or a test) can tell
// a spine build from a restored-satellite build. Hidden so the default command
// surface is unchanged.
var buildtagsCmd = &cobra.Command{
Use: "buildtags",
Short: "Report which ADR-0012 archive build tags this binary was compiled with",
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if len(archiveBuildTags) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), "spine (no archive build tags; corpus/flywheel + RPI/factory omitted)")
return nil
}
for _, t := range archiveBuildTags {
fmt.Fprintln(cmd.OutOrStdout(), t)
}
return nil
},
}
func init() {
rootCmd.AddCommand(buildtagsCmd)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build flywheel
package main
// Compiled only with `-tags flywheel`. Marks the binary as carrying the archived
// corpus/flywheel command set (ADR-0012). The default build omits this file.
func init() {
archiveBuildTags = append(archiveBuildTags, "flywheel")
}
+9
View File
@@ -0,0 +1,9 @@
//go:build legacy
package main
// Compiled only with `-tags legacy`. Marks the binary as carrying the archived
// RPI/factory command set (ADR-0012). The default build omits this file.
func init() {
archiveBuildTags = append(archiveBuildTags, "legacy")
}
+38
View File
@@ -0,0 +1,38 @@
package main
import (
"bytes"
"testing"
)
// TestBuildtags_DefaultBuildIsSpine asserts that the default build (no archive
// build tags) reports the spine: archiveBuildTags is empty and `ao buildtags`
// prints the spine line. The flywheel/legacy variants are exercised by the
// build-tag verification script (scripts/verify-buildtags.sh), which actually
// compiles with -tags; here we lock the default-build contract.
func TestBuildtags_DefaultBuildIsSpine(t *testing.T) {
if len(archiveBuildTags) != 0 {
t.Fatalf("default build must carry NO archive build tags, got %v", archiveBuildTags)
}
var out bytes.Buffer
buildtagsCmd.SetOut(&out)
t.Cleanup(func() { buildtagsCmd.SetOut(nil) })
if err := buildtagsCmd.RunE(buildtagsCmd, nil); err != nil {
t.Fatalf("buildtags RunE: %v", err)
}
got := out.String()
if !bytes.Contains([]byte(got), []byte("spine")) {
t.Fatalf("default build buildtags output = %q, want it to mention 'spine'", got)
}
}
// TestBuildtags_HiddenFromDefaultSurface keeps the introspection command off the
// public command surface so adding the mechanism is no user-facing behavior
// change.
func TestBuildtags_HiddenFromDefaultSurface(t *testing.T) {
if !buildtagsCmd.Hidden {
t.Fatal("buildtags must stay Hidden so the default command surface is unchanged")
}
}
+2
View File
@@ -402,6 +402,7 @@ func TestCobraCommandTreeRegistration(t *testing.T) {
"badge",
"batch-feedback",
"beads",
"buildtags",
"canon",
"capabilities",
"chaos-test",
@@ -554,6 +555,7 @@ func TestCobraExpectedCmdsMatchRegistration(t *testing.T) {
"badge",
"batch-feedback",
"beads",
"buildtags",
"canon",
"capabilities",
"chaos-test",
+64
View File
@@ -0,0 +1,64 @@
# Build tags: archiving the satellites (ADR-0012)
> How AgentOps keeps the unproven corpus/flywheel and RPI/factory satellites
> **buildable but omitted by default**. Decision: [ADR-0012](../adr/ADR-0012-focus-surface-on-membrane-bookkeeper-archive-satellites.md).
> The satellites are **archived, not deleted**, because the ADR-0004/0009/0011
> revival conditions require the code to stay compilable.
## The two tags
| Tag | Archives | Bead |
|-----|----------|------|
| `flywheel` | corpus/flywheel commands + packages (forge, compile, wiki, harvest, mine, pool, maturity, refinery, flywheel, dedup, defrag, curate, ratchet, …) | `…m1wg.13` |
| `legacy` | RPI/factory commands (orchestrate, codex lifecycle, tick, autodev, evolve, loop, turn, harness, operator) | `…m1wg.14` |
The **default** `go build ./...` (and the shipped `ao`) compile **neither**. They are the spine.
## How to build each variant
```bash
make build # spine — archived sets omitted (default)
make build-flywheel # restore BOTH (-tags "flywheel legacy")
AGENTOPS_LEGACY=1 make build # restore the legacy (RPI/factory) set only
cd cli && go build -tags flywheel ./... # flywheel only
cd cli && go build -tags "flywheel legacy" ./... # both
```
Introspect a binary: `ao buildtags` prints `spine` (default) or the tags it was built with.
## How to archive a command behind a tag
Build tags are **file-level** in Go. To archive a command:
1. Put the command's definition + its `init()` registration (the `rootCmd.AddCommand(...)`
call) in a file carrying the tag constraint as the **first line**, followed by a blank line:
```go
//go:build flywheel
package main
// … the cobra command var + func init(){ rootCmd.AddCommand(fooCmd) } …
```
With the tag absent, the file is not compiled, so the command is neither built
nor registered — it simply isn't in the spine binary.
2. **Self-containment is the rule.** Any symbol the archived command references
(helper funcs, vars, packages) must also be tag-gated, or the spine build
breaks with "undefined". Move shared helpers that the spine still needs into an
untagged file; move helpers only the archived command uses into the tagged file.
If a spine file references an archived symbol, provide a `//go:build !flywheel`
stub.
3. Whole packages (e.g. `internal/wiki`, `internal/pool`, `internal/ratchet`) are
archived by tagging every file in the package, or by ensuring the only importers
are themselves tag-gated.
4. Retag the disposition ledger and regenerate: `make regen-all`, then `make regen-check`.
## Verifying the mechanism
`make verify-buildtags` (→ `scripts/verify-buildtags.sh`) compiles the spine,
`flywheel`, `legacy`, and combined variants and asserts `ao buildtags` reports
each correctly. The default build must omit the tagged sets; the tags must restore
them. The `cmd/ao/buildtags*.go` files are the mechanism's anchor and self-test.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"schema_version": 2,
"generated_at": "2026-06-30T14:45:12Z",
"generated_at": "2026-06-30T20:43:37Z",
"summary": {
"skills": 75,
"hooks": 0,
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
#
# verify-buildtags.sh — prove the ADR-0012 build-tag archive mechanism.
#
# The default `go build` must OMIT the archived command sets; the `flywheel` and
# `legacy` tags must restore them. We can't grep for archived commands until
# .13/.14 move real commands behind the tags, so this checks the mechanism via
# the hidden `ao buildtags` introspection surface:
# default build -> "spine" (no archive tags compiled in)
# -tags flywheel -> "flywheel"
# -tags legacy -> "legacy"
# -tags flywheel legacy-> "flywheel" + "legacy"
#
# Exit codes: 0 = mechanism works; 1 = a variant did not report as expected;
# 2 = script/build error.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLI="$ROOT/cli"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
build() { # build <out> [tags...]
local out="$1"; shift
local tagflag=()
if [ "$#" -gt 0 ]; then tagflag=(-tags "$*"); fi
( cd "$CLI" && go build "${tagflag[@]}" -o "$out" ./cmd/ao ) || {
echo "FAIL: go build (tags='$*') errored — archived code must stay buildable" >&2
exit 2
}
}
expect() { # expect <binary> <substr> <present|absent>
local bin="$1" needle="$2" mode="$3" out
out="$("$bin" buildtags 2>/dev/null || true)"
# Whole-line match: a tag is emitted on its own line, so the spine's
# explanatory text ("…corpus/flywheel…") never false-matches a tag check.
case "$mode" in
present)
if ! printf '%s\n' "$out" | grep -qx "$needle"; then
echo "FAIL: '$bin buildtags' = '$out' — expected a line '$needle'" >&2
exit 1
fi ;;
absent)
if printf '%s\n' "$out" | grep -qx "$needle"; then
echo "FAIL: '$bin buildtags' = '$out' — expected NO line '$needle'" >&2
exit 1
fi ;;
esac
}
echo "verify-buildtags: default (spine) build…"
build "$TMP/ao-spine"
if ! "$TMP/ao-spine" buildtags 2>/dev/null | grep -q "spine"; then
echo "FAIL: default build should report 'spine'" >&2; exit 1
fi
expect "$TMP/ao-spine" "flywheel" absent
expect "$TMP/ao-spine" "legacy" absent
echo "verify-buildtags: -tags flywheel…"
build "$TMP/ao-flywheel" flywheel
expect "$TMP/ao-flywheel" "flywheel" present
expect "$TMP/ao-flywheel" "legacy" absent
echo "verify-buildtags: -tags legacy (AGENTOPS_LEGACY path)…"
build "$TMP/ao-legacy" legacy
expect "$TMP/ao-legacy" "legacy" present
expect "$TMP/ao-legacy" "flywheel" absent
echo "verify-buildtags: -tags 'flywheel legacy' (make build-flywheel)…"
build "$TMP/ao-all" flywheel legacy
expect "$TMP/ao-all" "flywheel" present
expect "$TMP/ao-all" "legacy" present
echo "OK: build-tag mechanism verified — default build omits archived sets; flywheel/legacy restore them."