diff --git a/README.md b/README.md index 6613bba..8394e8c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ async def main(): # Load Anchors as the complete-host base and a Foundation provider partial. anchors = await load_bundle( "git+https://github.com/microsoft/amplifier-foundation@main" - "#subdirectory=bundles/anchors.md" + "#subdirectory=bundles/anchors/bundle.md" ) provider = await load_bundle( "git+https://github.com/microsoft/amplifier-foundation@main" @@ -120,8 +120,8 @@ This repo also contains reference bundle content for common configurations: |------|---------| | `behaviors/` | Reusable capability behaviors — the primary authoring and sharing surface | | `bundle.md` | Legacy selected Foundation root, retained for compatible complete compositions | -| `bundles/anchors.md` | Recommended supporting root for a new complete host; assets stay in `bundles/anchors/` | -| `bundles/anchors-amp-dev.md` | Anchors plus the portable `behaviors/amp-dev.yaml` capability | +| `bundles/anchors/bundle.md` | Recommended supporting root for a new complete host, alongside its agents and context | +| `bundles/anchors-amp-dev/bundle.md` | Anchors plus the portable `behaviors/amp-dev.yaml` capability | | `providers/` | Provider configurations (anthropic, openai, azure-openai, gemini, ollama) | | `agents/` | Reusable agent definitions | | `context/` | Shared context files | @@ -132,8 +132,8 @@ This repo also contains reference bundle content for common configurations: `behaviors/amp-dev.yaml` adds the lean `amp-dev:amplifier-dev-expert`, short ecosystem instructions, and the Amplifier Tester behavior without selecting an orchestrator or context manager. Existing hosts can compose that capability -without adopting Anchors. The old `bundles/anchors/bundle.md` and -`bundles/anchors-amp-dev/bundle.md` paths remain compatibility wrappers; the former +without adopting Anchors. The nested manifests are the only Anchors entry points; +there are no flat manifests or compatibility wrappers. The former `anchors-amp-dev:amplifier-dev-expert` agent alias is not retained. ## Examples diff --git a/bundles/anchors-amp-dev.md b/bundles/anchors-amp-dev.md deleted file mode 100644 index fde19e4..0000000 --- a/bundles/anchors-amp-dev.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -bundle: - name: anchors-amp-dev - display_name: Anchors · Amplifier development - version: 0.3.0 - description: Anchors with portable Amplifier ecosystem development and isolated validation. - -includes: - - bundle: foundation:bundles/anchors.md - - bundle: foundation:behaviors/amp-dev.yaml ---- - -@anchors:context/system.md \ No newline at end of file diff --git a/bundles/anchors-amp-dev/README.md b/bundles/anchors-amp-dev/README.md index d225695..9dccc37 100644 --- a/bundles/anchors-amp-dev/README.md +++ b/bundles/anchors-amp-dev/README.md @@ -1,6 +1,6 @@ # Anchors + Amplifier-Ecosystem Knowledge -The [`anchors`](../anchors.md) bundle plus one portable capability: knowledge of +The [`anchors`](../anchors/bundle.md) bundle plus one portable capability: knowledge of the Amplifier ecosystem itself — repo dependency order, cross-repo validation in a Digital Twin Universe, and bundle/agent authoring. @@ -10,9 +10,10 @@ it is being used to build. ## Install -### Migration from the nested entry point +### Resource identifiers -The old root URI remains supported, but its added resource identifiers changed: +The nested root is canonical. Its added resource identifiers use the portable +capability's namespace: | Former identifier | Replacement | |---|---| @@ -20,9 +21,9 @@ The old root URI remains supported, but its added resource identifiers changed: | `anchors-amp-dev:context/amplifier-ecosystem.md` | `amp-dev:context/amplifier-dev/amplifier-ecosystem.md` | Update explicit delegate calls and context references. The old agent alias and -context path are not compatibility exports. The wrappers and expert use the -enclosing `foundation:` resource namespace; it must resolve to this candidate -version or later, not a stale separately cached Foundation tree. +context path are not compatibility exports. The expert uses the enclosing +`foundation:` resource namespace for documentation, not its runtime. There are +no flat manifests or compatibility wrappers. ### Select the complete root @@ -33,22 +34,27 @@ amplifier bundle use anchors-amp-dev ``` Or add it explicitly by URI (single-quote to prevent shell expansion of the `#` -fragment; the `.md` suffix is required): +fragment): ```bash -amplifier bundle add 'git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors-amp-dev.md' --name anchors-amp-dev +amplifier bundle add 'git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors-amp-dev' --name anchors-amp-dev amplifier bundle use anchors-amp-dev ``` ## What it is, mechanically -The canonical `bundles/anchors-amp-dev.md` declares no runtime of its own. Its -includes and instruction body are: +The canonical `bundles/anchors-amp-dev/bundle.md` declares no runtime of its own. +The directory URI selects that manifest; an explicit +`#subdirectory=bundles/anchors-amp-dev/bundle.md` also works. Its own namespace +stays at the manifest directory, without a `namespace_root` override or variant +assets. Self-namespaced relative includes select the base and capability from the +same repository, including direct local file/directory loads without separately +registering Foundation. Its includes and instruction body are: ```yaml includes: - - bundle: foundation:bundles/anchors.md - - bundle: foundation:behaviors/amp-dev.yaml + - bundle: anchors-amp-dev:../anchors/bundle.md + - bundle: anchors-amp-dev:../../behaviors/amp-dev.yaml ``` ``` @@ -99,11 +105,11 @@ includes produce a byte-identical mount plan. ``` amplifier-foundation/ ├── bundles/ -│ ├── anchors.md # complete Anchors host -│ ├── anchors-amp-dev.md # Anchors + amp-dev behavior +│ ├── anchors/ +│ │ └── bundle.md # complete Anchors host, alongside its assets │ └── anchors-amp-dev/ │ ├── README.md # this file -│ └── bundle.md # compatibility wrapper +│ └── bundle.md # Anchors + amp-dev behavior ├── behaviors/amp-dev.yaml # complete portable amp-dev capability ├── agents/amplifier-dev-expert.md # lean ecosystem authority └── context/amplifier-dev/ @@ -113,15 +119,17 @@ amplifier-foundation/ ## Status -Version 0.3.0. The flat root composes Anchors plus the shared capability. The old -`bundles/anchors-amp-dev/bundle.md` URI remains a compatibility wrapper, not a -second implementation. Local targeted qualification passes 232 checks. Separate -`amplifier-tester` acceptance through official `amplifier-app-cli` passed 17 -bounded checks covering both flat roots and both compatibility entry points: +Version 0.3.0. The nested root composes canonical nested Anchors plus the shared +capability, without a second implementation or flat entry point. + +**Historical qualification (before the nested-only layout):** Local targeted +qualification passed 232 checks. Separate `amplifier-tester` acceptance through +official `amplifier-app-cli` passed 17 bounded checks covering the then-flat roots +and compatibility entry points: real model responses, file and Bash tools, named-agent spawning, skills loading, candidate resource provenance, and retained streaming/simple runtimes. -The portable capability also passed real root and expert-spawn checks in an -isolated non-Anchors host. These observations do not establish a clean -repository-wide recipe verdict; full validation failures and coverage limits -remain separate evidence. +The portable capability also historically passed real root and expert-spawn +checks in an isolated non-Anchors host. These observations do not qualify the +nested-only layout or establish a clean repository-wide recipe verdict; full +validation failures and coverage limits remain separate evidence. diff --git a/bundles/anchors-amp-dev/bundle.md b/bundles/anchors-amp-dev/bundle.md index 2a44261..d0a2d29 100644 --- a/bundles/anchors-amp-dev/bundle.md +++ b/bundles/anchors-amp-dev/bundle.md @@ -3,10 +3,11 @@ bundle: name: anchors-amp-dev display_name: Anchors · Amplifier development version: 0.3.0 - description: Compatibility entry point for the flat Anchors Amplifier-development host. + description: Anchors with portable Amplifier ecosystem development and isolated validation. includes: - - bundle: foundation:bundles/anchors-amp-dev.md + - bundle: anchors-amp-dev:../anchors/bundle.md + - bundle: anchors-amp-dev:../../behaviors/amp-dev.yaml --- @anchors:context/system.md \ No newline at end of file diff --git a/bundles/anchors.md b/bundles/anchors.md deleted file mode 100644 index 7264d77..0000000 --- a/bundles/anchors.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -bundle: - name: anchors - display_name: Anchors - version: 0.3.0 - description: Lean principle-driven host with purposeful agents and a standard development tool roster. - namespace_root: anchors - -includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/streaming-ui.yaml - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/status-context.yaml - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/redaction.yaml - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/logging.yaml - - bundle: git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=behaviors/context-intelligence-logging.yaml - - bundle: git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=behaviors/context-intelligence-navigation.yaml - # Role routing remains a host choice, not part of the amp-dev capability. - - bundle: git+https://github.com/microsoft/amplifier-bundle-routing-matrix@main#subdirectory=behaviors/routing.yaml - -session: - raw: true - orchestrator: - module: loop-streaming - source: git+https://github.com/microsoft/amplifier-module-loop-streaming@main - config: - extended_thinking: true - context: - module: context-simple - source: git+https://github.com/microsoft/amplifier-module-context-simple@main - config: - compact_threshold: 0.8 - auto_compact: true - -tools: - - module: tool-filesystem - source: git+https://github.com/microsoft/amplifier-module-tool-filesystem@main - - module: tool-bash - source: git+https://github.com/microsoft/amplifier-module-tool-bash@main - - module: tool-web - source: git+https://github.com/microsoft/amplifier-module-tool-web@main - - module: tool-search - source: git+https://github.com/microsoft/amplifier-module-tool-search@main - - module: tool-todo - source: git+https://github.com/microsoft/amplifier-module-tool-todo@main - - module: tool-apply-patch - source: git+https://github.com/microsoft/amplifier-bundle-filesystem@main#subdirectory=modules/tool-apply-patch - - module: tool-delegate - source: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/tool-delegate - config: - features: - self_delegation: - enabled: true - session_resume: - enabled: true - context_inheritance: - enabled: true - max_turns: 10 - provider_selection: - enabled: true - settings: - exclude_tools: [tool-delegate] - timeout: null - max_llm_calls: null - - module: tool-skills - source: git+https://github.com/microsoft/amplifier-bundle-skills@main#subdirectory=modules/tool-skills - config: - skills: - - "git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=skills" - - "git+https://github.com/microsoft/amplifier-bundle-skills@main#subdirectory=skills" - visibility: - enabled: false - - module: tool-mode - source: git+https://github.com/microsoft/amplifier-bundle-modes@main#subdirectory=modules/tool-mode - config: - gate_policy: "warn" - - module: tool-recipes - source: git+https://github.com/microsoft/amplifier-bundle-recipes@main#subdirectory=modules/tool-recipes - config: - session_dir: ~/.amplifier/projects/{project}/recipe-sessions - auto_cleanup_days: 7 - -hooks: - - module: hooks-todo-reminder - source: git+https://github.com/microsoft/amplifier-module-hooks-todo-reminder@main - config: - inject_role: user - priority: 10 - - module: hooks-todo-display - source: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/hooks-todo-display - config: - show_progress_bar: true - show_border: true - - module: hooks-session-naming - source: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/hooks-session-naming - config: - initial_trigger_turn: 2 - update_interval_turns: 5 - - module: hooks-mode - source: git+https://github.com/microsoft/amplifier-bundle-modes@main#subdirectory=modules/hooks-mode - config: - search_paths: [] - - module: hooks-approval - source: git+https://github.com/microsoft/amplifier-module-hooks-approval - config: - rules: [] - default_action: continue - policy_driven_only: true - -agents: - include: - - anchors:explorer - - anchors:architect - - anchors:builder - - anchors:debugger - - anchors:git-ops - - anchors:researcher ---- - -@anchors:context/system.md \ No newline at end of file diff --git a/bundles/anchors/README.md b/bundles/anchors/README.md index 0a0f8e1..b9ac5ba 100644 --- a/bundles/anchors/README.md +++ b/bundles/anchors/README.md @@ -1,7 +1,8 @@ # Anchors Bundle -The CLI's current default runtime bundle and the recommended supporting root for a -new complete host. It remains a lean experimental bundle that shapes the agent's +The lean base host included by the CLI's default `anchors-amp-dev` bundle and the +recommended supporting root for a new complete host. It remains a lean +experimental bundle that shapes the agent's conduct with a short, explicit set of **behavioral principles** placed at the very top of the system prompt -- rather than encoding behavior across large rule documents. @@ -14,12 +15,13 @@ still producing disciplined, delegation-aware behavior. ## Install ```bash -amplifier bundle add 'git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md' --name anchors +amplifier bundle add 'git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors' --name anchors amplifier bundle use anchors ``` -Single-quote the URI to prevent shell expansion of the `#` fragment. The `.md` -suffix is required. +Single-quote the URI to prevent shell expansion of the `#` fragment. The directory +selects `bundle.md`; an explicit `#subdirectory=bundles/anchors/bundle.md` also +names the same manifest. ## The idea @@ -77,20 +79,18 @@ for them. Every module, tool, hook, and behavior is referenced by its full `git+https://...` source URL rather than a `foundation:` namespace. The bundle's own agents and context are referenced through its own `anchors:` -namespace. The canonical manifest is `bundles/anchors.md`; its -`namespace_root: anchors` is relative to the manifest's containing `bundles/` -directory, so assets still resolve under `bundles/anchors/`. It does not require -the Foundation runtime to be composed. Keep the manifest and its asset directory -together when relocating it. +namespace. The canonical manifest is `bundles/anchors/bundle.md`; its containing +directory is the asset root, so no `namespace_root` override is needed. It does +not require the Foundation runtime to be composed. Keep the manifest and its +agents and context together when relocating it. ## Files ``` bundles/ -├── anchors.md # canonical session / tools / hooks / agents └── anchors/ ├── README.md # this file - ├── bundle.md # compatibility wrapper for anchors.md + ├── bundle.md # canonical session / tools / hooks / agents ├── agents/ │ ├── explorer.md # multi-file recon │ ├── architect.md # design / spec / review @@ -115,17 +115,17 @@ boundary is not permission to ignore rules that apply after discovery. Promoted out of an `experiments/` prototype to a published bundle by 70a84d0 (#259); `anchors-amp-dev` followed in 78d0abe (#273). The prototype trees were -deleted once promotion made them stale copies -- read `bundles/anchors.md` and -`bundles/anchors-amp-dev.md` for the live manifests, and those two commits (or +deleted once promotion made them stale copies -- read `bundles/anchors/bundle.md` +and `bundles/anchors-amp-dev/bundle.md` for the live manifests, and those two commits (or `git log --diff-filter=D -- experiments/behavioral-anchor`) for the originals. -Version 0.3.0 -- the flat manifest retains the evaluated (#327) principle and +Version 0.3.0 -- the nested manifest retains the historically evaluated (#327) principle and agent text, and is the source of the runtime that `anchors-amp-dev` includes. The principle set and tool/agent roster are a starting point and will be adjusted as observation shows what helps or hurts. -`bundles/anchors/bundle.md` remains a compatibility entry point. The complete -Anchors root chooses `loop-streaming` and `context-simple`; reusable behaviors do -not choose either runtime, and applications may override those defaults. -Validation of the flat-root migration is pending; the historical evaluations -above do not qualify this new layout. +The nested manifest is the only entry point, not a compatibility wrapper. The +complete Anchors root chooses `loop-streaming` and `context-simple`; reusable +behaviors do not choose either runtime, and applications may override those +defaults. The historical evaluations above do not qualify this nested-only +layout. diff --git a/bundles/anchors/bundle.md b/bundles/anchors/bundle.md index d6d42be..57e485b 100644 --- a/bundles/anchors/bundle.md +++ b/bundles/anchors/bundle.md @@ -3,10 +3,115 @@ bundle: name: anchors display_name: Anchors version: 0.3.0 - description: Compatibility entry point for the flat Anchors host. + description: Lean principle-driven host with purposeful agents and a standard development tool roster. includes: - - bundle: foundation:bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/streaming-ui.yaml + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/status-context.yaml + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/redaction.yaml + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=behaviors/logging.yaml + - bundle: git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=behaviors/context-intelligence-logging.yaml + - bundle: git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=behaviors/context-intelligence-navigation.yaml + # Role routing remains a host choice, not part of the amp-dev capability. + - bundle: git+https://github.com/microsoft/amplifier-bundle-routing-matrix@main#subdirectory=behaviors/routing.yaml + +session: + raw: true + orchestrator: + module: loop-streaming + source: git+https://github.com/microsoft/amplifier-module-loop-streaming@main + config: + extended_thinking: true + context: + module: context-simple + source: git+https://github.com/microsoft/amplifier-module-context-simple@main + config: + compact_threshold: 0.8 + auto_compact: true + +tools: + - module: tool-filesystem + source: git+https://github.com/microsoft/amplifier-module-tool-filesystem@main + - module: tool-bash + source: git+https://github.com/microsoft/amplifier-module-tool-bash@main + - module: tool-web + source: git+https://github.com/microsoft/amplifier-module-tool-web@main + - module: tool-search + source: git+https://github.com/microsoft/amplifier-module-tool-search@main + - module: tool-todo + source: git+https://github.com/microsoft/amplifier-module-tool-todo@main + - module: tool-apply-patch + source: git+https://github.com/microsoft/amplifier-bundle-filesystem@main#subdirectory=modules/tool-apply-patch + - module: tool-delegate + source: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/tool-delegate + config: + features: + self_delegation: + enabled: true + session_resume: + enabled: true + context_inheritance: + enabled: true + max_turns: 10 + provider_selection: + enabled: true + settings: + exclude_tools: [tool-delegate] + timeout: null + max_llm_calls: null + - module: tool-skills + source: git+https://github.com/microsoft/amplifier-bundle-skills@main#subdirectory=modules/tool-skills + config: + skills: + - "git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=skills" + - "git+https://github.com/microsoft/amplifier-bundle-skills@main#subdirectory=skills" + visibility: + enabled: false + - module: tool-mode + source: git+https://github.com/microsoft/amplifier-bundle-modes@main#subdirectory=modules/tool-mode + config: + gate_policy: "warn" + - module: tool-recipes + source: git+https://github.com/microsoft/amplifier-bundle-recipes@main#subdirectory=modules/tool-recipes + config: + session_dir: ~/.amplifier/projects/{project}/recipe-sessions + auto_cleanup_days: 7 + +hooks: + - module: hooks-todo-reminder + source: git+https://github.com/microsoft/amplifier-module-hooks-todo-reminder@main + config: + inject_role: user + priority: 10 + - module: hooks-todo-display + source: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/hooks-todo-display + config: + show_progress_bar: true + show_border: true + - module: hooks-session-naming + source: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=modules/hooks-session-naming + config: + initial_trigger_turn: 2 + update_interval_turns: 5 + - module: hooks-mode + source: git+https://github.com/microsoft/amplifier-bundle-modes@main#subdirectory=modules/hooks-mode + config: + search_paths: [] + - module: hooks-approval + source: git+https://github.com/microsoft/amplifier-module-hooks-approval + config: + rules: [] + default_action: continue + policy_driven_only: true + +agents: + include: + - anchors:explorer + - anchors:architect + - anchors:builder + - anchors:debugger + - anchors:git-ops + - anchors:researcher --- @anchors:context/system.md \ No newline at end of file diff --git a/context/understanding-mechanisms/mechanisms/bundles.md b/context/understanding-mechanisms/mechanisms/bundles.md index 3ac766b..c4e459e 100644 --- a/context/understanding-mechanisms/mechanisms/bundles.md +++ b/context/understanding-mechanisms/mechanisms/bundles.md @@ -67,7 +67,7 @@ not require replacing that host: ```yaml includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md - bundle: recipes:behaviors/recipes ``` @@ -75,12 +75,15 @@ The root preserves `@anchors:context/system.md` when it has its own instruction body. Existing selected legacy roots remain valid. A root can also anchor repo-level namespaced resources, so behavior-first is not a root ban. -For example, `bundles/anchors-amp-dev.md` composes `bundles/anchors.md` plus +For example, `bundles/anchors-amp-dev/bundle.md` composes `bundles/anchors/bundle.md` plus `behaviors/amp-dev.yaml`. All Amplifier-development additions belong to that runtime-neutral behavior, including the `amp-dev:amplifier-dev-expert` agent and Tester behavior. Another host can compose the same capability without Anchors. -The flat Anchors manifest uses `namespace_root: anchors`, relative to its -containing `bundles/` directory, to retain the existing `anchors:` assets. +The Anchors manifest sits beside its `agents/` and `context/` assets, so its +directory is the `anchors:` resource root without a namespace override. The +variant also keeps its namespace at its manifest directory, with no variant +assets or namespace override. Self-namespaced relative includes select its base +and capability from the same repository, including on direct local loads. ### Behaviors diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index f87c51f..4367d87 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -174,7 +174,7 @@ from amplifier_foundation import load_bundle base = await load_bundle( "git+https://github.com/microsoft/amplifier-foundation@main" - "#subdirectory=bundles/anchors.md" + "#subdirectory=bundles/anchors/bundle.md" ) overlay = await load_bundle("./local-overlay.md") composed = base.compose(overlay) diff --git a/docs/APPLICATION_INTEGRATION_GUIDE.md b/docs/APPLICATION_INTEGRATION_GUIDE.md index 8674321..1740354 100644 --- a/docs/APPLICATION_INTEGRATION_GUIDE.md +++ b/docs/APPLICATION_INTEGRATION_GUIDE.md @@ -227,7 +227,7 @@ bundle: version: 1.0.0 includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md - bundle: my-app:behaviors/domain-expert --- diff --git a/docs/BUNDLE_GUIDE.md b/docs/BUNDLE_GUIDE.md index 1f1d9cb..af01f39 100644 --- a/docs/BUNDLE_GUIDE.md +++ b/docs/BUNDLE_GUIDE.md @@ -86,7 +86,7 @@ bundle: description: Runnable composition for my capability includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md - bundle: my-capability:behaviors/my-capability --- @@ -109,11 +109,11 @@ off; the app CLI composes its full skills behavior for its own sessions. Add additional behaviors explicitly when a complete host needs them. Do not infer other runtime semantics from a manifest field name. -The canonical Anchors manifest is `bundles/anchors.md`. Its -`bundle.namespace_root: anchors` is relative to the containing `bundles/` -directory, preserving `anchors:agents/` and `anchors:context/` resources in the -existing asset directory. `bundles/anchors/bundle.md` remains a compatibility -wrapper; new compositions should name the flat manifest. +The canonical Anchors manifest is `bundles/anchors/bundle.md`. Its containing +directory is the asset root for `anchors:agents/` and `anchors:context/`, so it +needs no `namespace_root` override. A directory URI ending in +`#subdirectory=bundles/anchors` selects the same manifest. There are no flat +Anchors manifests or compatibility wrappers. Foundation library imports, `foundation:` documentation namespaces, provider partials, and selected legacy Foundation roots remain valid. Behavior-first does @@ -362,7 +362,7 @@ its supporting root composes the behavior and preserves the Anchors instruction: bundle: name: my-capability includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md - bundle: my-capability:behaviors/my-capability --- @@ -436,14 +436,14 @@ Legitimate awareness content, in practice: ### The thin-variant pattern (worked example) -`bundles/anchors.md` and `bundles/anchors-amp-dev.md` in this repository are the +`bundles/anchors/bundle.md` and `bundles/anchors-amp-dev/bundle.md` in this repository are the worked example. The variant composes one complete base and one portable capability: ```yaml includes: - - bundle: foundation:bundles/anchors.md - - bundle: foundation:behaviors/amp-dev.yaml + - bundle: anchors-amp-dev:../anchors/bundle.md + - bundle: anchors-amp-dev:../../behaviors/amp-dev.yaml ``` Its body preserves `@anchors:context/system.md`. All Amplifier-development @@ -452,15 +452,21 @@ additions live in `behaviors/amp-dev.yaml`: the lean behavior with its transitive DTU/Gitea capability. The behavior selects no runtime and includes no Anchors root. Another complete host can compose the same capability while keeping its own base, runtime, and instruction. There is no -second copy of the base or capability to keep in sync. The nested -`bundles/anchors-amp-dev/bundle.md` path is a compatibility wrapper; the old +second copy of the base or capability to keep in sync. The variant keeps its +`anchors-amp-dev:` namespace at the manifest directory, without a `namespace_root` +override or variant assets. Its relative includes normalize to the base and +capability in the same repository, preserving the selected Git ref. This keeps +direct local directory and manifest loads source-local without separately +registering Foundation. The +nested path is canonical, not a compatibility wrapper; the old `anchors-amp-dev:amplifier-dev-expert` alias is not retained. Instruction preservation, namespace resolution, and intended skill precedence -need composition checks. Local migration qualification passes those checks. -The portable capability also passed real expert-spawn checks in an isolated -non-Anchors host. A structural description alone is not runtime evidence, and -that bounded run does not establish acceptance of every complete host. +need composition checks. Historical flat-root migration qualification passed +those checks, and the portable capability passed real expert-spawn checks in an +isolated non-Anchors host. Those observations do not qualify the nested-only +layout. A structural description alone is not runtime evidence, and that +bounded run does not establish acceptance of every complete host. ### Validation payload lifetime @@ -479,7 +485,7 @@ repository, supply `AMPLIFIER_RECIPE_SCRATCH_DIR` outside the target tree or add `.validate-bundle-payloads-*/` to that repository's ignore rules. POSIX mode checks do not establish Windows ACL protection. -The historical head-cost figures below precede the flat-root migration; no +The historical head-cost figures below precede the flat-root and nested-only migrations; no remeasurement of this layout is claimed here. Measured head cost (`validate-bundle-repo.yaml` Phase 2.86): anchors **2,483 @@ -572,7 +578,7 @@ Bundle repos follow **conventions** that enable maximum reusability and composit **Supporting root** (`/bundle.md`): An optional complete runnable composition. When present, it establishes the usual enclosing namespace layout and includes its own behavior. It is structurally a root bundle, but it is not the reusable capability's required entry point. -**Standalone bundles** (`/bundles/*.yaml` or `/bundles/*.md`): Pre-composed variants ready to use as-is. They can combine a base with a provider or capability choice. Examples: `with-anthropic.yaml`, `anchors-amp-dev.md`. These are structurally "nested bundles" (loaded via `namespace:bundles/foo`) but conventionally "standalone" because they're complete and ready to use. +**Standalone bundles** (`/bundles/*.yaml`, `/bundles/*.md`, or `/bundles//bundle.md`): Pre-composed variants ready to use as-is. They can combine a base with a provider or capability choice. Examples: `with-anthropic.yaml`, `anchors-amp-dev/bundle.md`. These are structurally "nested bundles" (loaded via `namespace:bundles/foo`) but conventionally "standalone" because they're complete and ready to use. **Behavior bundles** (`/behaviors/*.yaml`): The reusable capability this repo provides. When someone wants to add your capability to their existing host, they include this behavior. It contains agents, context, and optionally capability-specific tools or hooks; it does not choose the host's root, provider, orchestrator, context manager, or root instruction. @@ -591,7 +597,7 @@ bundle: version: 1.0.0 includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md - bundle: my-capability:behaviors/my-capability # DRY: include own behavior ``` @@ -867,7 +873,7 @@ bundle: description: Provides X capability includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md - bundle: my-capability:behaviors/my-capability --- @@ -1085,7 +1091,7 @@ Use this self-referential git URL form even when the module lives in the bundle' ```yaml # DON'T DO THIS when a supporting root already provides the complete host includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md tools: - module: tool-filesystem # The supporting root has this! @@ -1496,7 +1502,7 @@ bundle: description: What this bundle provides includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md - bundle: my-bundle:behaviors/x # Include behaviors # Only declare additional tools NOT inherited from Anchors @@ -1556,7 +1562,7 @@ can also select Anchors as its base: ```yaml includes: - - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md # Supporting root only + - bundle: git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md # Supporting root only - bundle: git+https://github.com/org/capability@main#subdirectory=behaviors/feature.yaml - bundle: my-bundle:behaviors/foo # Behavior within the same repository ``` @@ -1814,7 +1820,7 @@ includes: Put reusable capability configuration in `behaviors/`. If a complete host is also useful, keep its root thin: compose the chosen base plus the behavior and explicitly preserve that base's instruction in its own body. For a new Anchors -host, use `bundles/anchors.md` and `@anchors:context/system.md`; an existing host +host, use `bundles/anchors/bundle.md` and `@anchors:context/system.md`; an existing host keeps its own base. Neither orchestrator nor context-manager selection belongs in the capability behavior. diff --git a/recipes/validate-bundle-repo.yaml b/recipes/validate-bundle-repo.yaml index 681a252..1681b1e 100644 --- a/recipes/validate-bundle-repo.yaml +++ b/recipes/validate-bundle-repo.yaml @@ -1,9 +1,12 @@ # validate-bundle-repo.yaml -# Repository-Wide Bundle Validator Recipe v3.17.1 +# Repository-Wide Bundle Validator Recipe v3.17.2 # Validates an entire bundle repository against structural requirements, conventions, # and Amplifier bundle philosophy (context sink pattern, thin behaviors, tool placement) # # CHANGELOG: +# v3.17.2 (2026-10-06) - BUGFIX: Active Anchors guidance still named the removed +# flat entry point. Use bundles/anchors/bundle.md; discovery unchanged. +# # v3.17.1 - A full Foundation run failed before quality-classification could # start: its rendered command was 139,923 bytes (Linux argv limit). # Discovery, individual validation, and tool placement now retain @@ -609,7 +612,7 @@ description: | - All measurements use TOKENS (len/4), never line counts For single bundle validation, use validate-bundle.yaml instead. -version: "3.17.1" +version: "3.17.2" author: "Amplifier Foundation Team" tags: ["bundle", "validation", "quality", "conventions", "repository", "packaging", "hygiene", "context-sink"] @@ -2816,7 +2819,7 @@ steps: "fix": ( "Add session configuration or compose the Anchors supporting base " "(git+https://github.com/microsoft/amplifier-foundation@main" - "#subdirectory=bundles/anchors.md) or another intentionally " + "#subdirectory=bundles/anchors/bundle.md) or another intentionally " "selected complete host." ) }) @@ -5557,7 +5560,7 @@ steps: "primary capability path. If a complete host is needed, use " "the Anchors supporting base " "`git+https://github.com/microsoft/amplifier-foundation@main" - "#subdirectory=bundles/anchors.md` with explicit " + "#subdirectory=bundles/anchors/bundle.md` with explicit " "registration/selection, or set root_bundle_repo: \"true\" " "for a genuine root-product repository." ) @@ -7445,7 +7448,7 @@ steps: - If skipped == true: "README install-instructions convention check: skipped ()" - For each finding of type readme_recommends_root_bundle: - [NEEDS WORK] the finding's message (README's primary capability install is not a canonical behavior URI) - - The fix: lead with `amplifier bundle add "#subdirectory=behaviors/.yaml" --app`. A new complete host can explicitly register/select the Anchors supporting base (`git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md`); root_bundle_repo: "true" remains for a genuine root-product repo. + - The fix: lead with `amplifier bundle add "#subdirectory=behaviors/.yaml" --app`. A new complete host can explicitly register/select the Anchors supporting base (`git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md`); root_bundle_repo: "true" remains for a genuine root-product repo. - For each finding of type readme_missing_app_flag: - [NEEDS WORK] the finding's message (behavior installation commands missing --app) - The fix: add --app to each behavior `amplifier bundle add` command. Do not add it merely to register/select a supporting root. diff --git a/recipes/validate-single-bundle.yaml b/recipes/validate-single-bundle.yaml index f7f91d1..a656d9f 100644 --- a/recipes/validate-single-bundle.yaml +++ b/recipes/validate-single-bundle.yaml @@ -9,6 +9,10 @@ # CHANGELOG # ============================================================================= # +# v2.5.1 (2026-10-06): +# - BUGFIX: Active Anchors guidance still named the removed flat entry point. +# Use bundles/anchors/bundle.md; validation logic unchanged. +# # v2.5.0: # - Reject session.orchestrator and session.context in behavior targets; # complete roots own runtime defaults and applications may override them. @@ -110,7 +114,7 @@ description: | Behavior partials are valid targets: assess their composability and hygiene, not standalone session completeness. New complete hosts should normally use Anchors as their supporting base and compose capabilities explicitly. -version: "2.5.0" +version: "2.5.1" author: "Amplifier Foundation Team" tags: ["bundle", "validation", "single", "dependency-tracing", "yaml-lint"] @@ -887,7 +891,7 @@ steps: - If it is `standalone` or `root`, report whether its declared composition supplies the appropriate runnable session surface. For a newly authored complete host, recommend the Anchors supporting base - (`git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors.md`) + (`git+https://github.com/microsoft/amplifier-foundation@main#subdirectory=bundles/anchors/bundle.md`) and explicit capability behaviors; keep intentionally selected legacy roots as reported configuration, not an automatic failure. diff --git a/tests/test_amp_dev_composition.py b/tests/test_amp_dev_composition.py index 5013a62..1fb1c7e 100644 --- a/tests/test_amp_dev_composition.py +++ b/tests/test_amp_dev_composition.py @@ -1,15 +1,38 @@ -"""Real registry composition of the portable capability and flat host roots.""" +"""Real registry composition and resources of the nested-only host roots.""" +import io +import logging from pathlib import Path +from unittest.mock import AsyncMock, MagicMock import pytest import yaml from amplifier_foundation import Bundle, BundleRegistry from amplifier_foundation.bundle._prepared import PreparedBundle, BundleModuleResolver +from amplifier_foundation.configurator._inspector import ( + _reset_cycle_warnings_for_testing, + walk_include_chains, +) from amplifier_foundation.mentions import BaseMentionResolver +from amplifier_foundation.modules.activator import ModuleActivator +from amplifier_foundation.paths.resolution import ResolvedSource, parse_uri ROOT = Path(__file__).resolve().parents[1] FOUNDATION = "git+https://github.com/microsoft/amplifier-foundation@main" +ADDED_AGENTS = { + "amp-dev:amplifier-dev-expert", "amplifier-tester:setup-digital-twin", + "amplifier-tester:validator", "digital-twin-universe:dtu-profile-builder", +} + + +def read_utf8(path): + return path.read_text(encoding="utf-8") + + +@pytest.fixture(scope="module") +def registry_home(tmp_path_factory): + """Share downloaded sources, never registrations or loaded bundle state.""" + return tmp_path_factory.mktemp("nested-bundle-registry") def registry(home): @@ -27,6 +50,96 @@ def resolver(bundle): return BaseMentionResolver(bundles=prepared._build_bundles_for_resolver(bundle)) +def entry(name, form): + path = ROOT / "bundles" / name + if "manifest" in form: + path /= "bundle.md" + if form.startswith("root-"): + return ROOT.as_uri() + "#subdirectory=" + path.relative_to(ROOT).as_posix() + return path.as_uri() if form.endswith("-uri") else str(path) + + +def assert_cycle_free(loader): + states = {name: loader.get_state(name) for name in loader.list_registered()} + for name, state in states.items(): + assert name not in (state.includes or []), name + assert name not in (state.included_by or []), name + + def visit(name, chain): + assert name not in chain, f"Registry cycle: {chain + (name,)}" + for child in states[name].includes or []: + if child in states: + visit(child, chain + (name,)) + + for name in states: + visit(name, ()) + # Exercise the production configurator traversal (also used by bundle show), + # resetting warning deduplication so an earlier test cannot hide a cycle. + _reset_cycle_warnings_for_testing() + for name in states: + chains = walk_include_chains(name, states) + assert chains, name + for chain in chains: + names = [step.bundle for step in chain] + assert names[-1] == name + assert len(names) == len(set(names)), names + + +async def prompt(bundle, cwd, monkeypatch): + """Prepare context and exercise the prompt factory, not runtime activation.""" + monkeypatch.setattr(ModuleActivator, "activate_all", AsyncMock(return_value={})) + monkeypatch.setattr(ModuleActivator, "finalize", lambda self: None) + prepared = await bundle.prepare(install_deps=False) + # create_session resolves these before wiring its prompt factory. + prepared.bundle.resolve_pending_context() + session = MagicMock() + session.coordinator.hooks.emit = AsyncMock() + return await prepared.create_system_prompt_factory(session, session_cwd=cwd)() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("eol", [b"\n", b"\r\n"], ids=["LF", "CRLF"]) +async def test_prompt_oracle_uses_utf8_with_cp1252_default(tmp_path, monkeypatch, eol): + """The shared expected reader must match runtime UTF-8, not the locale.""" + system = tmp_path / "system.md" + ecosystem = tmp_path / "ecosystem.md" + system_text = "Host system\n" + ecosystem_text = "# Ecosystem\ncore → foundation → modules → bundles → apps\n" + for path, text in ((system, system_text), (ecosystem, ecosystem_text)): + path.write_bytes(text.encode("utf-8").replace(b"\n", eol)) + + bundle = Bundle( + name="fixture", base_path=tmp_path, instruction="@fixture:system.md", + source_base_paths={"fixture": tmp_path}, + _pending_context={ + "fixture:ecosystem.md": "fixture:ecosystem.md", + "fixture:system.md": "fixture:system.md", + }, + ) + bundle.resolve_pending_context() + assert bundle.context == { + "fixture:ecosystem.md": ecosystem, "fixture:system.md": system, + } + prepared = PreparedBundle({}, BundleModuleResolver(module_paths={}), bundle) + session = MagicMock() + session.coordinator.hooks.emit = AsyncMock() + with monkeypatch.context() as locale_default: + # Simulate a legacy default even when the interpreter enables UTF-8 mode. + # Explicit codecs, including the runtime's UTF-8 reads, stay unchanged. + locale_default.setattr(io, "text_encoding", lambda encoding, stacklevel=2: + "cp1252" if encoding is None else encoding) + head = await prepared.create_system_prompt_factory(session, session_cwd=tmp_path)() + expected_system = read_utf8(system) + expected_ecosystem = read_utf8(ecosystem) + assert head.count(expected_system) == 1 + assert head.count(expected_ecosystem) == 1 + assert head.index(expected_system) < head.index(expected_ecosystem) + assert (expected_system, expected_ecosystem) == (system_text, ecosystem_text) + implicit_ecosystem = ecosystem.read_text() + assert implicit_ecosystem != ecosystem_text + assert head.count(implicit_ecosystem) == 0 + + @pytest.mark.asyncio async def test_behavior_is_portable_and_preserves_both_host_runtimes(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) @@ -43,34 +156,68 @@ async def test_behavior_is_portable_and_preserves_both_host_runtimes(tmp_path, m assert combined.instruction == host.instruction assert "host-tool" in {row["module"] for row in combined.tools} assert not any(key.startswith("anchors:") for key in behavior.agents) - assert set(behavior.agents) == { - "amp-dev:amplifier-dev-expert", "amplifier-tester:setup-digital-twin", - "amplifier-tester:validator", "digital-twin-universe:dtu-profile-builder"} + assert set(behavior.agents) == ADDED_AGENTS resources = resolver(behavior) assert resources.resolve("@amp-dev:agents/amplifier-dev-expert.md") == ROOT / "agents/amplifier-dev-expert.md" assert resources.resolve("@foundation:context/amplifier-dev/ecosystem-map.md") == ROOT / "context/amplifier-dev/ecosystem-map.md" assert resources.resolve("@foundation:bundles/anchors/context/agent-baseline.md") == ROOT / "bundles/anchors/context/agent-baseline.md" assert resources.resolve("@amp-dev:context/amplifier-dev/amplifier-ecosystem.md") == ROOT / "context/amplifier-dev/amplifier-ecosystem.md" expert = behavior.agents["amp-dev:amplifier-dev-expert"] - assert "@anchors:" not in (ROOT / "agents/amplifier-dev-expert.md").read_text() + assert "@anchors:" not in read_utf8(ROOT / "agents/amplifier-dev-expert.md") assert "provider_preferences" not in expert skills = next(row for row in behavior.tools if row["module"] == "tool-skills")["config"]["skills"] assert any("amplifier-bundle-gitea@" in path for path in skills) assert any("amplifier-bundle-digital-twin-universe@" in path for path in skills) + head = await prompt(combined, tmp_path, monkeypatch) + assert head.startswith(host.instruction) + assert head.count(read_utf8(ROOT / "context/amplifier-dev/amplifier-ecosystem.md")) == 1 + assert read_utf8(ROOT / "bundles/anchors/context/system.md") not in head @pytest.mark.asyncio -async def test_flat_roots_and_compatibility_entrypoints_resolve_equally(tmp_path, monkeypatch): +@pytest.mark.parametrize("form", [ + "directory", "manifest", "directory-uri", "manifest-uri", + "root-directory-uri", "root-manifest-uri", +]) +@pytest.mark.parametrize("order", [ + ("anchors", "anchors-amp-dev"), ("anchors-amp-dev", "anchors"), +]) +@pytest.mark.parametrize("custom_aliases", [False, True], ids=["canonical", "custom-aliases"]) +async def test_nested_roots_compose_source_local_without_cycles( + tmp_path, monkeypatch, registry_home, caplog, form, order, custom_aliases +): monkeypatch.chdir(tmp_path) - loader = registry(tmp_path / "registry") - anchors = await loader.load(str(ROOT / "bundles/anchors.md")) - amp_dev = await loader.load(str(ROOT / "bundles/anchors-amp-dev.md")) + caplog.set_level(logging.WARNING) + loader = registry(registry_home) + assert loader.list_registered() == [] + names = {name: f"custom-{name}" if custom_aliases else name for name in order} + loader.register({names[name]: entry(name, form) for name in order}) + assert loader.find("foundation") is None + loaded = {} + for name in order: + loaded[name] = await loader.load(names[name]) + assert_cycle_free(loader) + anchors = loaded["anchors"] + amp_dev = loaded["anchors-amp-dev"] + behavior = await loader.load(str(ROOT / "behaviors/amp-dev.yaml")) + assert anchors.name == "anchors" and anchors.display_name == "Anchors" + assert amp_dev.name == "anchors-amp-dev" + assert amp_dev.display_name == "Anchors · Amplifier development" + assert amp_dev.source_base_paths["anchors-amp-dev"] == ROOT / "bundles/anchors-amp-dev" + assert anchors.version == amp_dev.version == "0.3.0" + assert amp_dev.to_mount_plan() == anchors.compose(behavior).to_mount_plan() assert anchors.session == amp_dev.session assert anchors.hooks == amp_dev.hooks - assert set(amp_dev.agents) - set(anchors.agents) == { - "amp-dev:amplifier-dev-expert", "amplifier-tester:setup-digital-twin", - "amplifier-tester:validator", "digital-twin-universe:dtu-profile-builder"} - assert anchors.instruction == amp_dev.instruction + assert anchors.providers == amp_dev.providers == [] + assert set(amp_dev.agents) - set(anchors.agents) == ADDED_AGENTS + assert all(amp_dev.agents[name] == agent for name, agent in anchors.agents.items()) + for bundle in (anchors, amp_dev, behavior): + bundle.resolve_pending_context() + assert amp_dev.context == {**anchors.context, **behavior.context} + assert anchors.instruction == amp_dev.instruction == "@anchors:context/system.md" + assert {row["module"] for row in anchors.tools} == {row["module"] for row in amp_dev.tools} + assert [row for row in anchors.tools if row["module"] != "tool-skills"] == [ + row for row in amp_dev.tools if row["module"] != "tool-skills"] skills = next(row for row in amp_dev.tools if row["module"] == "tool-skills")["config"]["skills"] assert skills == [ "git+https://github.com/microsoft/amplifier-bundle-context-intelligence@main#subdirectory=skills/context-intelligence-session-navigation", @@ -81,15 +228,106 @@ async def test_flat_roots_and_compatibility_entrypoints_resolve_equally(tmp_path "git+https://github.com/microsoft/amplifier-bundle-gitea@main#subdirectory=skills", "git+https://github.com/microsoft/amplifier-bundle-digital-twin-universe@main#subdirectory=skills"] assert resolver(amp_dev).resolve("@anchors:context/system.md") == ROOT / "bundles/anchors/context/system.md" - for name, expected in (("anchors", anchors), ("anchors-amp-dev", amp_dev)): - old = await loader.load(str(ROOT / f"bundles/{name}/bundle.md")) - assert old.to_mount_plan() == expected.to_mount_plan() - assert old.instruction == expected.instruction - assert resolver(old).resolve("@anchors:context/system.md") == ROOT / "bundles/anchors/context/system.md" + assert resolver(anchors).resolve("@anchors:agents/builder.md") == ROOT / "bundles/anchors/agents/builder.md" + assert resolver(amp_dev).resolve("@amp-dev:agents/amplifier-dev-expert.md") == ROOT / "agents/amplifier-dev-expert.md" + for reference in ( + "docs/BUNDLE_GUIDE.md", "docs/AGENT_AUTHORING.md", + "context/shared/description-authoring-principles.md", + "context/amplifier-dev/ecosystem-map.md", + "context/amplifier-dev/dev-workflows.md", + "context/amplifier-dev/testing-patterns.md", + "bundles/anchors/context/agent-baseline.md", + ): + assert resolver(amp_dev).resolve(f"@foundation:{reference}") == ROOT / reference + assert not any(name.startswith("foundation:") for name in amp_dev.agents) + assert str(ROOT) not in loader._loaded_bundles and ROOT.as_uri() not in loader._loaded_bundles + system = read_utf8(ROOT / "bundles/anchors/context/system.md") + ecosystem = read_utf8(ROOT / "context/amplifier-dev/amplifier-ecosystem.md") + head = await prompt(amp_dev, tmp_path, monkeypatch) + assert head.count(system) == 1 + assert head.count(ecosystem) == 1 + assert head.index(system) < head.index(ecosystem) + assert read_utf8(ROOT / "bundles/anchors/context/agent-baseline.md") not in head + assert not caplog.records, [(row.name, row.getMessage()) for row in caplog.records] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("manifest_path", [ + "bundles/anchors-amp-dev", "bundles/anchors-amp-dev/bundle.md", +]) +@pytest.mark.parametrize("ref", ["main", "qualified-candidate"]) +async def test_relative_includes_normalize_children_and_preserve_selected_git_ref( + tmp_path, monkeypatch, registry_home, caplog, manifest_path, ref +): + """Use a local checkout binding, without fetching the synthetic selected ref.""" + monkeypatch.chdir(tmp_path) + caplog.set_level(logging.WARNING) + loader = registry(registry_home) + selected = FOUNDATION.rsplit("@", 1)[0] + "@" + ref + resolve = loader._source_resolver.resolve + + async def candidate_checkout(uri): + if uri.startswith(selected + "#subdirectory="): + parsed = parse_uri(uri) + return ResolvedSource(active_path=ROOT / parsed.subpath, source_root=ROOT) + return await resolve(uri) + + monkeypatch.setattr(loader._source_resolver, "resolve", candidate_checkout) + root_uri = selected + "#subdirectory=" + manifest_path + variant = await loader.load(root_uri) + assert variant._source_uri == root_uri + assert variant.source_base_paths["anchors-amp-dev"] == ROOT / "bundles/anchors-amp-dev" + for name, path in ( + ("anchors", "bundles/anchors/bundle.md"), + ("amp-dev", "behaviors/amp-dev.yaml"), + ): + expected = selected + "#subdirectory=" + path + child = loader._loaded_bundles[expected] + assert loader.get_state(name).uri == child._source_uri == expected + assert parse_uri(child._source_uri).ref == ref + assert loader._resolved_sources[expected].active_path == ROOT / path + assert loader._resolved_sources[expected].source_root == ROOT + assert_cycle_free(loader) + assert not caplog.records, [(row.name, row.getMessage()) for row in caplog.records] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("name", ["anchors", "anchors-amp-dev"]) +@pytest.mark.parametrize("custom_alias", [False, True], ids=["canonical", "custom-alias"]) +async def test_nested_reload_repairs_stale_self_edges_and_preserves_unrelated_relations( + tmp_path, monkeypatch, registry_home, caplog, name, custom_alias +): + monkeypatch.chdir(tmp_path) + caplog.set_level(logging.WARNING) + loader = registry(registry_home) + registered_name = f"custom-{name}" if custom_alias else name + loader.register({ + registered_name: entry(name, "manifest-uri"), + "unrelated-child": str(tmp_path / "unrelated-child"), + "unrelated-parent": str(tmp_path / "unrelated-parent"), + }) + state = loader.get_state(registered_name) + state.includes = [registered_name, "unrelated-child"] + state.included_by = [registered_name, "unrelated-parent"] + loader.get_state("unrelated-child").included_by = [registered_name, "unrelated-parent"] + loader.get_state("unrelated-parent").includes = [registered_name, "unrelated-child"] + + await loader.load(registered_name) + await loader._load_single(registered_name, refresh=True) + + assert state.includes and registered_name not in state.includes + assert "unrelated-child" not in state.includes + assert state.included_by == ["unrelated-parent"] + assert loader.get_state("unrelated-child").included_by == ["unrelated-parent"] + assert loader.get_state("unrelated-parent").includes == [registered_name, "unrelated-child"] + if name == "anchors-amp-dev": + assert state.includes == ["anchors", "amp-dev"] + assert_cycle_free(loader) + assert not caplog.records, [(row.name, row.getMessage()) for row in caplog.records] def test_behavior_contains_entire_delta_without_root_inclusion(): - behavior = yaml.safe_load((ROOT / "behaviors/amp-dev.yaml").read_text()) + behavior = yaml.safe_load(read_utf8(ROOT / "behaviors/amp-dev.yaml")) assert not {"session", "providers"} & behavior.keys() assert behavior["includes"] == [{"bundle": "git+https://github.com/microsoft/amplifier-bundle-amplifier-tester@main#subdirectory=behaviors/amplifier-tester.yaml"}] diff --git a/tests/test_anchors_agent_contracts.py b/tests/test_anchors_agent_contracts.py index af427e7..38e806b 100644 --- a/tests/test_anchors_agent_contracts.py +++ b/tests/test_anchors_agent_contracts.py @@ -44,7 +44,7 @@ async def _prepared_catalog( """ return await prepare_agent_catalog( tmp_path, monkeypatch, - (REPO_ROOT / "bundles/anchors.md" if bundle_dir == ANCHORS_DIR + (ANCHORS_DIR / "bundle.md" if bundle_dir == ANCHORS_DIR else REPO_ROOT / "behaviors/amp-dev.yaml").as_uri() ) @@ -261,7 +261,7 @@ async def test_amp_dev_readme_include_example_matches_runtime_source_order( ) -> None: """The documented include order is parsed and compared to the loaded bundle.""" includes = yaml.safe_load( - (REPO_ROOT / "bundles/anchors-amp-dev.md").read_text().split("---", 2)[1])["includes"] + (AMP_DEV_DIR / "bundle.md").read_text().split("---", 2)[1])["includes"] readme = (AMP_DEV_DIR / "README.md").read_text(encoding="utf-8") match = re.search(r"```yaml\n(?Pincludes:\n.*?\n)```", readme, re.DOTALL) assert match, "README must contain a YAML includes example" diff --git a/tests/test_anchors_bundles_dry.py b/tests/test_anchors_bundles_dry.py index 38dbe64..637be10 100644 --- a/tests/test_anchors_bundles_dry.py +++ b/tests/test_anchors_bundles_dry.py @@ -69,15 +69,6 @@ "docs", } -# The anchors include must be a full git URL with a #subdirectory= fragment, not -# a bare `anchors` name. A bare name resolves only where the CLI happens to have -# registered that namespace; the full URL keeps the bundle liftable, which is -# the "self-contained by design" convention the anchors README states. -ANCHORS_INCLUDE_RE = re.compile( - r"^git\+https://github\.com/microsoft/amplifier-foundation@[^#]+" - r"#subdirectory=bundles/anchors(?:/bundle\.md)?$" -) - _FRONTMATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL) @@ -162,21 +153,37 @@ def test_declares_no_session_tools_or_hooks(self) -> None: A duplicate block here is how the two bundles' orchestrator, context window, tool roster or hook set drift apart without anyone editing both. """ - frontmatter = _frontmatter(BUNDLES_DIR / "anchors-amp-dev.md") + frontmatter = _frontmatter(AMP_DEV_DIR / "bundle.md") declared = [k for k in ("session", "tools", "hooks", "agents", "context") if k in frontmatter] assert not declared, ( - f"bundles/anchors-amp-dev.md re-declares {declared}; inherit the " + f"bundles/anchors-amp-dev/bundle.md re-declares {declared}; inherit the " "host from Anchors and the entire capability from amp-dev." ) def test_includes_own_anchors_and_shared_behavior(self) -> None: """Same-repository includes resolve through the containing namespace.""" - includes = _frontmatter(BUNDLES_DIR / "anchors-amp-dev.md").get("includes") or [] + frontmatter = _frontmatter(AMP_DEV_DIR / "bundle.md") + assert "namespace_root" not in frontmatter["bundle"] + includes = frontmatter.get("includes") or [] sources = [ entry.get("bundle") if isinstance(entry, dict) else entry for entry in includes ] - assert sources == ["foundation:bundles/anchors.md", "foundation:behaviors/amp-dev.yaml"] + assert sources == [ + "anchors-amp-dev:../anchors/bundle.md", + "anchors-amp-dev:../../behaviors/amp-dev.yaml", + ] + + def test_nested_manifests_are_the_only_entrypoints(self) -> None: + assert not (BUNDLES_DIR / "anchors.md").exists() + assert not (BUNDLES_DIR / "anchors-amp-dev.md").exists() + assert "namespace_root" not in _frontmatter(ANCHORS_DIR / "bundle.md")["bundle"] + for directory in (ANCHORS_DIR, AMP_DEV_DIR): + frontmatter = _frontmatter(directory / "bundle.md") + assert frontmatter["bundle"]["version"] == "0.3.0" + assert (directory / "bundle.md").read_text().split("---", 2)[2].strip() == ( + "@anchors:context/system.md" + ) class TestNoParallelCopies: diff --git a/tests/test_behavior_first_guidance.py b/tests/test_behavior_first_guidance.py index dc2488a..887efaa 100644 --- a/tests/test_behavior_first_guidance.py +++ b/tests/test_behavior_first_guidance.py @@ -11,13 +11,13 @@ REPO_ROOT = Path(__file__).parent.parent GUIDE = REPO_ROOT / "docs" / "BUNDLE_GUIDE.md" -ANCHORS_MANIFEST = REPO_ROOT / "bundles" / "anchors.md" +ANCHORS_MANIFEST = REPO_ROOT / "bundles" / "anchors" / "bundle.md" README = REPO_ROOT / "README.md" APPLICATION_GUIDE = REPO_ROOT / "docs" / "APPLICATION_INTEGRATION_GUIDE.md" API_REFERENCE = REPO_ROOT / "docs" / "API_REFERENCE.md" ANCHORS_URI = ( "git+https://github.com/microsoft/amplifier-foundation@main" - "#subdirectory=bundles/anchors.md" + "#subdirectory=bundles/anchors/bundle.md" ) diff --git a/tests/test_delegation_defaults.py b/tests/test_delegation_defaults.py index 8b9d282..07ebe33 100644 --- a/tests/test_delegation_defaults.py +++ b/tests/test_delegation_defaults.py @@ -14,7 +14,7 @@ @pytest.mark.asyncio @pytest.mark.parametrize( - "source", ["bundles/anchors.md", "behaviors/agents.yaml"] + "source", ["bundles/anchors/bundle.md", "behaviors/agents.yaml"] ) async def test_public_delegate_configs_do_not_enable_limits(tmp_path, monkeypatch, source): prepared, _bundle = await prepare_agent_catalog( diff --git a/tests/test_description_alignment_checks.py b/tests/test_description_alignment_checks.py index b288624..052ee97 100644 --- a/tests/test_description_alignment_checks.py +++ b/tests/test_description_alignment_checks.py @@ -407,8 +407,8 @@ def test_the_engineered_bundles_stay_under_the_threshold() -> None: """anchors is the bundle the head-cost work actually engineered. It must pass.""" result = run_repo_step("bundle-head-cost", REPO_ROOT) by_file = {d["file"]: d for d in result["bundle_details"]} - anchors = by_file["bundles/anchors.md"] - amp_dev = by_file["bundles/anchors-amp-dev.md"] + anchors = by_file["bundles/anchors/bundle.md"] + amp_dev = by_file["bundles/anchors-amp-dev/bundle.md"] assert anchors["head_chars"] < HEAD_COST_WARN_CHARS assert amp_dev["head_chars"] < HEAD_COST_WARN_CHARS @@ -417,7 +417,7 @@ def test_visibility_disabled_skills_are_excluded_but_still_reported() -> None: """Turning skill visibility off is a real lever; the report must show what it bought.""" result = run_repo_step("bundle-head-cost", REPO_ROOT) anchors = next( - d for d in result["bundle_details"] if d["file"] == "bundles/anchors.md" + d for d in result["bundle_details"] if d["file"] == "bundles/anchors/bundle.md" ) assert anchors["skill_description_chars"] == 0 assert anchors["skill_description_chars_excluded"] > 0 diff --git a/tests/test_git_ops_ci_catalog.py b/tests/test_git_ops_ci_catalog.py index 5dae283..468d44a 100644 --- a/tests/test_git_ops_ci_catalog.py +++ b/tests/test_git_ops_ci_catalog.py @@ -68,7 +68,7 @@ ), ( "anchors:git-ops", - "bundles/anchors.md", + "bundles/anchors/bundle.md", Path("bundles/anchors/agents/git-ops.md"), Path("agents/git-ops.md"), ), @@ -84,8 +84,8 @@ def _agent_source(path: Path) -> tuple[str, str]: async def _prepared_catalog( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, subdirectory: str ): - """Prepare canonical declarations without includes; recursive wrapper - equivalence is covered by test_amp_dev_composition with the local candidate. + """Prepare canonical declarations without includes; recursive composition + is covered by test_amp_dev_composition with the local candidate. """ uri = CATALOG_ROOT.as_uri() if subdirectory != ".": @@ -150,7 +150,7 @@ async def test_anchors_prepared_catalog_excludes_foundation_git_ops( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Anchors' independently live catalog does not inherit Foundation's one.""" - prepared = await _prepared_catalog(tmp_path, monkeypatch, "bundles/anchors.md") + prepared = await _prepared_catalog(tmp_path, monkeypatch, "bundles/anchors/bundle.md") assert "anchors:git-ops" in prepared.mount_plan["agents"] assert "foundation:git-ops" not in prepared.mount_plan["agents"] diff --git a/tests/test_module_dep_resolvability_check.py b/tests/test_module_dep_resolvability_check.py index fcd1ff6..9002e03 100644 --- a/tests/test_module_dep_resolvability_check.py +++ b/tests/test_module_dep_resolvability_check.py @@ -201,7 +201,7 @@ def test_version_is_current(self, recipe_data): accumulate. """ data, _ = recipe_data - assert data["version"] == "3.17.1" + assert data["version"] == "3.17.2" def test_changelog_has_v3_11_0_entry(self, recipe_data): _, content = recipe_data diff --git a/tests/test_yaml_structure_lint.py b/tests/test_yaml_structure_lint.py index 38edbb8..320f106 100644 --- a/tests/test_yaml_structure_lint.py +++ b/tests/test_yaml_structure_lint.py @@ -361,8 +361,8 @@ class TestSingleBundleRecipeStructure: def test_version_is_current(self, single_bundle_recipe): """Version must match the recipe's current release.""" data, _ = single_bundle_recipe - assert data["version"] == "2.5.0", ( - f"Expected version '2.5.0', got '{data['version']}'" + assert data["version"] == "2.5.1", ( + f"Expected version '2.5.1', got '{data['version']}'" ) def test_yaml_structure_lint_step_exists(self, single_bundle_steps):