Extending aayudha
aayudha grows in four ways: a new verb, a new floor, a new host, or a new integration. Each one has a single place
where it is declared, and the rest follows from tests and go generate ./.... This page names those places, using
how the existing ones are wired, so a change lands complete and passes CI the first time.
Declare it once, let the generators and tests do the rest, and expect a human review on protected paths.
Before you start
Work in a worktree cut from origin/dev and open the pull request to dev. Run the same checks CI runs:
go build ./... && go vet ./... && go test ./... && go generate ./...
golangci-lint run ./... # the CI `lint` job, config in .golangci.yml
markdownlint-cli2 # the CI `docs` job, config in .markdownlint-cli2.yaml
From AGENTS.md. Commit the generated files together with their source; the generate-drift job fails
otherwise. A new dependency needs its licence in THIRD_PARTY_NOTICES.md in the same pull request.
Add a verb
The verb set is a design decision, not just code. TestRegistry pins the list: "32 verbs in plan order, then chart and survey". So a new
verb starts with a change to the build plan and the wantVerbs list in
internal/verbs/registry_test.go. A new plumbing command is lighter: add a plumb(...) line to Plumbing and
update wantPlumbing.
- Registry entry. Add a
CommandtoVerbsininternal/verbs/registry.go. Fill inName, a one-lineSummaryof at most 200 characters,WP,Stage, the AccountableSeat(andOwnsif another seat owns the thresholds),Lane,Layers, typedInputs,Outputs,Gates,Surfaces,Identity,Mode, andExposure. Mark inputs that act as the human withhumanOnly(...). - RACI. Add the verb's Consulted and Informed seats to
RACIininternal/verbs/raci.go.verbs.LintRACIfails generation without it. - Handler. Add
internal/cli/<verb>.gowith aHandlerand register it by name. The CLI builds the cobra command, flags, and validation from the registry, so the handler only readsValues. Route every external write throughinternal/actions. IfModeisdirectorhybrid, call models only throughinternal/models, and only for a host-independent pass (ground rule 5). - Skill body. Run
go generate ./...once. It writesskills/<verb>/SKILL.mdwith the generated frontmatter and a placeholder body. Replace the body with what the host's model should do and whichaayucalls to make. Keep it under 500 lines; put detail inreferences/. - Tests. Add
internal/cli/<verb>_test.goagainst the fake GitHub server the other CLI tests use.TestSkillLintinembed_test.gochecks that everyaayucall in the skill names a real command and real flags. - Evals. For a host-reasoning skill, add cases under
evals/cases/<verb>/<name>/case.toml. Tier 1 runs ingo test(TestTier1ininternal/evals); add a[tier2]section with a rubric soevals.ymlcan judge it on a model.
Handler registration looks like this:
func init() { register("ship", runShip) }
From internal/cli/ship.go. The signature is func(ctx context.Context, e *Env, in Values) error, declared in
internal/cli/env.go. A registry command with no handler appears in aayu --help marked "lands in" its work
package and exits with code 2, so you can merge the registry entry before the handler.
go generate ./... then writes the verbs table row in docs/README.md, the verb map in docs/guide/when-to-use-what.md, docs/RACI.md,
the seat charters, AGENTS.md, and, when Exposure is not never, internal/mcp/schemas/<verb>.json. See
the verb registry.
Add a floor
A floor is a deterministic check with a fixed threshold. No model takes part (ground rule 7). Every floor implements one interface:
// Floor is one deterministic floor.
type Floor interface {
Name() string
// Applies says whether the floor runs on this change; when not, the
// reason is recorded as a skipped check (e.g. "no-L01").
Applies(r *Run) (bool, string)
Run(ctx context.Context, r *Run) (Result, error)
}
From internal/verify/verify.go (tabs shown as spaces).
- Type. Write the floor in
internal/verify, asSecretsininternal/verify/scanners.godoes: check the tool exists withr.need, record its version, run it throughr.Sandbox.Run(a scrubbed child environment), parse the output, and fillproof.Checkwith the tool, version, value, and threshold. A missing tool or an error makes the floorblocked, never a pass. - Order. Add it to
Standard()ininternal/verify/scanners.go, the listverifyruns in order. - Threshold. Add a field to
config.Floorsininternal/config/config.go, and its default under[verify.floors.<name>]ininternal/config/defaults.toml. - Documentation row. Add a row with the key, its meaning, and its source to the Floors section in
internal/config/gen/main.go. Without itgo generatefails, soCONSTRAINTS.mdcannot omit a default. Regenerating also updatesinternal/config/schema.json. - CI tools. If the floor needs a new tool, pin its version and SHA-256 in the merge gate. There are two copies:
.github/workflows/aayudha-merge-gate.ymlfor this repository andinternal/initrepo/aayudha-merge-gate.yml, the templateaayu init --gatesinstalls. - Tests. Follow
TestCoverageFloorBoundaryandTestScannerFloorsFindingsininternal/verify/verify_test.go: a value just inside the threshold passes and one just outside fails.
[verify.floors.secrets]
max_findings = 0
From internal/config/defaults.toml. See floors.
Add a host
Hosts are registry data plus an adapter.
- Registry. Add a
HostIDconstant and an entry toHostsininternal/verbs/registry.go, withExcludesfor model families it cannot run.TestRegistrythen requires a default for every lane on the new host, so add one to eachLanesentry. - Adapter. In
internal/hosts/install.go, set where the host reads skills (SkillDir), and extendhostFilesto register the MCP server ("command": <aayu>, "args": ["mcp"]) and the two hooks,session-startandpre-tool. Every hook command comes fromHookCommand, and theourHookpattern lists the host IDs it recognises. Ininternal/hosts/hook.go, giveDenyandRunHookthe host's output format; Cursor, for example, wants JSON with apermissionfield. - Templates. A whole-file template, like the Amp plugin
hosts/amp/aayudha.ts, goes inhosts/<host>/, whichembed.goembeds. JSON settings are merged into what is already there instead. - Detection. Add the environment variable the host sets to
hostEnvininternal/doctor/checks.go. - Loops.
internal/loopsrenders loops for Amp (JSON), Cursor (a Terraform file), and a GitHub Actions fallback, and Claude Code gets none; decide what the new host gets. - Tests and docs. Add a golden tree under
internal/hosts/testdata/golden(TestGoldenTrees), extendTestHooks, and update the host tables inhosts/README.mdanddocs/getting-started.md.
Add an integration
Integrations are read-only adapters to outside services, used for baselines, design references, and measure.
- Adapter. Add
internal/integrations/<name>with a constructor and a client that implementsScope(the reachability and key-scope checkdoctorruns), plusBaselineorExportif it serves those. Wrap its HTTP transport inintegrations.ReadOnly, which keeps every adapter read-only. - Catalog. Add a
ProvidertoKnownininternal/integrations/providers/providers.go, withNeedlisting the[integrations.<name>]fields it requires. - Config. Values live in
[integrations.<name>](config.Integration). They name secrets, never hold them, andscope = "write"is refused before any call. - Init. To offer it in
aayu init --integrations, add anIntegrationwith its cost and scope toIntegrationsininternal/initrepo/extras.go. - Tests. Replay recorded fixtures through
Deps.Transport, asTestBaselinesGoldendoes.
{
Name: "posthog", Purpose: "product analytics baselines", Need: posthog.Need,
open: func(d Deps, c integrations.Conn) (scoper, error) { return posthog.New(c, d.Transport, d.now) },
},
From internal/integrations/providers/providers.go.
Protected paths
.github/CODEOWNERS lists the paths that need a human CODEOWNER review. The protected-paths check fails by design
when a pull request touches one without that review.
/.github/ @rahulnakmol
/skills/ @rahulnakmol
/ops/ @rahulnakmol
/seats/ @rahulnakmol
/CONSTRAINTS.md @rahulnakmol
/internal/config/defaults.toml @rahulnakmol
/.aayudha/config.toml @rahulnakmol
/.agents/ @rahulnakmol
/.claude/ @rahulnakmol
From .github/CODEOWNERS. So a new verb (its skills/ body), a new floor (defaults.toml, CONSTRAINTS.md,
the workflow), and a new host whose files land in .agents/ or .claude/ all need a human review. A new
integration needs one when it touches .aayudha/config.toml or a workflow.
Honest status: this repository runs in solo mode (one human). A protected-path pull request merges only through the founder's recorded admin bypass, after a PR comment starting
Solo-mode bypass:, and only when every other check is green.
Invariants
- A registry change keeps its RACI complete. Enforced by
verbs.LintRACIininternal/verbs/raci.go(TestLintRACI). - Every lane has a default on every host, and every verb has a valid stage, lane, and surfaces. Enforced in
internal/verbs/registry_test.go(TestRegistry). - Skills call only real commands and flags. Enforced by
internal/skills.Lint(TestSkillLintinembed_test.go). - Every config default has a documented row. Enforced by
internal/config/gen/main.go, run bygenerate-drift. - A floor that cannot run is blocked, never passed. Enforced by
verify.Executeininternal/verify/verify.go. - Adapters cannot write. Enforced in
internal/integrations(TestReadOnlyTransport) andinternal/integrations/providers(TestWriteScopedKeyRefusedBeforeAnyCall). - Protected paths get a human review. Enforced by
.github/workflows/protected-paths.ymlagainst.github/CODEOWNERS.
Where it lives in the code
- internal/verbs/registry.go and internal/verbs/raci.go
- internal/cli
- skills and evals
- internal/verify
- internal/config/defaults.toml and internal/config/gen
- internal/hosts and hosts
- internal/integrations
- internal/initrepo/extras.go
- .github/CODEOWNERS