feat(goose): add experimental Goose (Block) harness#1833
Conversation
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.
b03dfb7 to
e92b6da
Compare
There was a problem hiding this comment.
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
GooseClientAdapterand a sharedYamlMcpClientAdapterbase to manage YAML MCP config documents (Hermes + Goose). - Adds Goose agent recipe generation (
.goose/recipes/*.yaml) and.goosehintsstub compilation that importsAGENTS.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. |
|
@microsoft-github-policy-service agree |
c7e7b53 to
54c858c
Compare
OpenAPM spec-conformance: new requirement vs. waiver?The OpenAPM Mode B gate flags this PR because it adds code under a critical path ( Context:
Two paths, happy to take either: A. Add
I'd add the anchor + Appendix C row + manifest entry (MUST, §8.5, consumer) + a B. Waiver, if you consider 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.
54c858c to
546f52a
Compare
feat(goose): add experimental Goose (Block) harness
TL;DR
Adds Goose (by Block) as a new experimental APM target, gated behind the
gooseflag. 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 YAMLextensions:block in~/.config/goose/config.yaml, and instructions reach Goose through a.goosehintsstub that imports the generatedAGENTS.md. Like the other frontier targets,gooseis 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:
~/.config/goose/config.yaml, in YAML, under anextensions:key with a Goose-native per-server schema (type: stdio/cmd/args/envs/timeout) — not the JSONmcpServersschema every other adapter writes.goose run --recipe), which is the natural target for APM'sagentsprimitive — no other harness maps agents this way..goosehints, which supports an@./pathimport preprocessor, so the right move is a stub that importsAGENTS.mdrather than a second on-disk copy — context pulled at load time, anchor: PROSE, "Context arrives just-in-time, not just-in-case."..agents/skills/standard, so that surface is reused as-is.Approach (WHAT)
Each APM primitive maps onto the native Goose surface it belongs to:
goose run --recipe).goose/recipes/<name>.yaml(project scope)SKILL.md).agents/skills/<name>/(project) ·~/.agents/skills/<name>/(--global).goosehintsimportingAGENTS.md.goosehints+AGENTS.mdat project rootextensions: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 gooseexits with an enable hint.Implementation (HOW)
adapters/client/_yaml_config.py(new)YamlMcpClientAdapterbase — YAML round-trip, sibling-key preservation, atomic0o600write, malformed-file refusal,configure_mcp_serverflow.adapters/client/hermes.py_to_hermes_formatsurface unchanged).adapters/client/goose.py(new)GooseClientAdapter—~/.config/goose/config.yamlpath (honours$XDG_CONFIG_HOME),extensionskey, stdio/streamable_http per-server transform.integration/agent_integrator.py_write_goose_recipe(agent.md→ recipe.yaml:instructions+ apromptfor headless, verbatimparameters:+ preserved{{ }},settings.goose_model) + shared_parse_agent_frontmatterfactored out of the Codex writer.core/target_detection.pydetect_targetechoes an explicit/configgoose(added toTargetType) soapm compile -t gooseroutes the.goosehintsstub instead of falling back tominimal;_validate_canonical_v2(the MCP-install--targetresolver) now accepts experimental + explicit-only targets, fixing a pre-existing crash whereapm install --target <goose|antigravity|hermes>aborted whenever the package declared anmcp:dependency.integration/mcp_integrator_install.py~/.config/gooseorgooseon PATH), soapm install --target goosewrites the package'smcp:deps to~/.config/goose/config.yamlinstead of silently skipping them.utils/yaml_io.pyyaml_to_str(multiline_block=True)rendersinstructions/promptas readable literal blocks; default path (Hermes) unchanged.compilation/{gemini_formatter,goose_formatter,agents_compiler}.pyGeminiFormatterparameterised by stub attributes soGooseFormatterreuses it;_compile_gemini_mdgeneralised into_compile_import_stub.integration/targets.py,factory.py,core/experimental.py,core/target_detection.py,commands/install.pygooseTargetProfile, adapter registration, experimental flag, detection/description wiring, help text.docs/,CHANGELOG.mdDiagrams
Legend: how one
apm install/apm compile -t goosefans each APM primitive out to its native Goose surface.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() }Trade-offs
config.yaml, not embedded in recipes (Option A). An APM agent declares no MCP servers (onlymodel+ a tool-name whitelist), so per-recipeextensionswould have to source the package's resolved MCP deps and thread them through the agent integrator — a cross-phase change. Goose readsconfig.yamlglobally at run time, so recipes still work. Embedding remains a clean follow-up.$GOOSE_RECIPE_PATHwith no canonical user-scope home, so theagentsprimitive is excluded at--global(skills + MCP still deploy at user scope)._compile_import_stub, and_parse_agent_frontmatterwere extracted rather than duplicated — both to satisfy the R0801 duplication gate and on principle, anchor: PROSE, "Favor small, chainable primitives over monolithic frameworks.".hermes/openclaw) so it stays out of default flows until proven.Benefits
apm.yml.min-similarity-lines=10).config.yamlare written atomically with0o600and never interpolated into errors/logs.config.yamlis refused, never clobbered.Validation
Test suite, lint, duplication gate
Also verified end-to-end against the real
gooseCLI:apm compile -t goosewritesAGENTS.md+.goosehints,apm install --target goosewrites.goose/recipes/<name>.yaml+.agents/skills/, and a recipe with noextensions:block inherits the MCP server from the global~/.config/goose/config.yamlat run time (so MCP is not embedded per-recipe).A supply-chain-security review pass (per the repo's
supply-chain-security-expertpersona) returned APPROVE with no required findings:config.yamlwrite is0o600+ atomic, path containment viaensure_path_withinruns before the recipe dispatch, symlink sources are refused, and no new install-time network/code-execution path is introduced.Scenario Evidence
test_goose_update_config_writes_extensions_block_0600test_goose_update_config_refuses_malformed_yamltest_goose_agent_compiles_to_recipe_yamlprompt)test_goose_recipe_falls_back_to_default_prompt,test_goose_recipe_uses_authored_prompt_verbatim{{ }}templating pass throughtest_goose_recipe_passes_parameters_and_preserves_templatingapm compile -t gooseactually emits the.goosehintsstubtest_goose_detect_target_echoes_explicit_and_configapm install --target goosewith anmcp:dep configures Goose (no crash)test_goose_accepted_by_resolve_targets,test_goose_runtime_discovered_when_opted_intest_goose_skills_deploy_to_agents_skills.goosehintstest_goose_compile_writes_agents_md_and_goosehints--target alltest_goose_excluded_from_target_all,test_goose_resolves_only_when_named_with_flag_enabledHow to test
apm experimental enable goose.apm/agents/<name>.md, runapm compile -t goose→ confirmAGENTS.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 --globalwith an MCP dep → confirm anextensions:entry in~/.config/goose/config.yamlwith mode0o600.apm compile --all→ confirm.goosehintsis not generated (goose is excluded fromall).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.