Imported from Zagforge-Org/maestro (
AGENTS.md). Install upstream withnpx skills add Zagforge-Org/maestro. Copyright stays with the author.
AGENTS.md
Guidance for AI coding agents (Claude Code, Cursor, etc.) contributing to this codebase. Read this top-to-bottom before your first non-trivial edit. The sections marked critical call out invariants the unit tests don't always catch.
What this project is
maestro is a CLI that scaffolds and reconciles Go workspaces with multiple service modules. The user runs maestro init once, then maestro service <name> per service. project.toml is the source of truth; maestro refresh reconciles every generated file to match it. The mental model: project.toml is the score; every generated file is conducted into harmony with it.
MIT licensed, pre-1.0. See LICENSE.
Module layout
| Path | Role |
|---|---|
main.go |
Trivial — calls cmd.Execute(). |
cmd/ |
Cobra commands: init, service, refresh, delete, rename, doctor, preset, secrets, version. Keep thin; delegate to internal/. |
internal/project/ |
Project context, project.toml load/save, workspace + common/go bootstrap. |
internal/project/config/ |
project.toml schema: Project, Dev, Secrets, Service. |
internal/service/ |
Per-service orchestration. service.New(name, ctx).WithDB(db).Generate() is the entry point. |
internal/service/docker/ |
Per-service dev.Dockerfile / prod.Dockerfile generation. |
internal/scaffold/ |
Generators, one concern per subpackage: database, dockercompose, doppler, gomod, servicetemplate, taskfile, ciworkflow, golangci, license, projectreadme, devsecrets, editorconfig, gitignore. |
internal/scaffold/commongo/ |
Embedded common/go source shipped into every project (identity, ginx, jwtx, mailer, internalauth, logger, rdb, ...). Capability opt-ins are declared in embed.go; import-prefix rewriting lives in rewrite.go (ImportRoot + RewriteImportRoot). |
internal/preset/ |
Preset system: bundles of files + capabilities layered onto a scaffold (presets/auth-jwt, presets/api-gateway). subst.go/walker.go do substitution and cross-package import rewrite via commongo.RewriteImportRoot. |
internal/refresh/ |
Reconciliation engine; refresh/generator/ holds per-file generators. |
internal/tui/ |
bubbletea wizards — initwizard (project init), dbwizard (service shape + DB). |
internal/doctor/ |
PATH checks for required external tools. |
internal/dbping/ |
Database reachability probe. |
internal/constants/ |
Shared constants (file modes, well-known names). |
internal/e2e/ |
Build-tagged smoke tests. Run with go test -tags=e2e ./internal/e2e/. |
pkg/colors/, pkg/fileutil/ |
Shared utilities; safe to depend on from internal/. |
test/ |
Local scratch project. Gitignored. Do not commit anything inside it. |
Critical invariants
Read this before editing generators, templates, or refresh logic. Violating any of these is the fastest way to break every existing user's project on refresh.
Managed vs user-owned files
Every generated file is one or the other:
- Managed (compose files, root + per-service Taskfile,
.air.toml,doppler.yaml,.gitignore): regenerated onrefresh, butrefreshpreserves user edits where the schema allows. Seeinternal/scaffold/dockercompose/init.go— the merge model never overwrites; it surgically updates managed keys (build/env_file/depends_on/volumes) and leaves everything else alone. - User-owned (
cmd/<svc>/main.go,internal/server/server.go,Taskfile.local.yaml, anything the user is expected to edit): write-once. Templates must check existence and skip. See the existence guard inservicetemplate.writeFiles.
If you add a new generator, classify it explicitly. Don't guess.
refresh idempotency
Running refresh twice with no project.toml change must produce zero file diffs. New managed content must also be idempotent across runs. The e2e tests don't cover idempotency yet — when in doubt, add a unit test that runs the generator twice and diffs.
Doppler is read-only
maestro writes doppler.yaml and shells doppler secrets download. It never creates Doppler projects, configs, or pushes secrets. Don't add code that mutates Doppler state — that's an explicit design choice (the user manages secrets in Doppler; maestro is a client).
DATABASE_URL policy
For docker-mode databases, DATABASE_URL is always set in the compose env (routing info, not secret) regardless of whether Doppler is enabled. Live-mode URLs are the user's responsibility — they go into Doppler or .env, never into a generated compose file.
Hyphenated service names
proto3 packages and Go identifiers can't contain hyphens. servicetemplate.protoIdent() underscores the name for package <ident>.v1; and the go_package alias; the module path keeps hyphens (legal in Go module paths). Wherever you derive an identifier from ServiceName, ask whether it needs the same treatment.
commongo import rewriting
Every file shipped from internal/scaffold/commongo imports its siblings under commongo.ImportRoot. At scaffold time those prefixes are rewritten to the target project's module via commongo.RewriteImportRoot - the single source of truth used by both the project init walker and preset apply. If you add code that copies embedded commongo files onto disk, it must run the rewrite, or a package that imports a sibling will resolve against maestro and fail go mod tidy in the generated project. New capability packages also need an //go:embed entry and a Capabilities decision in commongo/embed.go.
Path construction
Use filepath.Join("foo", "bar") — never "./foo/bar". Hardcoded ./ prefixes break path comparisons and aren't cross-platform.
No reflect
Project rule. Use interfaces, type switches, or compile-time checks (var _ Interface = (*Impl)(nil)).
Architecture: the refresh loop
The reconciler asks, for every generator: "Does the file match what project.toml implies right now? If not, update it (preserving user keys); if it's no longer applicable, remove it."
This is why:
project.tomlis the only source of truth — everything else is downstream.- The dockercompose generator has explicit merge logic (
refreshManaged()ininternal/scaffold/dockercompose/init.go). - The gomod scaffold uses
golang.org/x/mod/modfileinstead of regex, so concurrent rewrites can't corrupt go.mod files. - Generators are pure-ish: input is
(rootPath, config), output is files on disk. External side-effects (Doppler, go.work) are explicit and one-shot.
If you add a new managed file, think through what happens when the user edits it and re-runs refresh. That's the contract you're signing.
How to work
Build + run
task b # builds bin/maestro
task rb # rebuilds and runs with {{.CLI_ARGS}} in $USER_WORKING_DIR
Tests
go test ./... # unit suite, ~2s
go test -tags=e2e ./internal/e2e/ # e2e suite, ~10s warm, ~30s cold
The e2e suite scaffolds real projects, builds them, runs them, and probes them. If a refactor breaks the template → build → run pipeline, e2e is the safety net. Run it before any non-trivial change to internal/scaffold/, internal/service/, or internal/refresh/.
Lint / vet
go vet ./...
No golangci-lint config exists yet. Don't introduce style enforcement the codebase doesn't already follow.
CI
.github/workflows/tests.yml runs unit + e2e on every push to main and every PR. .github/workflows/release.yml builds and publishes the GitHub Release on any v* tag. Match their expectations locally before pushing.
Conventions
- Go 1.26.3 (pinned in
go.mod). Use modern stdlib idioms:errors.Join,signal.NotifyContext,slices,cmp, etc. - Cobra for CLI. Commands stay thin: parse args, load context, delegate. Business logic lives in
internal/. - bubbletea + lipgloss for TUIs (the
initwizardanddbwizardpackages). go-yaml(goccy) for YAML — uses inlineExtracatch-all maps to preserve unknown keys during compose merge.BurntSushi/tomlfor project.toml.- Comments are sparse. Write a comment only when the why is non-obvious — a hidden constraint, a workaround, a subtle invariant. Don't restate the code. Don't reference the current task or PR in code comments.
- Pointers + interfaces for structs that grow beyond a few fields or have multiple variants. Use
var _ Interface = (*Impl)(nil)for compile-time conformance. - Error wrapping.
fmt.Errorf("foo: %w", err)— preserve the chain soerrors.Is/Asworks upstream. - No new external deps without asking.
go.modis small on purpose.
When to ask vs. proceed
| Change | Default |
|---|---|
| Renames, typos, single-file tightening | Proceed and report. |
| Test additions or fixes | Proceed. |
Anything touching internal/scaffold/, internal/refresh/, or templates |
Pause; explain the plan; wait for OK. |
| New external dependency | Ask first. |
project.toml schema change |
Ask first; discuss migration (no migration mechanism exists yet — new fields must default sensibly when absent). |
| Refactor touching >10 files | Write a plan, get explicit OK, then execute. |
Big behavioural change to refresh |
Always ask. This is the contract with every existing user. |
The repo owner explicitly does not want huge blobs of code generated without their reasoning being in the loop. You are encouraged to push back, propose alternatives, and flag architectural smells you spot along the way.
Commit + PR hygiene
- One logical change per commit. Don't bundle a rename with a feature, or a refactor with a bugfix.
- Commit subject: short, imperative ("rename module to maestro", not "renamed module"). Body explains why if needed.
- Run
go test ./...before every commit; rungo test -tags=e2e ./...before any template / scaffold edit. - Never use
--no-verifyor--amendon published commits. If a pre-commit hook fails, fix the underlying issue. - PRs include a short summary + test plan; CI must be green before merge.
Reference
- Effective Go — https://go.dev/doc/effective_go
- Cobra — https://github.com/spf13/cobra
- bubbletea — https://github.com/charmbracelet/bubbletea
- lipgloss — https://github.com/charmbracelet/lipgloss
- Taskfile — https://taskfile.dev
- buf — https://buf.build/docs/installation
- Air — https://github.com/air-verse/air
- Doppler CLI — https://docs.doppler.com/docs/install-cli