AgentOps now describes the selected Context Delivery Lifecycle alongside standalone RPI: external reviewed memory, native work and runtime ownership, independent validation, and explicit limits on what the pilots proved. The active ADRs name which historical prescriptions are superseded and which invariants remain. This integrates the BD foundation, including doctor diagnostics that distinguish BD from BR and protection against private tracker data entering outgoing Git history. It also fixes skills audit resolution of explicit relative reference paths. Source-owned projections were regenerated, and the landed stopping behavior is preserved. Validation: Go build/vet/test and race/shuffle, 1,325 Bats cases, the local aggregate, and full AO gates. The gate registry reports 71 passes and one nonblocking preamble-adoption failure on the existing report script; that advisory remains disclosed. Historical probe receipts whose source hashes changed remain historical evidence. This is foundation adoption. It does not claim that the later memory, goal, background, privacy-release, or benefit-measurement work is implemented. Independent Astra and Fable judgments both PASS over the exact integrated content. Earlier negative results and their factual resolution remain preserved in private work evidence.
16 KiB
Hexagon Port-Realness Audit
RETIRED / HISTORICAL (snapshot 2026-05-23). Runtime and tracker choices below describe that dated inventory, not current instructions. The repository now uses latest stable BD; the legacy BR estate is preserved migration history. Read AGENTS.md and current command help for live behavior.
Empirical ground-truth inventory of the AgentOps runtime hexagonal seam, taken 2026-05-23 against
main. Bead:soc-upwor(epicsoc-zvhsl— "3.0: make the hexagon real"). This is the disjoint adapter backlog the adapter-building beads execute against:soc-rg5z0(WorkspacePort),soc-ebgjk(TrackerPort),soc-21mrg(loop-state),soc-vzyhn(corpus FS).Companion to Ports and Adapters (conceptual overview). This audit corrects two stale claims in that doc and in the epic survey: the hexagon is not "only
storage_fsis real" — sixteenproduction*adapters exist incli/cmd/ao/(compile-cycles 112/113) plus three incli/internal/wiki/. The real gaps are narrower and more specific, enumerated below.
Headline counts
| Metric | Count |
|---|---|
Declared port interfaces (cli/internal/ports/*.go, non-inmemory, non-_test) |
26 |
Ports with an in-memory double (inmemory_*.go) |
20 |
| Ports with a real (non-in-memory) adapter | 18 |
| Ports with no conforming adapter at all (in-memory-only or signature-mismatched) | 8 |
Domains called directly from cli/cmd/ao with no port (git, bd) |
2 (git: 35 callsites, bd: 10 callsites) |
The epic soc-zvhsl survey says "21 ports declared, only storage_fs has a real
adapter." Both numbers are stale: 26 interfaces are declared, and 18 have real
adapters. The accurate backlog is the 8 no-adapter ports plus the 2 directly-coupled
domains, scoped below.
Port realness table
Legend — declared?: interface exists in cli/internal/ports/. in-mem?: an
inmemory_*.go double exists. real?: a non-in-memory adapter that satisfies the
interface exists. consumed: via-port = a command/package calls the adapter
through the port's request/result types; bypassed = the domain is reached without
the port; unwired = real adapter exists but no command constructs it.
| Port | declared? | in-mem? | real? | consumed | real-adapter / evidence (file:line) |
|---|---|---|---|---|---|
PacketRepository |
✅ storage.go:12 |
❌ | ✅ | via-port | adapters/storage_fs/packet_repo.go:28 (var _ ports.PacketRepository) |
CIStatusPort |
✅ ci_status.go:67 |
✅ | ✅ | via-port | cmd/ao/ci_status_adapter.go:128; wired cmd/ao/ci.go:115; runs gh at ci_status_adapter.go:39 |
CitationPort |
✅ citation.go:78 |
✅ | ✅ | via-port | cmd/ao/citation_port_adapter.go:82; wired cmd/ao/citation_cmd.go:100 |
ClaimEvidencePort |
✅ claim_evidence.go:57 |
✅ | ✅ | via-port | cmd/ao/claim_evidence_adapter.go:104 |
ClaimEvidenceBinderPort |
✅ claim_evidence_binder.go:69 |
✅ | ✅ | via-port | cmd/ao/claim_evidence_binder_adapter.go:199; wired cmd/ao/claim_cmd.go:166 |
ConvergenceCheckPort |
✅ convergence_check.go:68 |
✅ | ✅ | via-port | cmd/ao/convergence_check_adapter.go:63; wired cmd/ao/loop_converged.go:126 |
CorpusReaderPort |
✅ corpus_reader.go:48 |
✅ | ✅ | via-port (partial) | cmd/ao/corpus_reader_adapter.go:145; wired cmd/ao/corpus_inject.go:107; real walk corpus_reader_adapter.go:64 |
CorpusWriterPort |
✅ corpus_writer.go:52 |
✅ | ✅ | via-port (partial) | cmd/ao/corpus_writer_adapter.go:119; wired cmd/ao/corpus_capture.go:189 |
EventBusPort |
✅ event_bus.go:45 |
✅ | ✅ | via-port | cmd/ao/event_bus_adapter.go:156 |
FactoryAdmissionPort |
✅ factory_admission.go:75 |
✅ | ✅ | via-port | cmd/ao/factory_admission_adapter.go:100 |
FindingCompilerPort |
✅ finding_compiler.go:64 |
✅ | ✅ | via-port | cmd/ao/finding_compiler_adapter.go:167 |
GateRunnerPort |
✅ gate_runner.go:69 |
✅ | ✅ | via-port | cmd/ao/gate_runner_adapter.go:115; wired cmd/ao/gate_cmd.go:93; runs bash scripts/check-*.sh gate_runner_adapter.go:53,61 |
HarnessPort |
✅ harness.go:49 |
✅ | ✅ | via-port | cmd/ao/harness_adapter.go:131; wired cmd/ao/harness_cmd.go:100 |
HypothesisLedgerPort |
✅ hypothesis_ledger.go:52 |
✅ | ✅ | via-port | cmd/ao/hypothesis_ledger_adapter.go:158; wired cmd/ao/loop_hypothesis.go:103 |
LoopReaderPort |
✅ loop_reader.go:57 |
✅ | ✅ | via-port (evolve lane only) | cmd/ao/loop_reader_adapter.go:158; wired cmd/ao/loop.go:142,229 (reads .agents/evolve/cycle-history.jsonl) |
LoopWriterPort |
✅ loop_writer.go:25 |
✅ | ✅ | via-port (evolve lane only) | cmd/ao/loop_writer_adapter.go:131; wired cmd/ao/loop_append.go:141 |
OperatorPort |
✅ operator.go:35 |
✅ | ✅ | via-port | cmd/ao/operator_adapter.go:123; wired cmd/ao/operator_cmd.go:148 |
WikiIndexPort |
✅ wiki_index.go:45 |
❌ | ✅ | via-port | cli/internal/wiki/index.go:80,103 (Reindex/Records); wired cmd/ao/wiki.go:213,217 |
FrontmatterCodecPort |
✅ frontmatter_codec.go:46 |
❌ | ✅ | unwired | cli/internal/wiki/frontmatter.go:223 (PortCodec shim, NewPortCodec at :227); no command constructs it |
FreshnessPolicyPort |
✅ freshness_policy.go:66 |
❌ | ⚠️ no conforming | bypassed | cli/internal/wiki/freshness.go:189 is the production logic but Evaluate (freshness.go:228) uses domain types, not the port signature Evaluate(claimVolatility, claimAuthority string, …) (freshness_policy.go:68) — no shim exists |
CloseoutPort |
✅ closeout.go:42 |
✅ | ❌ | bypassed | in-memory-only (inmemory_closeout.go); required-7 |
ContextCompilerPort |
✅ context_compiler.go:54 |
✅ | ❌ | bypassed | in-memory-only (inmemory_context_compiler.go); required-7 |
SafetyPolicyPort |
✅ safety_policy.go:51 |
✅ | ❌ | bypassed | in-memory-only (inmemory_safety_policy.go); required-7 |
WorkspacePort |
✅ workspace.go:32 |
✅ | ❌ | bypassed | in-memory-only (inmemory_workspace.go), not constructed anywhere; required-7 → soc-rg5z0 |
IssueTracker |
✅ tracker.go:7 |
❌ | ❌ | bypassed | no adapter, no in-memory double; bd reached directly → soc-ebgjk |
LLMClient |
✅ llm.go:7 |
❌ | ❌ | n/a | no adapter, no in-memory double (forecast port) |
8 no-adapter ports: FreshnessPolicyPort, CloseoutPort, ContextCompilerPort,
SafetyPolicyPort, WorkspacePort, IssueTracker, LLMClient — plus
FrontmatterCodecPort whose real adapter exists but is unwired (counts as
"real" in the realness column but is a latent gap).
Required-port set (scripts/check-hook-port-replacements.sh)
The CI gate enforces seven ports as hook-replacement targets:
Closeout / ContextCompiler / EventBus / GateRunner / Harness / SafetyPolicy / Workspace
(scripts/check-hook-port-replacements.sh, REQUIRED_PORTS array). Their backing:
| Required port | Real adapter? | Notes |
|---|---|---|
EventBusPort |
✅ | cmd/ao/event_bus_adapter.go:156 |
GateRunnerPort |
✅ | cmd/ao/gate_runner_adapter.go:115 — shells real check-*.sh |
HarnessPort |
✅ | cmd/ao/harness_adapter.go:131 |
CloseoutPort |
❌ in-memory-only | no production* / fs adapter |
ContextCompilerPort |
❌ in-memory-only | no production* / fs adapter |
SafetyPolicyPort |
❌ in-memory-only | no production* / fs adapter |
WorkspacePort |
❌ in-memory-only | not even constructed; soc-rg5z0 |
So 3 of the required 7 are real; 4 are in-memory-only (Closeout, ContextCompiler,
SafetyPolicy, Workspace). The gate only checks that the interface + in-memory double
exist and are referenced by a non-remove hook lease — it does not assert a real
adapter, which is exactly why these four passed CI while remaining doubles.
Direct-coupling hotspots
Domains the CLI reaches without going through a port. These are the bypass surfaces the adapter beads collapse.
git — 35 direct exec.Command("git", …) callsites in cmd/ao (→ soc-rg5z0 WorkspacePort)
Worktree lifecycle and repo queries shell out to git directly. WorkspacePort
(workspace.go:32, Setup/Cleanup) is declared to absorb exactly this, but its
only impl is inmemory_workspace.go and nothing constructs it. Representative
worktree-lifecycle coupling (the load-bearing subset for soc-rg5z0):
cmd/ao/rpi_parallel.go:281—git worktree add -b <branch> <path>cmd/ao/rpi_parallel.go:200—git worktree remove --force <path>cmd/ao/rpi_cleanup.go:258—git worktree list --porcelaincmd/ao/rpi_cleanup.go:464—git worktree remove --force <path>cmd/ao/rpi_cleanup.go:487—git worktree prunecmd/ao/worktree.go:180—git rev-parse --show-toplevelcmd/ao/worktree.go:381—git -C <wt> status --porcelain -uallcmd/ao/rpi_parallel.go:412—git merge <branch> --no-ff
Plus repo-root/status reads scattered across beads_resume.go:267, curate.go:233,
hooks_run.go:156,394, init.go:420, vibe_check.go:114, rpi_status.go:761,
beads_audit_cluster.go:120. There is no central git wrapper — each callsite
builds its own exec.Command. (Helpers worktree_config.go, internal/rpi/worktree.go
exist but do not gate the subprocess.)
bd — 10 direct exec.Command("bd", …) callsites in cmd/ao (→ soc-ebgjk TrackerPort)
IssueTracker (tracker.go:7, Mode/CreateEpic/CreateIssue) has no
implementation at all — no real adapter and no in-memory double. bd is invoked
directly:
cmd/ao/beads.go:42— genericbd <args>passthroughcmd/ao/session_bootstrap.go:210—bd ready --jsoncmd/ao/beads_resume.go:66—bd show <id> --jsoncmd/ao/beads_stale.go:140—bd list --status in_progress --json …cmd/ao/plans.go:710—bd list --type epic --jsoncmd/ao/quickstart.go:474—bd init --prefix <p>cmd/ao/overnight_packets.go:961,995,1025—bd list … --jsoncmd/ao/beads_audit_cluster.go(git, listed above)
IssueTracker's declared surface (epic/issue creation) is narrower than the actual
bd usage (mostly read/list). soc-ebgjk should widen the port to cover the
ready/list/show read paths the CLI actually depends on, not just creation.
loop-state — split-lane persistence (→ soc-21mrg)
The soc-21mrg bead and the epic survey cite .agentops/loop-state.json read/written
in rpi_phased_*. That literal path does not exist anywhere in cli/ — the claim
is imprecise. The real situation is two distinct loop-persistence lanes:
-
Evolve lane —
.agents/evolve/cycle-history.jsonl, already behindLoopReaderPort/LoopWriterPortviaproductionLoopReader/Writer(loop_reader_adapter.go:65, wiredloop.go:142). Already a real adapter. -
RPI supervisor lane —
rpi_loop_supervisor.gopersists its own state with rawjson.MarshalIndent+os.WriteFile/os.ReadFile, bypassing the ports:rpi_loop_supervisor.go:1007—json.MarshalIndent(l.meta, …)(meta file)rpi_loop_supervisor.go:1029,1034—os.ReadFile+json.Unmarshalof metarpi_loop_supervisor.go:1071,1077— append totelemetry.jsonlrpi_loop_supervisor.go:1131,1139—json.Marshal+ atomicos.WriteFile(tmp)Noterpi_status.go:677also reads a fallback flatphased-state.json.
⚠️
rpi_loop_supervisor.gois the legacy RPI lane the repo CLAUDE.md flags as load-bearing-but-frozen ("do not write new tests or features for the legacy RPI lane").soc-21mrgmust target the live loop-state surface, not retrofit the frozen supervisor. Confirm which lane is current (gc bridge vs legacy tmux) before building; the atomic-file adapter belongs behindLoopReaderPort/LoopWriterPortcovering the live lane, not a new write to the frozen one.
corpus FS — compile/mine/grow/defrag walk .agents directly (→ soc-vzyhn)
CorpusReaderPort/CorpusWriterPort have real adapters
(corpus_reader_adapter.go:64 does a real filepath.WalkDir), wired into the
corpus inject/corpus capture commands. But the knowledge-flywheel core
(compile = mine/grow/defrag) does not use them — it walks the filesystem directly:
cmd/ao/compile.go:464,561—os.ReadDir(target)over.agents/compiledcli/internal/corpus/fitness.go— direct corpus walkcli/internal/context/run.go— direct corpus walk (context assembly)cmd/ao/context_assemble.go:346—os.ReadDir(dir)cmd/ao/maturity.go:472,945—os.ReadDir/filepath.WalkDirover.agents/learningscmd/ao/knowledge.go:598,648,knowledge_native.go:267,685—os.ReadDircli/internal/doctor/fix_knowledge.go,internal/daemon/{snapshot,wiki_jobs,store}.go
50 filepath.Walk/WalkDir/os.ReadDir callsites live in cmd/ao non-adapter files
alone. soc-vzyhn routes the mine/grow/defrag core through CorpusReaderPort/
CorpusWriterPort so the flywheel becomes testable without on-disk fixtures.
gh / CI — already behind a port
gh is invoked only inside the CIStatusPort adapter (ci_status_adapter.go:39),
which is the correct pattern. No CI/gh direct-coupling backlog. Gate execution is
likewise behind GateRunnerPort (gate_runner_adapter.go:53,61 shells check-*.sh).
Recommended adapter build order
Priority by load-bearing-ness × bypass blast radius, mapped to existing beads.
-
WorkspacePort→cli/internal/adapters/workspace_git/(soc-rg5z0, P1). Highest leverage: 35 directgitcallsites, the port is declared and not even constructed, and worktree lifecycle is load-bearing for the multi-agent discipline (bd worktree createis mandatory per CLAUDE.md). Collapse therpi_parallel.go/rpi_cleanup.go/worktree.goworktree subset behindSetup/Cleanupfirst; leave read-onlyrev-parsequeries for a follow-up. Closes one of the 4 in-memory-only required-7 ports. -
IssueTracker→ bd-backed adapter (soc-ebgjk, P1).bdhas 10 direct callsites and the port has zero implementations (not even an in-memory double — build that for L1/L2 testability too). Widen the port to the read paths (ready/list/show) the CLI actually uses before wiring, then atasklistfilesystem fallback for theMode() == "tasklist"branch. -
CorpusReader/CorpusWriterPortadoption in the flywheel core (soc-vzyhn, P2, "L"). The adapters already exist and are real — the work is migration, not greenfield: routecompile(compile.go),internal/corpus/fitness.go, andinternal/context/run.gothrough the existing ports instead of direct walks. Lower risk than 1–2 (no new I/O surface), but largest in scope. Decouples the knowledge-flywheel from file layout and makes mine/grow/defrag fixture-free. -
Loop-state atomic adapter for the live lane (
soc-21mrg, P2). The evolve lane is already real; the gap is the RPI supervisor lane — but that lane is frozen legacy code. Resolve the lane question first (gc bridge is the live path per CLAUDE.md). Build the atomic-file adapter behind the existingLoopReaderPort/LoopWriterPortfor the live loop-state surface; do not add features torpi_loop_supervisor.go.
Out-of-scope / deferred (no bead yet)
CloseoutPort,ContextCompilerPort,SafetyPolicyPort— the other three in-memory-only required-7 ports. Real adapters needed to satisfy the spirit (not just the letter) ofcheck-hook-port-replacements.sh. File follow-up beads undersoc-zvhsl.FreshnessPolicyPort— production logic exists incli/internal/wiki/freshness.gobut the method signature does not match the port; add aPortFreshnessPolicyshim (mirror thePortCodecpattern atfrontmatter.go:223).FrontmatterCodecPort— realPortCodecadapter exists but is unwired; a one-line wiring in the frontmatter consumer closes it.LLMClient— forecast port (llm.go:7); no adapter and no consumer. Defer.check-hook-port-replacements.shgate gap — consider extending the gate to assert a non-in-memory adapter for required ports so a future port can't regress to a double silently.