All notable changes to AOH. Format: Keep a Changelog, versioning: SemVer.
-
v0.3 phase B — authoring/promote (SDD, commits
be42725→494e4cf, 365 tests):aoh skill promote <name> [--from DIR] --pack <name> [--pr]: promotes a locally-drafted skill (found via.claude/skills/<name>or.agents/skills/<name>, searched upward from cwd, or an explicit--from) into a git-hosted pack. Direct-commit is the default (bare-mirror → fresh temp worktree cut from a freshly-fetched default branch → full pack validation in the worktree → commit → fast-forward push to the real repo);--pris opt-in (feature branchskill/<name>pushed to the real repo,gh pr create). Prints the staged diff (git diff --cached) before announcing success. A no-op re-promote (unchanged skill) is detected via git's own "nothing to commit" and reported without a duplicate commit. A non-fast-forward push rejection (someone else promoted in between) surfaces asPromoteErrorwith "re-run with --pr" — never auto-retried, never force-pushed.src/aoh/gitops.pywrite primitives:fetch_default_branch(re-resolvesorigin/HEADfresh every call, falling back tomain/master),create_worktree/remove_worktree,check_identity(fails fast ifuser.name/user.emailaren't configured),commit_all,push_fast_forward,create_branch,push_branch(pushes to the real remote by URL, not the mirror),set_remote_url,staged_diff,open_pr(shells out togh pr create).src/aoh/skillcopy.py(new): hygiene-filtered skill tree copy — rejects.gitdirectories, symlinks, devices/sockets/fifos, and oversized files; enforcesMAX_FILE_BYTES(10 MiB/file),MAX_TOTAL_BYTES(50 MiB/skill),MAX_FILE_COUNT(500 files) in a single pre-scan pass, so any violation leaves the destination completely untouched (no partial copy).src/aoh/promote.py(new): orchestrates discover → copy-hygiene → worktree → validate → commit/PR, wrapping git and copy-hygiene failures asPromoteError(distinct fromPackError, which stays for genuine pack validation failures).collections/core/aoh-authoringpack: one skill (author-and-promote-skill) that walks an agent through confirming a draft validates standalone, identifying the target pack from~/.aoh/config.yaml, runningscripts/promote.sh(a thinexec aoh skill promote "$@"wrapper), reading the staged diff before it ships, and reporting the result — states the secrets-honesty contract explicitly ("AOH does not intentionally manage secrets").- Live validation evidence:
docs/demos/promote-validation-2026-07-19.md— real, private, throwaway GitHub repo (never the realagenticdevops/aoh), proving direct-commit promote, no-op re-promote, and a real merged--prPR, including an honestly-recordedopen_prbug found during the live run.
-
v0.3 phase A — fleet inventory, lock, convergent installs (SDD, commits
9750a3c→3e657df, 307 tests):kind: UserConfig(~/.aoh/config.yaml, lazy-loaded — every command still works with no config present): named pack sources,site:pointer, namedregistries:(placeholder for v0.3 phase C),defaults.runtime,defaults.model, tri-statedefaults.workspaceRoot.kind: Site(site.yaml): advisoryworkspaceRoot,defaults(runtime/model) split fromtargetDefaults, structuredpacks:({repo, subdir, ref}or a bare local path string),groups:with sharedvars,bindingsDir:(one level, sorted, filename stem must equalmetadata.name, symlinked dir or files rejected, duplicate names rejected).Bindinggained optionalspec.pack/spec.group/spec.runtimefields.kind: SiteLock(site.lock.yaml, committed next tosite.yaml): per-pack{repo, subdir, requestedRef, resolvedCommit}(or{local: true, path}).aoh lockinitializes entries that don't yet exist and never silently moves an existing one;aoh lock --update [<pack>]is the only mover and requires--yes(or interactive confirmation) on a source/ref change.src/aoh/gitops.py: bare-mirror git cache keyed by a normalized-URL hash under$AOH_HOME/cache, fcntl-locked (ensure_mirror);export_treepreflightsgit ls-treefor symlink/submodule entries before any extraction, extracts into a private temp dir, verifies containment, then atomically renames onto the destination;source_checkoutcaches exports by<urlhash>-<commit>-<subdirhash>-<format>with a.completemarker written last (an incomplete export dir is wiped and re-exported).src/aoh/manifest.py+src/aoh/installer.py: every install now writesaoh-manifest.json(source, resolved commit, per-file content hashes, canonical→materialized artifact map + adapter transform id, owned-file list,namingScheme). Installs are crash-safe and convergent: materialize into a staging dir, write a write-ahead journal (.aoh-journal.json, phasestaged→committing, fsync'd at each transition) before touching the real workspace, back up every replaced/removed owned file, then commit; an interrupted install recovers to either "nothing happened" (phasestaged) or "the new install completed" (phasecommitting, rolls forward) on the next run. Locally modified owned files refuse the install unless--discard-localis passed. ALL install paths — the legacy single-shotaoh install --runtime … --output …(recorded withnamingScheme: v1-legacy) and the new site fan-out — route through the sameinstall_workspace.- Site-qualified RBAC naming: bindings installed via a site fan-out get
ServiceAccount/ClusterRoleBindingnames of the formaoh-<site>-<binding>(DNS-1123 + 63-char validated); standalone (site-less) bindings keep the legacyaoh-<binding>name. Manifest recordsnamingScheme(v2-site-qualified|v1-legacy). - `aoh install --site [--group ] [--binding ] [--workspace-root ] [--accept-site-root] [--discard-local]`: fan-out install across every (or a filtered subset of) site bindings, each into `//`. Requires a lock that agrees with `site.yaml`'s source/ref (missing or disagreeing lock → error naming `aoh lock`). Per-binding failures are caught and isolated — other bindings still install, exit 1 if any failed.
aoh list [--site <dir>] [--workspace-root <dir>]: fleet table — binding, role,pack@ref, runtime, context/namespace, access, workspace path, provisioned state, credential state (fromaoh-provision.jsonexpiry).--sitefalls back toUserConfig.sitewhen omitted.aoh config init|get|set <dotted.key> [value]: manage~/.aoh/config.yaml(or$AOH_HOME/config.yaml).aoh lock [--site <dir>] [--update [<pack>]] [--yes]: resolve every site pack ref to a commit and write/updatesite.lock.yaml.src/aoh/paths.py:safe_segment/safe_join— every workspace-relative path (bindings dir entries, manifestownedFiles/artifactMap, journalstagingDir/backupDir) is validated as non-escaping before use; nothing path-like from a manifest or journal is trusted without going through it.AOH_HOMEenv var respected by every command that touches config, cache, or exports (defaults to~/.aoh).
-
Adapter contract standardized:
RuntimeAdapter.materializewrites into EXACTLYrequest.output_dirfor all three adapters (Hermes, Claude Code, Codex); the Hermes CLI's historical<output>/<profile>/nesting is now computed in the CLI handler before callingmaterialize, not inside the adapter.AdapterResultgainedartifact_map(canonical pack-relative path → materialized path) andtransform_id(e.g.identity-v1,codex-ops-rename-v1);generated_filesis now a complete walk of every regular file under the output directory, not just pack-sourced ones. -
RuntimeAdapterProtocol (materialize(MaterializeRequest) -> AdapterResult) +ADAPTERSregistry, extracted from the Hermes adapter —src/aoh/adapters/base.py. -
Claude Code runtime adapter: self-contained workspace (
.claude/skills,.claude/commands/ops/<skill>.md→/ops:<skill>,.claude/agents/<role>.md,.claude/settings.jsonpermission deny/allow +PreToolUseguardrail hook,CLAUDE.md). -
Codex runtime adapter: self-contained workspace (
.agents/skills/ops-<skill>/with frontmatternamerewritten, invoked$ops-<skill>,AGENTS.md,.codex/config.toml, best-effort.codex/rules/kubectl-readonly.rulesexecpolicy guardrail with documented bypass gaps). -
aoh install --runtime <hermes|claude-code|codex> <pack> --output <dir>— unified CLI entrypoint intoADAPTERS[<runtime>].materialize(...); oldinstall-hermes*subcommands remain as unchanged compat handlers,install-hermes-agentprints a stderr deprecation hint. -
Binding.access: scoped | inherit(defaultscoped);inheritmode materializes a credential-freekubeconfig-overlayviaprepare-overlay.sh(resolves cluster/user names from a redactedkubectl config view, never--raw; writes no credentials; self-verifies via--minifyand a credential-shape grep). -
Live validation evidence:
docs/demos/adapter-validation-2026-07-16.md— realkind-sresquad-democluster run proving the scoped RBAC boundary, theauth can-imatrix (including theget secrets→ no flip),codex execpolicy checkproofs for both caught and gap-form commands, and adversarial Claude Code hook proofs. -
docs/adapters.md: runtime adapters reference — workspace layouts, threat model, access modes, guardrail mapping per runtime. -
Documentation site (Docusaurus) — Concepts/Getting Started/Tutorials/Reference + Field Notes blog; GH Pages deploy.
-
collections/core/kubeopspack: pod-crashloop-triage, pending-pod-triage, node-notready-triage, k8s-service-health-report skills +kubeops-copilotrole. -
Minimal
kind: Binding(role × target, open target map), loaded standalone from site repos —examples/sresquad-site/shows the shape. -
aoh install-hermes-agent --binding <yaml>: materializes the binding — generatesprovision.sh(dedicated read-only RBAC identity: get/list/watch), a scoped kubeconfig, KUBECONFIG wiring in launch.sh, and a binding block in SOUL.md. -
Demo walkthrough:
docs/demos/kubeops-readonly.md(safe agentic harness showcase).
apiVersionis nowopenagentix.io/v1alpha2; v1alpha1 packs are rejected with a migration pointer. No compatibility shim.- Removed
kind: Workflow. Multi-skill workflows became process skills (platform-sre-triage,devops-release-automation,mlops-training-triage); single-skill wrappers were deleted. A staleworkflows/dir is a validation error. - Renamed
agents/→roles/andkind: AgentRole→kind: Role; roles no longer carry aworkflows:field. A staleagents/dir is a validation error. Evalnow requiresspec.skillpointing at the skill it tests.- Hermes adapter generates one command per skill, namespaced
ops-<skill>.md(canonical nameops:<skill>; separator mapping is per-adapter). - Installed skill reference renamed
references/aoh-workflow.md→references/aoh-pack.md.
- ClusterRole
aoh-readonlynarrowed from a wildcard read grant (apiGroups: ["*"], resources: ["*"], verbs: [get, list, watch]) to an explicit resource allowlist sized to the kubeops skills (core/apps/batch/metrics/events.k8s.io kinds, get/list/watch only).secrets,configmaps,nodes/proxy,pods/exec,pods/attach,pods/portforward,serviceaccounts/token, RBAC objects, andcertificatesigningrequestsare explicitly excluded. Shared by every runtime adapter viasrc/aoh/adapters/_k8s.py::render_provision_script. Any previously provisioned identity still has the old wildcard grant — re-runprovision.shfor each existing binding to pick up the narrower allowlist (it updates the ClusterRole in place; verified live:auth can-i get secretsflips fromyestono, seedocs/demos/adapter-validation-2026-07-16.md).
- Validator: progressive disclosure — only
AOH.yaml+ at least one skill are mandatory; workflows, agents, teams, models, evals, runtime-requirements are opt-in
.planning/project memory system (GSD-compatible) +CLAUDE.mdsession protocolplanning-contextskill (.claude/skills/planning-context)
- AOH pack format (
AOH.yaml+ skills/workflows/agents/teams/models/ runtime-requirements/evals) with referential-integrity validator - CLI:
validate,init-pack,adapt-hermes,install-hermes,install-hermes-agent,install-hermes-team - Hermes runtime adapter: generates
config.yaml,SOUL.md,aoh-agent.json,launch.sh, profile-local skills, team manifests - Example pack
examples/acme-platform-ops(platform-ops team, 3 roles, 5 skills) - Core collection
collections/core/docker-disk-cleanup(first vertical slice) - Authoring skill
authoring-skills/create-aoh-pack