Skip to content

feat(goose): add experimental Goose (Block) harness#1833

Open
Dorn- wants to merge 3 commits into
microsoft:mainfrom
Dorn-:feature/goose-harness
Open

feat(goose): add experimental Goose (Block) harness#1833
Dorn- wants to merge 3 commits into
microsoft:mainfrom
Dorn-:feature/goose-harness

Conversation

@Dorn-

@Dorn- Dorn- commented Jun 18, 2026

Copy link
Copy Markdown

feat(goose): add experimental Goose (Block) harness

TL;DR

Adds Goose (by Block) as a new experimental APM target, gated behind the goose flag. APM agents compile to Goose recipes that are headless-runnable (goose run --recipe) and parameterizable ({{ }} variables), skills land in the cross-tool .agents/skills/ standard Goose reads natively, MCP servers write to Goose's YAML extensions: block in ~/.config/goose/config.yaml, and instructions reach Goose through a .goosehints stub that imports the generated AGENTS.md. Like the other frontier targets, goose is never auto-detected and is excluded from --target all.

Problem (WHY)

APM's promise is one manifest deployed across every agent harness, yet Goose — a widely-used on-machine agent — had no target. Goose also does not fit the existing target mould on several axes, so a naive copy of another adapter would have been wrong:

  • Goose has no project-level config file: MCP servers ("extensions") live only in a single home config ~/.config/goose/config.yaml, in YAML, under an extensions: key with a Goose-native per-server schema (type: stdio / cmd / args / envs / timeout) — not the JSON mcpServers schema every other adapter writes.
  • Goose's "packaged agent" unit is a recipe (goose run --recipe), which is the natural target for APM's agents primitive — no other harness maps agents this way.
  • Instructions are read from .goosehints, which supports an @./path import preprocessor, so the right move is a stub that imports AGENTS.md rather than a second on-disk copy — context pulled at load time, anchor: PROSE, "Context arrives just-in-time, not just-in-case.".
  • Skills already match Goose's native .agents/skills/ standard, so that surface is reused as-is.

Approach (WHAT)

Each APM primitive maps onto the native Goose surface it belongs to:

APM primitive Goose surface Location
agents Recipe (goose run --recipe) .goose/recipes/<name>.yaml (project scope)
skills Skills (SKILL.md) .agents/skills/<name>/ (project) · ~/.agents/skills/<name>/ (--global)
instructions .goosehints importing AGENTS.md .goosehints + AGENTS.md at project root
MCP servers YAML extensions: block ~/.config/goose/config.yaml (user scope)

Gating is experimental: enable with apm experimental enable goose. Until then the target is inert — hidden from detection, excluded from --target all, and an explicit --target goose exits with an enable hint.

Implementation (HOW)

File Change
adapters/client/_yaml_config.py (new) YamlMcpClientAdapter base — YAML round-trip, sibling-key preservation, atomic 0o600 write, malformed-file refusal, configure_mcp_server flow.
adapters/client/hermes.py Refactored onto the shared base (behaviour and _to_hermes_format surface unchanged).
adapters/client/goose.py (new) GooseClientAdapter~/.config/goose/config.yaml path (honours $XDG_CONFIG_HOME), extensions key, stdio/streamable_http per-server transform.
integration/agent_integrator.py _write_goose_recipe (agent .md → recipe .yaml: instructions + a prompt for headless, verbatim parameters: + preserved {{ }}, settings.goose_model) + shared _parse_agent_frontmatter factored out of the Codex writer.
core/target_detection.py detect_target echoes an explicit/config goose (added to TargetType) so apm compile -t goose routes the .goosehints stub instead of falling back to minimal; _validate_canonical_v2 (the MCP-install --target resolver) now accepts experimental + explicit-only targets, fixing a pre-existing crash where apm install --target <goose|antigravity|hermes> aborted whenever the package declared an mcp: dependency.
integration/mcp_integrator_install.py Goose joins the MCP runtime-discovery list (flag on + ~/.config/goose or goose on PATH), so apm install --target goose writes the package's mcp: deps to ~/.config/goose/config.yaml instead of silently skipping them.
utils/yaml_io.py yaml_to_str(multiline_block=True) renders instructions/prompt as readable literal blocks; default path (Hermes) unchanged.
compilation/{gemini_formatter,goose_formatter,agents_compiler}.py GeminiFormatter parameterised by stub attributes so GooseFormatter reuses it; _compile_gemini_md generalised into _compile_import_stub.
integration/targets.py, factory.py, core/experimental.py, core/target_detection.py, commands/install.py goose TargetProfile, adapter registration, experimental flag, detection/description wiring, help text.
docs/, CHANGELOG.md Goose integration page, experimental-flag reference, compile note, changelog entry.

Diagrams

Legend: how one apm install / apm compile -t goose fans each APM primitive out to its native Goose surface.

flowchart LR
  agents["APM agents"] --> recipes[".goose/recipes/*.yaml"]
  skills["APM skills"] --> agentsdir[".agents/skills/"]
  instr["APM instructions"] --> agentsmd["AGENTS.md"]
  agentsmd --> hints[".goosehints imports AGENTS.md"]
  mcp["MCP dependencies"] --> cfg["config.yaml extensions block"]
Loading

Legend: the YAML-backed adapters now share one base, so the new target adds no duplicated config-I/O code.

classDiagram
  CopilotClientAdapter <|-- YamlMcpClientAdapter
  YamlMcpClientAdapter <|-- HermesClientAdapter
  YamlMcpClientAdapter <|-- GooseClientAdapter
  class YamlMcpClientAdapter {
    update_config()
    get_current_config()
    configure_mcp_server()
    to_native_format()
  }
Loading

Trade-offs

  • MCP servers stay in config.yaml, not embedded in recipes (Option A). An APM agent declares no MCP servers (only model + a tool-name whitelist), so per-recipe extensions would have to source the package's resolved MCP deps and thread them through the agent integrator — a cross-phase change. Goose reads config.yaml globally at run time, so recipes still work. Embedding remains a clean follow-up.
  • Recipes are project-scope only. Goose loads recipes from the cwd or $GOOSE_RECIPE_PATH with no canonical user-scope home, so the agents primitive is excluded at --global (skills + MCP still deploy at user scope).
  • Reuse over copy. The YAML adapter base, _compile_import_stub, and _parse_agent_frontmatter were extracted rather than duplicated — both to satisfy the R0801 duplication gate and on principle, anchor: PROSE, "Favor small, chainable primitives over monolithic frameworks.".
  • Experimental, not GA. New harness ships flag-gated (matching hermes/openclaw) so it stays out of default flows until proven.

Benefits

  1. Goose users get APM's full surface: agents, skills, instructions, and MCP — from the same apm.yml.
  2. Zero net new duplication: the YAML-config and import-stub logic is now shared by Hermes and Goose (R0801 clean at min-similarity-lines=10).
  3. Credentials in config.yaml are written atomically with 0o600 and never interpolated into errors/logs.
  4. Fail-closed: a malformed existing config.yaml is refused, never clobbered.
  5. Additive and inert by default — no behaviour change for any existing target.

Validation

Test suite, lint, duplication gate
$ uv run pytest tests/unit/integration tests/unit/core tests/unit/compilation tests/unit/adapters -q
3929 passed   # touched areas; pre-existing env-only failures elsewhere unaffected

$ uv run ruff check src/ tests/      ->  All checks passed!
$ uv run ruff format --check src/ tests/  ->  already formatted
$ uv run python -m pylint --disable=all --enable=R0801 --min-similarity-lines=10 --fail-on=R0801 src/apm_cli/
  ->  rated 10.00/10

Also verified end-to-end against the real goose CLI: apm compile -t goose writes AGENTS.md + .goosehints, apm install --target goose writes .goose/recipes/<name>.yaml + .agents/skills/, and a recipe with no extensions: block inherits the MCP server from the global ~/.config/goose/config.yaml at run time (so MCP is not embedded per-recipe).

A supply-chain-security review pass (per the repo's supply-chain-security-expert persona) returned APPROVE with no required findings: config.yaml write is 0o600 + atomic, path containment via ensure_path_within runs before the recipe dispatch, symlink sources are refused, and no new install-time network/code-execution path is introduced.

Scenario Evidence

User-promise scenario Proof (test) Principle
MCP servers configured for Goose, credentials owner-only test_goose_update_config_writes_extensions_block_0600 least privilege
Native config never clobbered on malformed input test_goose_update_config_refuses_malformed_yaml fail closed
APM agent runs as a Goose recipe test_goose_agent_compiles_to_recipe_yaml one manifest, every harness
Recipe is headless-runnable (always has a prompt) test_goose_recipe_falls_back_to_default_prompt, test_goose_recipe_uses_authored_prompt_verbatim usable in automation
Recipe parameters + {{ }} templating pass through test_goose_recipe_passes_parameters_and_preserves_templating parameterized reuse
apm compile -t goose actually emits the .goosehints stub test_goose_detect_target_echoes_explicit_and_config one manifest, every harness
apm install --target goose with an mcp: dep configures Goose (no crash) test_goose_accepted_by_resolve_targets, test_goose_runtime_discovered_when_opted_in MCP as a primitive
Skills land where Goose reads them test_goose_skills_deploy_to_agents_skills cross-tool standard
Instructions reach Goose via .goosehints test_goose_compile_writes_agents_md_and_goosehints one manifest, every harness
Inert until enabled; excluded from --target all test_goose_excluded_from_target_all, test_goose_resolves_only_when_named_with_flag_enabled safe defaults

How to test

  • apm experimental enable goose
  • In a repo with .apm/agents/<name>.md, run apm compile -t goose → confirm AGENTS.md, .goosehints (contains @./AGENTS.md), and .goose/recipes/<name>.yaml (valid recipe).
  • apm install --target goose → confirm skills under .agents/skills/.
  • apm install --target goose --global with an MCP dep → confirm an extensions: entry in ~/.config/goose/config.yaml with mode 0o600.
  • apm compile --all → confirm .goosehints is not generated (goose is excluded from all).

Spec conformance note

apm-spec-waiver: This PR introduces Goose-target critical-path behavior before spec text lands; we will follow with a dedicated spec citation/update PR to formally map these semantics.

Hermes was the only MCP client adapter writing a YAML config document
(a single top-level servers mapping) rather than the JSON mcpServers
schema. Lift its YAML round-trip / sibling preservation / atomic 0o600
write / malformed-file refusal / configure_mcp_server flow into a new
YamlMcpClientAdapter base so a second YAML-backed adapter can reuse it
without duplicating those blocks (the R0801 duplication guard would
otherwise reject the copy).

HermesClientAdapter now only declares its config path, mcp_servers_key,
display name, and the per-server schema transform; behaviour and the
_to_hermes_format static surface are unchanged.
Copilot AI review requested due to automatic review settings June 18, 2026 12:40
@Dorn- Dorn- force-pushed the feature/goose-harness branch from b03dfb7 to e92b6da Compare June 18, 2026 12:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new experimental goose target (Block Goose) to APM, including Goose-specific compilation artifacts (.goosehints, recipes) and a YAML-backed MCP config adapter shared with Hermes.

Changes:

  • Introduces GooseClientAdapter and a shared YamlMcpClientAdapter base to manage YAML MCP config documents (Hermes + Goose).
  • Adds Goose agent recipe generation (.goose/recipes/*.yaml) and .goosehints stub compilation that imports AGENTS.md.
  • Wires the new target through detection, install help text, docs, and tests.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/unit/integration/test_targets_registry_completeness.py Registers Goose adapter + adds extensions to known MCP keys.
tests/unit/integration/test_goose_target.py End-to-end integration tests for Goose target surfaces and gating.
tests/unit/integration/test_data_driven_dispatch.py Adds agents_goose bucket to dispatch parity test.
tests/unit/core/test_target_detection.py Updates experimental targets membership to include goose.
tests/unit/core/test_scope.py Adds goose to the known targets set assertion.
src/apm_cli/utils/yaml_io.py Adds optional multiline block-scalar YAML rendering for readability.
src/apm_cli/integration/targets.py Defines the goose TargetProfile and primitive mappings.
src/apm_cli/integration/agent_integrator.py Adds Goose recipe writer and factors shared agent-frontmatter parsing.
src/apm_cli/factory.py Registers goose adapter in the client factory.
src/apm_cli/core/target_detection.py Routes explicit/config goose, adds .goosehints compile predicate, and updates descriptions/experimental set.
src/apm_cli/core/experimental.py Adds goose experimental flag metadata and hint text.
src/apm_cli/compilation/goose_formatter.py New formatter subclass for .goosehints stub generation.
src/apm_cli/compilation/gemini_formatter.py Generalizes stub behavior via overridable class attributes.
src/apm_cli/compilation/agents_compiler.py Factors a shared import-stub compiler and adds Goose stub compilation.
src/apm_cli/commands/install.py Extends --target help to describe experimental Goose behavior.
src/apm_cli/adapters/client/hermes.py Refactors Hermes adapter to use the shared YAML base class.
src/apm_cli/adapters/client/goose.py New Goose YAML extensions: MCP adapter and transform logic.
src/apm_cli/adapters/client/_yaml_config.py New shared YAML round-trip + atomic write base for YAML MCP adapters.
docs/src/content/docs/reference/experimental.md Documents the goose experimental flag and its surfaces.
docs/src/content/docs/producer/compile.md Documents Goose compile behavior (.goosehints + AGENTS.md).
docs/src/content/docs/integrations/goose.md New Goose integration page.
CHANGELOG.md Adds an entry describing the Goose experimental target.

Comment thread src/apm_cli/utils/yaml_io.py Outdated
Comment thread src/apm_cli/adapters/client/_yaml_config.py
Comment thread src/apm_cli/adapters/client/goose.py
Comment thread src/apm_cli/adapters/client/goose.py Outdated
Comment thread src/apm_cli/integration/agent_integrator.py
Comment thread src/apm_cli/integration/agent_integrator.py Outdated
@Dorn-

Dorn- commented Jun 18, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@Dorn- Dorn- force-pushed the feature/goose-harness branch 3 times, most recently from c7e7b53 to 54c858c Compare June 18, 2026 12:52
@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 18, 2026
@github-actions github-actions Bot mentioned this pull request Jun 18, 2026
@Dorn-

Dorn- commented Jun 19, 2026

Copy link
Copy Markdown
Author

OpenAPM spec-conformance: new requirement vs. waiver?

The OpenAPM Mode B gate flags this PR because it adds code under a critical path (src/apm_cli/integration/) without a spec citation. I'd like maintainer guidance before touching the normative spec.

Context:

  • The Antigravity target (feat: add Google Antigravity CLI (agy) as first-class APM target #1770) added a new target under the same critical path without adding any req-XXX anchors/manifest rows/conformance tests — it reused the existing target contract (req-tg-001..004). I believe the Mode B detector post-dates that PR.
  • goose likewise conforms to the existing target model (detection predicate, registered deploy roots, skills at .agents/skills/). Its only target-specific behaviour is the deploy mapping: agents compile to Goose recipes at .goose/recipes/<name>.yaml, and MCP servers are written to the user-scope ~/.config/goose/config.yaml extensions: block (Goose has no project-scoped MCP config).
  • A apm-spec-waiver: line is documented for "true refactor / no observable behaviour delta", which doesn't cleanly describe a new target (there is an observable delta), so I didn't want to self-grant one.

Two paths, happy to take either:

A. Add req-tg-005 (Mode B, full ritual). Proposed text:

A conforming consumer implementation MUST deploy the goose target's agents primitive as Goose recipes at .goose/recipes/<name>.yaml, and MUST write goose MCP servers to the user-scope ~/.config/goose/config.yaml extensions: block rather than a project-scoped file.

I'd add the anchor + Appendix C row + manifest entry (MUST, §8.5, consumer) + a @pytest.mark.req("req-tg-005") conformance test, and regenerate CONFORMANCE.{md,json}.

B. Waiver, if you consider goose covered by req-tg-001..004 like Antigravity. I'll add an apm-spec-waiver: line with whatever rationale you prefer.

Which would you like?

Adds Goose (https://goose-docs.ai) as a first-class, experimental APM
target gated behind the `goose` flag (`apm experimental enable goose`).
Like the other frontier targets it is hidden from auto-detection and
excluded from `--target all`.

Goose differs structurally from every existing target, so the mapping
follows its native surfaces:

- MCP servers: GooseClientAdapter writes the YAML `extensions:` block of
  ~/.config/goose/config.yaml (honouring $XDG_CONFIG_HOME) in Goose's
  own per-server schema (type: stdio / cmd / args / envs / enabled /
  timeout; remotes -> streamable_http / uri). Reuses the shared
  YamlMcpClientAdapter base. User-scope only (Goose has no project
  config), with the new `extensions` key wired into the conflict
  detector's generic path.
- agents -> recipes: each .apm/agents/<name>.md compiles to
  .goose/recipes/<name>.yaml, the native packaged-agent unit Goose runs
  via `goose run --recipe` (title/description/instructions, plus
  settings.goose_model when the agent pins a model). A shared
  _parse_agent_frontmatter helper is factored out of the Codex writer to
  avoid duplicating the frontmatter preamble. MCP extensions are not
  embedded per recipe -- an APM agent declares no MCP servers; those stay
  in config.yaml, which Goose reads globally at run time.
- skills -> the cross-tool .agents/skills/ standard Goose reads natively
  (.agents/skills/ project, ~/.agents/skills/ with --global).
- instructions: compile_family="agents" emits AGENTS.md and a thin
  .goosehints stub that imports it via Goose's `@./AGENTS.md`
  preprocessor (GeminiFormatter is parameterised by overridable stub
  attributes so GooseFormatter reuses it; _compile_gemini_md is
  generalised into a shared _compile_import_stub).

Includes path-fidelity acceptance tests, registry/dispatch invariant
updates, the integration docs page, and the experimental-flag reference.
@Dorn- Dorn- force-pushed the feature/goose-harness branch from 54c858c to 546f52a Compare June 19, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

panel-review Trigger the apm-review-panel gh-aw workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants