docs(learnings): CLI-wiring cycle-shape captured from cycles 144-146

After the 3-cycle slice arc (144-146) closed soc-y5vh.5 and exposed
3 production adapters via the CLI, the cycle-shape itself became
durable IP. This learning captures the template so future cycles
can replicate it for any production adapter without re-discovering
the pattern.

Key content:
- The 3 cycles in numbers (8-10 min wall-clock, ~250-335 LOC, 4-6
  tests each). Variation comes from adapter-side complexity.
- The full cobra/options template (parent noun + verb subcommand +
  injectable function field + line-delimited JSON output).
- 5 'why this shape works' principles (cobra noun-verb, injectable
  fn for tests, JSON line-delimited for shell composability, error
  wrapping, live-smoke verification).
- 3 anti-patterns observed (name collisions in cli/cmd/ao,
  shadowing Go builtins, dead imports — all caught by go vet).
- What this unblocks: any of the 14 production adapters can now be
  CLI-exposed in ~10 min using this template.

Updated docs/learnings/README.md catalog to surface the new
learning. Catalog now has 7 entries; the cli-wiring entry pairs
with cycle-122's wire-up-arc and cycle-127's substring-sed-overreach
as the 3 BC-ports-arc-derived rules.

Why this learning matters:
- The remaining BC ports (Citation, Operator, Harness,
  ClaimEvidenceBinder, GateRunner, FactoryAdmission, ClaimEvidence,
  EventBus, etc.) lacking CLI exposure are now cheap to add — the
  template removes design overhead.
- skills/evolve/SKILL.md Step 0/1.5 callers (soc-y5vh.1, .2)
  can consult this learning when wiring the new shell-outs.
- Future operator who wants to expose a new adapter doesn't have
  to re-derive the parent-noun + verb-subcommand + injectable-fn
  decision space.

Companion to cycle-122 wire-up-arc learning. Same arc shipped a
durable code surface AND now a durable how-to surface.

Cycle 147 / soc-y5vh.5 retrospective-learning mode.
This commit is contained in:
Boden Fuller
2026-05-12 21:25:24 -04:00
parent b567bdd20c
commit 8df9f2d7cf
2 changed files with 160 additions and 0 deletions
@@ -0,0 +1,159 @@
---
title: CLI wiring is a repeatable cycle-shape — 3 production adapters exposed in 3 cycles
date: 2026-05-13
tags: [hexagonal-architecture, ddd-bounded-context, cli, cobra, wire-up, cycle-shape]
source: cycles 144-146 of the 2026-05-12 → 2026-05-13 /evolve session
companion: 2026-05-13-bc-ports-wire-up-arc.md
---
# CLI-wiring cycle-shape
Cycles 144-146 shipped three `ao` subcommands that expose three
production adapters from the 14-port wire-up arc. Each cycle had the
same shape and the same cost. This learning captures the template
so future cycles can replicate it for any production adapter.
## The 3 cycles in numbers
| Cycle | Subcommand | Adapter | Time | LOC | Tests |
|---|---|---|---|---|---|
| 144 | `ao loop history` | `productionLoopReader` (cycle 108) | ~10 min | 335 | 6 |
| 145 | `ao ci latest/recent` | `productionCIStatus` (cycle 117) | ~8 min | 258 | 4 |
| 146 | `ao corpus inject` | `productionCorpusReader` (cycle 112) | ~8 min | 252 | 5 |
8-10 minutes wall-clock. ~250-335 LOC including tests + docs. The
adapter-side complexity dominates the variation — Loop took longer
because it needed JSON slicing logic; CI was simplest because the
adapter already had a clean stub-injectable shape.
## The template
```go
// cli/cmd/ao/<noun>.go
var <noun>Cmd = &cobra.Command{
Use: "<noun>",
Short: "BC<n> <surface> operations",
}
var <noun><Verb>Cmd = &cobra.Command{
Use: "<verb> [flags]",
Short: "Short imperative description",
Long: `Long description with Examples block.`,
RunE: run<Noun><Verb>,
}
type <noun><Verb>Options struct {
// flag-derived fields
writer io.Writer
// injectFn lets tests substitute the port without real I/O
injectFn func(ctx context.Context, opts <noun><Verb>Options) ([]ports.X, error)
}
func init() {
<noun>Cmd.GroupID = "core"
rootCmd.AddCommand(<noun>Cmd)
// flag registrations
<noun>Cmd.AddCommand(<noun><Verb>Cmd)
}
func run<Noun><Verb>(cmd *cobra.Command, _ []string) error {
// pull flag values, build options, delegate
return <noun><Verb>Run(cmd.Context(), opts)
}
func <noun><Verb>Run(ctx context.Context, opts <noun><Verb>Options) error {
if opts.writer == nil { opts.writer = os.Stdout }
fn := opts.injectFn
if fn == nil { fn = <noun><Verb>ViaPort }
items, err := fn(ctx, opts)
if err != nil { return fmt.Errorf("<noun> <verb>: %w", err) }
enc := json.NewEncoder(opts.writer)
for _, item := range items {
if err := enc.Encode(item); err != nil {
return fmt.Errorf("<noun> <verb> encode: %w", err)
}
}
return nil
}
func <noun><Verb>ViaPort(ctx context.Context, opts <noun><Verb>Options) ([]ports.X, error) {
adapter := newProduction<X>(/* construction args */)
return adapter.<Method>(ctx, /* args */)
}
```
```go
// cli/cmd/ao/<noun>_test.go
// 4-6 tests covering:
// - stub returns N items → N lines emitted
// - stub returns empty → 0 bytes emitted
// - stub error → wrapped error
// - live root (filesystem fixture) → walks correctly
// - flag combinations (limit, range, etc.) honored
```
After the .go + _test.go files: `scripts/generate-cli-reference.sh`
regenerates `cli/docs/COMMANDS.md`.
## Why this shape works
1. **Parent noun + verb subcommands.** `ao loop history` reads
better than `ao loop-history`. The parent groups future
subcommands (`ao loop write`, `ao loop tail`) under one verb-
space. cobra handles this natively.
2. **Injectable function field on Options.** Production runs use
the default port wrapper; tests substitute a stub. This is the
same pattern cycle 117's `productionCIStatus.runGH` proved —
refined here from a struct field to an option-bag function.
Faster than fake-file-tree harnesses and platform-neutral.
3. **Line-delimited JSON output.** One record per line means the
output composes with `jq -c`, `head`, `grep`, `awk`. Operators
don't need to remember the schema; they pipe and `jq '.field'`.
4. **Error wrapping with the command name.** `"<noun> <verb>:
underlying error"` makes debugging easy when the cobra layer
surfaces an error to stderr.
5. **Validate by live smoke after build.** Each cycle ran `make
build` then `./bin/ao <noun> <verb> <args>` against the real
data. Cycle 146's smoke ranked the wire-up-arc learning as the
top match for "wire-up" — proves end-to-end semantic
correctness, not just compilation.
## Anti-patterns observed during the 3 cycles
- **Name collisions in cli/cmd/ao.** Cycle 144's first helper was
named `loadCycleHistory` — collided with an existing function in
`metrics_health.go`. `go vet` caught it; renamed to
`loadCycleHistoryViaPort`. **Always grep before naming helpers
in cli/cmd/ao** (the package is ~150 files).
- **Don't shadow Go builtins.** Cycle 117 used `cap := limit`; same
rule applies to CLI helpers.
- **Don't import `internal/ports` into test files needlessly.**
Cycle 144's first test file had a dead import; go vet caught it.
## What this unblocks
Three cycles (144-146) cleared the cycle-122 wire-up arc's named
gap: "production adapters are latent until cross-package consumer
exists." Future:
- Any production adapter can be exposed in ~10 min with this template.
- The remaining BC ports lacking CLI exposure (Citation, Operator,
Harness, ClaimEvidenceBinder, GateRunner, FactoryAdmission,
ClaimEvidence, etc.) are now low-cost surfaces to add when needed.
- skills/evolve/SKILL.md Step 0/1.5 prior-failure-injection and
healing-classifier callers (soc-y5vh.1, .2) can shell out to
these subcommands instead of reaching for inline awk/jq/gh.
## See also
- `2026-05-13-bc-ports-wire-up-arc.md` (cycle 122) — the arc that
built the 12 production adapters this template wraps.
- `2026-05-13-substring-sed-rename-overreach.md` (cycle 127) —
prevention rule that applies to grep-before-name in this template.
- `cli/cmd/ao/loop.go`, `cli/cmd/ao/ci.go`, `cli/cmd/ao/corpus_inject.go`
— the three reference implementations.
- soc-y5vh.5 (closed cycle 146) — the bd that tracked these 3 slices.
+1
View File
@@ -13,6 +13,7 @@ layer in between.
| Date | File | One-line takeaway |
|---|---|---|
| 2026-05-13 | [cli-wiring-cycle-shape.md](2026-05-13-cli-wiring-cycle-shape.md) | CLI-wiring is a repeatable ~10-min cycle-shape: parent noun + verb subcommand + injectable func; 3 production adapters exposed in 3 cycles. |
| 2026-05-13 | [loop-context-drift-87-cycle-observation.md](2026-05-13-loop-context-drift-87-cycle-observation.md) | `/loop` context accumulates but disk-state ledger protects correctness — soc-wx55q.1 P1 may be P3. |
| 2026-05-13 | [substring-sed-rename-overreach.md](2026-05-13-substring-sed-rename-overreach.md) | Before any bulk `sed` rename, enumerate ALL substring-containing identifiers and classify by concept. |
| 2026-05-13 | [bc-ports-wire-up-arc.md](2026-05-13-bc-ports-wire-up-arc.md) | 12 production adapters in 11 cycles via repeatable file-triplet pattern; 8 distinct adapter shapes proven. |