moai-adk-go Best Practices
TRUST 5 Quality Gates
Every change must pass all five dimensions before completion:
| Pillar |
Gate |
Failure action |
| Tested |
go test ./... with coverage |
Block merge; generate missing tests |
| Readable |
golangci-lint run |
Warn; suggest refactoring |
| Unified |
go fmt + goimports |
Auto-format or warn |
| Secured |
OWASP-aligned review (per-spawn opus agent) |
Block; require review |
| Trackable |
Conventional Commits regex |
Suggest format |
Coverage targets: 85% package minimum; 90%+ for critical packages
(internal/cli, internal/template, internal/hook).
Test Isolation
- Always
t.TempDir() for temp dirs — auto-cleanup, under os.TempDir().
- macOS path pitfall:
t.TempDir() returns /var/folders/.... Go's
filepath.Join(cwd, absPath) does NOT strip the leading /:
filepath.Join("/a/b", "/var/folders/x") → "/a/b/var/folders/x" (WRONG).
Use filepath.Abs() when resolving user-supplied paths in CLI commands.
- No OTEL env in parallel tests (CLAUDE.local.md §WARN): never
t.Setenv("OTEL_EXPORTER_*", ...) in parallel tests — the OTEL SDK
initializes global state from env vars on first use, causing data races. Use
a fake/no-op exporter instead; or make the parent test non-parallel.
- No
t.Setenv("HOME", tmpDir) in GLM integration tests — parallel-test
pollution. Use t.TempDir() + explicit path construction.
- After fixing any test, run the FULL suite (
go test ./...) to catch
cascading failures. Use -count=1 to disable caching when debugging flaky
tests; use -race for concurrency-safety checks.
Hardcoding Prevention
- URLs / model names / org names / API headers → extract to
const.
- Environment variable names → define in
internal/config/envkeys.go as
constants; reference the constant everywhere. Never inline a raw env string.
- Thresholds → single source in
internal/config/defaults.go. Never
duplicate a threshold across packages.
- Cross-platform paths → prefer
$HOME, HOMEBREW_PREFIX, etc. In
.sh.tmpl fallback paths use $HOME (not .HomeDir), because .HomeDir
freezes at moai init time and breaks for users with non-standard layouts.
- Hardcoding allowed only in
CLAUDE.local.md, settings.local.json,
and _test.go files inside t.TempDir().
AskUserQuestion Boundary (orchestrator-only)
AskUserQuestion is the ONLY user-facing question channel, and it is
reserved for the MoAI orchestrator (main session).
- Subagents (including these harness specialists) MUST NOT invoke
AskUserQuestion. If user input is required, return a structured blocker
report to the orchestrator (see
.claude/rules/moai/core/askuser-protocol.md
§ Blocker Report Format).
- Deferred-tool preload:
AskUserQuestion, TaskCreate, TaskUpdate,
TaskList, TaskGet are deferred tools — schema not loaded at session
start. The orchestrator MUST call
ToolSearch(query: "select:AskUserQuestion,TaskCreate,...") before first
use. Subagents inherit this constraint.
- Free-form prose questions in response text are prohibited — always route
through AskUserQuestion (orchestrator) or a blocker report (subagent).
Archived-Agent Rejection Contract
12 agents are ARCHIVED and MUST NOT be referenced anywhere in generated
harness files — no delegates-to, no prose, no examples. The full list of
archived names lives in the canonical SSOT at
.claude/rules/moai/workflow/archived-agent-rejection.md §B; this skill does
not repeat the literal names (repeating them in every generated file would
re-seed the exact tokens the rejection contract is meant to suppress).
The 8 RETAINED agents are the only valid delegation targets:
manager-spec, manager-develop, manager-docs, manager-git,
plan-auditor, sync-auditor, builder-harness, Explore (Anthropic built-in)
For domain expertise formerly provided by the archived domain-expert agents,
use the per-spawn pattern: Agent(subagent_type: "general-purpose", model: "opus", tools: "<whitelist>", prompt: "...<domain> specialist: <conventions>...") at delegation time. See
.claude/rules/moai/workflow/archived-agent-rejection.md §C for the full
migration table (rows #1-#12), which maps each archived agent to its
canonical retained-agent or per-spawn replacement.
Verification-Claim Integrity
Per .claude/rules/moai/core/verification-claim-integrity.md:
- No unobserved claims. A "tests pass" / "coverage 87%" / "lint clean"
assertion is valid ONLY when the actor ran the command and observed the
output. An unran command is a gap, never a pass.
- No unobserved defect claims. Inferring a defect/debt/drift from
frontmatter text or grep matches alone — without the domain's dedicated tool
(
moai spec audit, go test -cover, golangci-lint) — is a hypothesis,
not a verified defect. The 2026-06-17 incident (29 SPECs wrongly flagged as
"Mx-close debt"; moai spec audit showed all 29 were grandfather-protected)
is the canonical worked example.
- Baseline attribution. Every verification claim names the command run +
the verbatim output observed, measured against this tree in this run. A
number from a different SPEC/package/time is a carry-over, not a baseline.
- 5-section report format: Claim / Evidence / Baseline-attribution / Gaps
/ Residual-risk. The Gaps section is the defense — force yourself to
enumerate what was NOT observed.
Cross-References
- CLAUDE.local.md §6 (testing), §14 (hardcoding), §19 (AskUserQuestion)
.claude/rules/moai/core/verification-claim-integrity.md v1.1.0
.claude/rules/moai/core/askuser-protocol.md
.claude/rules/moai/workflow/archived-agent-rejection.md
.claude/rules/moai/development/coding-standards.md
1---2name: harness-moaiadk-best-practices3description: moai-adk-go best-practices reference for the 4 harness specialists (cli-template-specialist, quality-specialist, workflow-specialist, hook-ci-specialist). Covers TRUST 5 gates, Go test isolation (t.TempDir, no OTEL env in parallel tests), hardcoding-prevention rules (env constants in envkeys.go, thresholds in defaults.go), the AskUserQuestion orchestrator-only boundary, the deferred-tool preload rule, the archived-agent rejection contract, and verification-claim integrity. Loaded by the specialists when authoring or reviewing moai-adk-go code.4---56# moai-adk-go Best Practices78## TRUST 5 Quality Gates910Every change must pass all five dimensions before completion:1112| Pillar | Gate | Failure action |13|--------|------|----------------|14| **Tested** | `go test ./...` with coverage | Block merge; generate missing tests |15| **Readable** | `golangci-lint run` | Warn; suggest refactoring |16| **Unified** | `go fmt` + `goimports` | Auto-format or warn |17| **Secured** | OWASP-aligned review (per-spawn opus agent) | Block; require review |18| **Trackable** | Conventional Commits regex | Suggest format |1920Coverage targets: 85% package minimum; 90%+ for critical packages21(`internal/cli`, `internal/template`, `internal/hook`).2223## Test Isolation2425- **Always** `t.TempDir()` for temp dirs — auto-cleanup, under `os.TempDir()`.26- **macOS path pitfall**: `t.TempDir()` returns `/var/folders/...`. Go's27 `filepath.Join(cwd, absPath)` does NOT strip the leading `/`:28 `filepath.Join("/a/b", "/var/folders/x")` → `"/a/b/var/folders/x"` (WRONG).29 Use `filepath.Abs()` when resolving user-supplied paths in CLI commands.30- **No OTEL env in parallel tests** (CLAUDE.local.md §WARN): never31 `t.Setenv("OTEL_EXPORTER_*", ...)` in parallel tests — the OTEL SDK32 initializes global state from env vars on first use, causing data races. Use33 a fake/no-op exporter instead; or make the parent test non-parallel.34- **No `t.Setenv("HOME", tmpDir)`** in GLM integration tests — parallel-test35 pollution. Use `t.TempDir()` + explicit path construction.36- **After fixing any test**, run the FULL suite (`go test ./...`) to catch37 cascading failures. Use `-count=1` to disable caching when debugging flaky38 tests; use `-race` for concurrency-safety checks.3940## Hardcoding Prevention4142- **URLs / model names / org names / API headers** → extract to `const`.43- **Environment variable names** → define in `internal/config/envkeys.go` as44 constants; reference the constant everywhere. Never inline a raw env string.45- **Thresholds** → single source in `internal/config/defaults.go`. Never46 duplicate a threshold across packages.47- **Cross-platform paths** → prefer `$HOME`, `HOMEBREW_PREFIX`, etc. In48 `.sh.tmpl` fallback paths use `$HOME` (not `.HomeDir`), because `.HomeDir`49 freezes at `moai init` time and breaks for users with non-standard layouts.50- **Hardcoding allowed** only in `CLAUDE.local.md`, `settings.local.json`,51 and `_test.go` files inside `t.TempDir()`.5253## AskUserQuestion Boundary (orchestrator-only)5455- `AskUserQuestion` is the ONLY user-facing question channel, and it is56 reserved for the MoAI orchestrator (main session).57- **Subagents (including these harness specialists) MUST NOT invoke58 AskUserQuestion.** If user input is required, return a structured blocker59 report to the orchestrator (see `.claude/rules/moai/core/askuser-protocol.md`60 § Blocker Report Format).61- **Deferred-tool preload**: `AskUserQuestion`, `TaskCreate`, `TaskUpdate`,62 `TaskList`, `TaskGet` are deferred tools — schema not loaded at session63 start. The orchestrator MUST call64 `ToolSearch(query: "select:AskUserQuestion,TaskCreate,...")` before first65 use. Subagents inherit this constraint.66- Free-form prose questions in response text are prohibited — always route67 through AskUserQuestion (orchestrator) or a blocker report (subagent).6869## Archived-Agent Rejection Contract707112 agents are ARCHIVED and MUST NOT be referenced anywhere in generated72harness files — no `delegates-to`, no prose, no examples. The full list of73archived names lives in the canonical SSOT at74`.claude/rules/moai/workflow/archived-agent-rejection.md` §B; this skill does75not repeat the literal names (repeating them in every generated file would76re-seed the exact tokens the rejection contract is meant to suppress).7778The 8 RETAINED agents are the only valid delegation targets:7980```81manager-spec, manager-develop, manager-docs, manager-git,82plan-auditor, sync-auditor, builder-harness, Explore (Anthropic built-in)83```8485For domain expertise formerly provided by the archived domain-expert agents,86use the per-spawn pattern: `Agent(subagent_type: "general-purpose", model:87"opus", tools: "<whitelist>", prompt: "...<domain> specialist:88<conventions>...")` at delegation time. See89`.claude/rules/moai/workflow/archived-agent-rejection.md` §C for the full90migration table (rows #1-#12), which maps each archived agent to its91canonical retained-agent or per-spawn replacement.9293## Verification-Claim Integrity9495Per `.claude/rules/moai/core/verification-claim-integrity.md`:9697- **No unobserved claims.** A "tests pass" / "coverage 87%" / "lint clean"98 assertion is valid ONLY when the actor ran the command and observed the99 output. An unran command is a gap, never a pass.100- **No unobserved defect claims.** Inferring a defect/debt/drift from101 frontmatter text or grep matches alone — without the domain's dedicated tool102 (`moai spec audit`, `go test -cover`, `golangci-lint`) — is a hypothesis,103 not a verified defect. The 2026-06-17 incident (29 SPECs wrongly flagged as104 "Mx-close debt"; `moai spec audit` showed all 29 were grandfather-protected)105 is the canonical worked example.106- **Baseline attribution.** Every verification claim names the command run +107 the verbatim output observed, measured against this tree in this run. A108 number from a different SPEC/package/time is a carry-over, not a baseline.109- **5-section report format**: Claim / Evidence / Baseline-attribution / Gaps110 / Residual-risk. The Gaps section is the defense — force yourself to111 enumerate what was NOT observed.112113## Cross-References114115- CLAUDE.local.md §6 (testing), §14 (hardcoding), §19 (AskUserQuestion)116- `.claude/rules/moai/core/verification-claim-integrity.md` v1.1.0117- `.claude/rules/moai/core/askuser-protocol.md`118- `.claude/rules/moai/workflow/archived-agent-rejection.md`119- `.claude/rules/moai/development/coding-standards.md`